diff --git a/desktop/scripts/fix-appimage.sh b/desktop/scripts/fix-appimage.sh
index 45f537bb72..973d9fbb62 100755
--- a/desktop/scripts/fix-appimage.sh
+++ b/desktop/scripts/fix-appimage.sh
@@ -114,6 +114,84 @@ echo "==> Symlinking system GStreamer plugin directory"
rm -rf "$LIBDIR/gstreamer-1.0"
ln -s "/usr/lib/$MULTIARCH/gstreamer-1.0" "$LIBDIR/gstreamer-1.0"
+# WebKitGTK 2.52 Skia's GPU path goes through Vulkan/radv. On non-conformant
+# AMD RDNA4 (gfx1200) that path paints nothing — transparent ghost window —
+# while WEBKIT_DISABLE_DMABUF_RENDERER does not help (#2643). Prefer Skia CPU
+# raster for AppImage launches; operators can override with =0.
+echo "==> Preferring WebKitGTK Skia CPU rendering for AppImage launches"
+HOOK_DIR="$WORKDIR/squashfs-root/apprun-hooks"
+mkdir -p "$HOOK_DIR"
+cat > "$HOOK_DIR/99-buzz-webkit-skia-cpu.sh" <<'EOF'
+# Allow operators to override (e.g. WEBKIT_SKIA_ENABLE_CPU_RENDERING=0) if needed.
+export WEBKIT_SKIA_ENABLE_CPU_RENDERING="${WEBKIT_SKIA_ENABLE_CPU_RENDERING:-1}"
+EOF
+if [[ -f "$WORKDIR/squashfs-root/AppRun" ]] \
+ && ! grep -q 'WEBKIT_SKIA_ENABLE_CPU_RENDERING' "$WORKDIR/squashfs-root/AppRun"; then
+ if ! grep -q 'apprun-hooks' "$WORKDIR/squashfs-root/AppRun"; then
+ tmp_apprun="$(mktemp)"
+ {
+ if head -n1 "$WORKDIR/squashfs-root/AppRun" | grep -q '^#!'; then
+ head -n1 "$WORKDIR/squashfs-root/AppRun"
+ echo 'export WEBKIT_SKIA_ENABLE_CPU_RENDERING="${WEBKIT_SKIA_ENABLE_CPU_RENDERING:-1}"'
+ tail -n +2 "$WORKDIR/squashfs-root/AppRun"
+ else
+ echo 'export WEBKIT_SKIA_ENABLE_CPU_RENDERING="${WEBKIT_SKIA_ENABLE_CPU_RENDERING:-1}"'
+ cat "$WORKDIR/squashfs-root/AppRun"
+ fi
+ } > "$tmp_apprun"
+ mv "$tmp_apprun" "$WORKDIR/squashfs-root/AppRun"
+ chmod +x "$WORKDIR/squashfs-root/AppRun"
+ fi
+fi
+
+# Fedora's COLRv1 Noto Color Emoji trips an assertion in WebKitGTK/Skia and
+# aborts the AppImage after a blank window (#2548). Reject that family so
+# emoji fall back to a non-COLRv1 face (or to monochrome glyphs).
+echo "==> Rejecting COLRv1 system color-emoji fonts for AppImage launches"
+FC_DIR="$WORKDIR/squashfs-root/usr/etc/fonts"
+mkdir -p "$FC_DIR"
+cat > "$FC_DIR/fonts.conf" <<'EOF'
+
+
+
+
+ /etc/fonts/fonts.conf
+
+
+
+ Noto Color Emoji
+
+
+
+
+EOF
+cat > "$HOOK_DIR/98-buzz-fontconfig-no-colrv1.sh" <<'EOF'
+# Prefer the AppImage fontconfig that rejects COLRv1 Noto Color Emoji (#2548).
+# Operators can point FONTCONFIG_FILE elsewhere to override.
+if [ -z "${FONTCONFIG_FILE:-}" ] && [ -n "${APPDIR:-}" ] && [ -f "$APPDIR/usr/etc/fonts/fonts.conf" ]; then
+ export FONTCONFIG_FILE="$APPDIR/usr/etc/fonts/fonts.conf"
+fi
+EOF
+if [[ -f "$WORKDIR/squashfs-root/AppRun" ]] \
+ && ! grep -q 'buzz-fontconfig-no-colrv1\|FONTCONFIG_FILE=.*usr/etc/fonts' "$WORKDIR/squashfs-root/AppRun"; then
+ if ! grep -q 'apprun-hooks' "$WORKDIR/squashfs-root/AppRun"; then
+ tmp_apprun="$(mktemp)"
+ {
+ if head -n1 "$WORKDIR/squashfs-root/AppRun" | grep -q '^#!'; then
+ head -n1 "$WORKDIR/squashfs-root/AppRun"
+ echo 'if [ -z "${FONTCONFIG_FILE:-}" ] && [ -n "${APPDIR:-}" ] && [ -f "$APPDIR/usr/etc/fonts/fonts.conf" ]; then export FONTCONFIG_FILE="$APPDIR/usr/etc/fonts/fonts.conf"; fi'
+ tail -n +2 "$WORKDIR/squashfs-root/AppRun"
+ else
+ echo 'if [ -z "${FONTCONFIG_FILE:-}" ] && [ -n "${APPDIR:-}" ] && [ -f "$APPDIR/usr/etc/fonts/fonts.conf" ]; then export FONTCONFIG_FILE="$APPDIR/usr/etc/fonts/fonts.conf"; fi'
+ cat "$WORKDIR/squashfs-root/AppRun"
+ fi
+ } > "$tmp_apprun"
+ mv "$tmp_apprun" "$WORKDIR/squashfs-root/AppRun"
+ chmod +x "$WORKDIR/squashfs-root/AppRun"
+ fi
+fi
+
echo "==> Repacking AppImage"
# Pass a pinned type2 runtime when provided (CI sets APPIMAGETOOL_RUNTIME_FILE);
# without it appimagetool downloads the runtime from its mutable `continuous`
diff --git a/desktop/src-tauri/src/lib.rs b/desktop/src-tauri/src/lib.rs
index 3688ee3a62..b09b00e88b 100644
--- a/desktop/src-tauri/src/lib.rs
+++ b/desktop/src-tauri/src/lib.rs
@@ -135,6 +135,14 @@ async fn wait_for_stable_initial_window_geometry(window: &tau
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
+ // WebKitGTK 2.52 Skia GPU/Vulkan paints nothing on non-conformant AMD RDNA4
+ // (radv gfx1200): transparent ghost window (#2643). Prefer CPU raster when
+ // unset; operators can force GPU with WEBKIT_SKIA_ENABLE_CPU_RENDERING=0.
+ #[cfg(target_os = "linux")]
+ if std::env::var_os("WEBKIT_SKIA_ENABLE_CPU_RENDERING").is_none() {
+ std::env::set_var("WEBKIT_SKIA_ENABLE_CPU_RENDERING", "1");
+ }
+
// mesh-llm's async chains (model download, node start/join) overflow
// tokio's default 2 MiB worker stacks — a stack-guard SIGABRT, not a
// panic. Upstream mesh-llm and mesh-console both run on 8 MiB worker
diff --git a/desktop/src-tauri/src/managed_agents/git_bash.rs b/desktop/src-tauri/src/managed_agents/git_bash.rs
index dab9e887c8..fdfc4e60e7 100644
--- a/desktop/src-tauri/src/managed_agents/git_bash.rs
+++ b/desktop/src-tauri/src/managed_agents/git_bash.rs
@@ -302,7 +302,9 @@ fn scan_path_for_command(
) -> Option {
let needs_exe = name.extension().is_none();
std::env::split_paths(path_env).find_map(|dir| {
- if system_root.is_some_and(|root| is_under_dir(&dir, root)) {
+ // System32 bash and %LOCALAPPDATA%\Microsoft\WindowsApps\bash.exe are
+ // WSL app-execution aliases, not Git Bash (#2685).
+ if system_root.is_some_and(|root| is_under_dir(&dir, root)) || is_windows_apps_dir(&dir) {
return None;
}
let candidate = dir.join(name);
@@ -320,6 +322,19 @@ fn scan_path_for_command(
})
}
+/// Windows exposes `bash.exe` app-execution aliases under WindowsApps that
+/// launch WSL rather than Git Bash. Skip those PATH entries so discovery does
+/// not boot `wsl.exe` on every unresolved command probe.
+#[cfg(windows)]
+fn is_windows_apps_dir(dir: &Path) -> bool {
+ dir.components().any(|component| {
+ component
+ .as_os_str()
+ .to_string_lossy()
+ .eq_ignore_ascii_case("WindowsApps")
+ })
+}
+
#[cfg(windows)]
fn is_under_dir(dir: &Path, root: &Path) -> bool {
let mut dir_components = dir.components();
@@ -501,6 +516,37 @@ mod tests {
);
}
+ #[test]
+ fn test_windows_apps_bash_alias_is_not_treated_as_git_bash() {
+ let temp = tempdir().expect("tempdir");
+ let windows_apps = temp.path().join("Microsoft").join("WindowsApps");
+ let alias = windows_apps.join("bash.exe");
+ let git = temp.path().join("Git").join("cmd").join("git.exe");
+ let bash = temp.path().join("Git").join("bin").join("bash.exe");
+ std::fs::create_dir_all(&windows_apps).expect("mkdir WindowsApps");
+ std::fs::create_dir_all(git.parent().expect("git parent")).expect("mkdir git");
+ std::fs::create_dir_all(bash.parent().expect("bash parent")).expect("mkdir bash");
+ std::fs::write(alias, []).expect("alias");
+ std::fs::write(&git, []).expect("git");
+ std::fs::write(&bash, []).expect("bash");
+
+ let path =
+ std::env::join_paths([windows_apps, git.parent().expect("cmd dir").to_path_buf()])
+ .expect("PATH");
+ assert_eq!(
+ resolve_git_bash_no_registry(
+ path.to_str().expect("utf8"),
+ None,
+ None,
+ None,
+ None,
+ None,
+ None,
+ ),
+ Some(bash)
+ );
+ }
+
#[test]
fn test_effective_buzz_shell_override_marks_agent_ready() {
let temp = tempdir().expect("tempdir");