diff --git a/CHANGELOG.md b/CHANGELOG.md index 0f0ecf704..ae12ceda2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,27 @@ All notable changes to Stability Matrix will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), and this project adheres to [Semantic Versioning 2.0](https://semver.org/spec/v2.0.0.html). +## v2.16.4 +### Added +- Added **Comfy Kitchen Attention** (`--use-ck-attention`) as a **Cross Attention Method** launch option for ComfyUI and ComfyUI-Zluda - thanks to @e-nord! +### Changed +- **Windows ROCm** PyTorch installs now come from AMD's new permanent ROCm 10 package repositories, avoiding the `CUDA error: invalid argument` / `hipErrorInvalidValue` failures reported with the ROCm 7.14.1 builds — packages already on 7.14.1 need a reinstall, or an update that also upgrades PyTorch (like ComfyUI's) - thanks to @NeuralFault! +### Fixed +- Installing a package into a folder that already contains files — like an existing ComfyUI installation placed in `Data\Packages` to be imported — now shows the folder's total size and file count and requires explicit confirmation, instead of silently deleting everything in it; this includes the first-run one-click installer ([#1733](https://github.com/LykosAI/StabilityMatrix/issues/1733)) +- Turning off shared model folders no longer tries to delete real directories at the link locations — only links created by Stability Matrix are removed, so the model folders of an imported package are never touched ([#1733](https://github.com/LykosAI/StabilityMatrix/issues/1733)) +- The package uninstall confirmation now correctly lists models/checkpoints among the items that will be deleted for packages using config-based model sharing (ComfyUI's default) — previously it implied they were safe — and shows the package folder's total size +- Fixed the **Workflows** page loading forever (and eventually freezing the app) when the workflow library folder is itself a link to ComfyUI's `user\default\workflows` folder — the `Stability Matrix` link ComfyUI gets at launch pointed back into the same folder, nesting `Stability Matrix\Stability Matrix\...` thousands of levels deep. That link is no longer created (and an existing one is removed the next time ComfyUI is launched) when it would loop, and the workflow and model folder scans now follow linked folders at most once +- Fixed the **Cross Attention Method** launch options for ComfyUI and ComfyUI-Zluda letting you enable several at once — they're mutually exclusive, so they're now radio buttons - thanks to @e-nord! +- Fixed [#1725](https://github.com/LykosAI/StabilityMatrix/issues/1725) - **AI-Toolkit** jobs crashing with a `distutils` error on installs from before v2.16.3 that still run Python 3.11, no reinstall needed - thanks to @NeuralFault! +- Fixed Wan2GP v13's **Deepy** panel repeatedly showing **Connection to server lost** and logging `issubclass() arg 1 must be a class` — Stability Matrix's console logging wrapper now preserves Gradio's exception class ([Wan2GP #2298](https://github.com/deepbeepmeep/Wan2GP/issues/2298)) +- Fixed Wan2GP (and AI Toolkit's SageAttention source build) crashing with `ModuleNotFoundError: No module named 'distutils'` on Python 3.12+ — forcing `SETUPTOOLS_USE_DISTUTILS=stdlib` unconditionally broke on versions where distutils was removed from the standard library entirely, it's now version-gated the same way AI Toolkit's own fix already was +- Environment Variables configured in **Settings** now correctly override the workaround variables (like `SETUPTOOLS_USE_DISTUTILS`) that some packages set internally +### Supporters +#### 🌟 Visionaries +Small release, big priority: 2.16.4 is mostly about keeping your files exactly where you left them. Imported ComfyUI folders can't be wiped by surprise anymore, and a looping workflow link can't freeze the app. That kind of careful, unflashy work only gets our full attention because of our Visionaries, so thank you **Waterclouds**, **MrMxyzptlk12836**, **bluepopsicle**, **Ibixat**, **Droolguy**, **snotty**, **dispenser**, **cusalapapen1481**, **moon_milky2843**, **sn3232323233350**, **CC**, **TwistedDragon**, **hasezou11013179**, and **Akros**. Every folder that stays put has a bit of you in it. 💛 +#### 🚀 Pioneers +To our Pioneers **SinthCore**, **Jisuren**, **jweg79**, **Hurbie53**, **Cjloha**, **Alligator1907**, **CommissarGiygas16050**, **bastardofbethlehem**, **Zombop**, **Silerae**, **joshsciascia72**, and **cy_hart691842**: patch releases like this one land quietly, and you back them anyway. We never take that for granted. And hello to **okachako**, our newest Pioneer; it's so nice to see your name here! 💛 + ## v2.16.3 ### Added #### New Feature: 🔀 CivitAI Workflow Browsing diff --git a/README.md b/README.md index a204dc7ef..e7d8cb099 100644 --- a/README.md +++ b/README.md @@ -49,6 +49,8 @@ Multi-Platform Package Manager and Inference UI for Stable Diffusion +See the [documentation index](docs/README.md) for installation, package management, models, and Inference guides. Start with [your first image](docs/inference/text-to-image.md), [importing local models](docs/checkpoint-manager/overview.md#import-local-models), or [managing installed packages](docs/package-manager/managing-packages.md). + ### 🖱️ One click install and update for Stable Diffusion Web UI Packages - Supports: - [Stable Diffusion WebUI reForge][reforge], [Stable Diffusion WebUI Forge][forge], [Stable Diffusion WebUI AMDGPU Forge][amdforge] [Automatic 1111][auto1111], [Automatic 1111 DirectML][auto1111-directml], [SD Web UI-UX][webui-ux], [SD.Next][sdnext] diff --git a/StabilityMatrix.Avalonia/DialogHelper.cs b/StabilityMatrix.Avalonia/DialogHelper.cs index 90f3e50a9..22c581a30 100644 --- a/StabilityMatrix.Avalonia/DialogHelper.cs +++ b/StabilityMatrix.Avalonia/DialogHelper.cs @@ -2,11 +2,13 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.IO; using System.Linq; using System.Runtime.CompilerServices; using System.Text; using System.Text.Json; using System.Threading.Tasks; +using AsyncAwaitBestPractices; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.Documents; @@ -20,19 +22,25 @@ using AvaloniaEdit.TextMate; using CommunityToolkit.Mvvm.Input; using FluentAvalonia.UI.Controls; +using Microsoft.Extensions.DependencyInjection; using NLog; using Refit; using StabilityMatrix.Avalonia.Controls; using StabilityMatrix.Avalonia.Helpers; using StabilityMatrix.Avalonia.Languages; using StabilityMatrix.Avalonia.Models; +using StabilityMatrix.Avalonia.Services; +using StabilityMatrix.Avalonia.ViewModels.Base; +using StabilityMatrix.Avalonia.ViewModels.Dialogs; using StabilityMatrix.Core.Exceptions; using StabilityMatrix.Core.Extensions; using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.FileInterfaces; using StabilityMatrix.Core.Services; using TextMateSharp.Grammars; using Process = FuzzySharp.Process; +using Size = StabilityMatrix.Core.Helper.Size; namespace StabilityMatrix.Avalonia; @@ -638,6 +646,77 @@ public static TaskDialog CreateTaskDialog(string title, string description) XamlRoot = App.VisualRoot, }; } + + /// + /// Shows a confirmation dialog before deleting existing files at a package install location, + /// e.g. a folder the user placed there manually to import an existing installation. + /// Returns true if the location does not exist, is empty, or the user explicitly confirmed + /// the deletion; false if the user cancelled. + /// + public static async Task ConfirmDeleteExistingInstallDataAsync(DirectoryPath installLocation) + { + if (!installLocation.Exists || !installLocation.Info.EnumerateFileSystemInfos().Any()) + return true; + + var vmFactory = App.Services.GetRequiredService>(); + + var vm = vmFactory.Get(); + vm.Title = "Existing files found"; + vm.Description = + "This folder already exists and contains files that were not installed by Stability Matrix. " + + "Installing here will permanently delete the folder shown below and everything inside it.\n\n" + + "If this is an existing installation you want to keep, choose Cancel and use Import " + + "from the Packages page instead, or move your data elsewhere first."; + vm.PathsToDelete = [installLocation.FullPath]; + vm.IsRecycleBinAvailable = false; + vm.TotalSizeText = "Calculating size…"; + + Task.Run(() => + { + var (fileCount, totalBytes) = GetFileStats(installLocation.Info); + Dispatcher.UIThread.Post(() => + vm.TotalSizeText = + $"Total: {Size.FormatBytes(Convert.ToUInt64(totalBytes))} in {fileCount:N0} files" + ); + }) + .SafeFireAndForget(ex => + { + Logger.Warn(ex, "Failed to calculate size of {Path}", installLocation.FullPath); + Dispatcher.UIThread.Post(() => vm.TotalSizeText = null); + }); + + return await vm.GetDialog().ShowAsync() == ContentDialogResult.Primary; + } + + /// + /// Gets the total file count and size of a directory, without following symbolic links / junctions. + /// + private static (int FileCount, long TotalBytes) GetFileStats(DirectoryInfo directory) + { + var fileCount = 0; + var totalBytes = 0L; + + foreach (var file in directory.EnumerateFiles()) + { + if (file.Attributes.HasFlag(FileAttributes.ReparsePoint)) + continue; + + fileCount++; + totalBytes += file.Length; + } + + foreach (var subDir in directory.EnumerateDirectories()) + { + if (subDir.Attributes.HasFlag(FileAttributes.ReparsePoint)) + continue; + + var (subCount, subBytes) = GetFileStats(subDir); + fileCount += subCount; + totalBytes += subBytes; + } + + return (fileCount, totalBytes); + } } // Text fields diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/ConfirmDeleteDialogViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/ConfirmDeleteDialogViewModel.cs index 5da0e0300..1823024c0 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/ConfirmDeleteDialogViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/ConfirmDeleteDialogViewModel.cs @@ -21,6 +21,18 @@ public partial class ConfirmDeleteDialogViewModel(ILogger + /// Optional description shown below the title, for extra context on what is being deleted and why. + /// + [ObservableProperty] + private string? description; + + /// + /// Optional total size / file count line shown below the paths list. + /// + [ObservableProperty] + private string? totalSizeText; + [ObservableProperty] [NotifyPropertyChangedFor(nameof(ConfirmDeleteButtonText))] [NotifyPropertyChangedFor(nameof(IsPermanentDelete))] diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/ConfirmPackageDeleteDialogViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/ConfirmPackageDeleteDialogViewModel.cs index 8a6a2c37a..cb60cb047 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/ConfirmPackageDeleteDialogViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/ConfirmPackageDeleteDialogViewModel.cs @@ -4,7 +4,9 @@ using StabilityMatrix.Avalonia.ViewModels.Base; using StabilityMatrix.Avalonia.Views.Dialogs; using StabilityMatrix.Core.Attributes; +using StabilityMatrix.Core.Helper; using StabilityMatrix.Core.Models; +using StabilityMatrix.Core.Models.FileInterfaces; namespace StabilityMatrix.Avalonia.ViewModels.Dialogs; @@ -21,6 +23,9 @@ public partial class ConfirmPackageDeleteDialogViewModel : ContentDialogViewMode [NotifyPropertyChangedFor(nameof(IsValid))] public partial string PackageName { get; set; } = string.Empty; + [ObservableProperty] + public partial string? FolderSizeText { get; set; } + public string? ExpectedPackageName => Package.DisplayName; public bool IsValid => ExpectedPackageName?.Equals(PackageName, StringComparison.Ordinal) ?? false; public string DeleteWarningText @@ -36,7 +41,9 @@ public string DeleteWarningText if (!Package.UseSharedOutputFolder) items.Add("• Images/outputs"); - if (Package.PreferredSharedFolderMethod is SharedFolderMethod.None) + // Symlink mode is the only one that relocates models out of the package folder; + // in Configuration (yaml) and None modes they are real files that will be deleted + if (Package.PreferredSharedFolderMethod is not SharedFolderMethod.Symlink) items.Add("• Models/checkpoints placed in the package's model folders"); items.Add("• Any custom files in the package folder"); @@ -50,4 +57,23 @@ private async Task CopyExpectedPackageName() { await App.Clipboard?.SetTextAsync(ExpectedPackageName); } + + /// + public override async Task OnLoadedAsync() + { + await base.OnLoadedAsync(); + + if (Package.FullPath is not { } fullPath) + return; + + try + { + var sizeBytes = await new DirectoryPath(fullPath).GetSizeAsync(includeSymbolicLinks: false); + FolderSizeText = $"Total size: {Size.FormatBytes(Convert.ToUInt64(sizeBytes))}"; + } + catch (Exception) + { + // Size display is informational only + } + } } diff --git a/StabilityMatrix.Avalonia/ViewModels/Dialogs/NewOneClickInstallViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/Dialogs/NewOneClickInstallViewModel.cs index 596ad32ff..2997c5871 100644 --- a/StabilityMatrix.Avalonia/ViewModels/Dialogs/NewOneClickInstallViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/Dialogs/NewOneClickInstallViewModel.cs @@ -6,6 +6,7 @@ using System.Threading; using System.Threading.Tasks; using AsyncAwaitBestPractices; +using Avalonia.Controls.Notifications; using Avalonia.Threading; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -121,9 +122,49 @@ private async Task InstallComfyForInference() [RelayCommand] private async Task InstallPackage(BasePackage selectedPackage) { - OnPrimaryButtonClick(); - var installLocation = Path.Combine(settingsManager.LibraryDir, "Packages", selectedPackage.Name); + var installPath = new DirectoryPath(installLocation); + + // In the first-run flow the progress dialog is opened when the recommended-models dialog + // closes; the existing-data path skips that dialog, so it opens the progress dialog itself + var showProgressDialogOnStart = false; + + if (installPath.Exists && installPath.Info.EnumerateFileSystemInfos().Any()) + { + // The install location already holds data we didn't put there — likely an existing + // installation the user placed for Import. Close this dialog first so the + // confirmation is the only dialog on screen, then require explicit confirmation + // before deleting anything. + OnCloseButtonClick(); + + if (!await DialogHelper.ConfirmDeleteExistingInstallDataAsync(installPath)) + { + notificationService.Show( + "Installation cancelled", + $"Existing files at {installLocation} were left untouched.", + NotificationType.Information + ); + return; + } + + await installPath.DeleteVerboseAsync(logger); + + // The Packages page indexed the staged folder as an unknown package; re-index now + // that it's gone so the stale card doesn't linger during the install + EventManager.Instance.OnInstalledPackagesChanged(); + + showProgressDialogOnStart = true; + } + else + { + OnPrimaryButtonClick(); + + if (installPath.Exists) + { + await installPath.DeleteVerboseAsync(logger); + } + } + var recommendedPython = selectedPackage.RecommendedPythonVersion; var steps = new List @@ -132,13 +173,6 @@ private async Task InstallPackage(BasePackage selectedPackage) new SetupPrerequisitesStep(prerequisiteHelper, selectedPackage, recommendedPython), }; - // get latest version & download & install - if (Directory.Exists(installLocation)) - { - var installPath = new DirectoryPath(installLocation); - await installPath.DeleteVerboseAsync(logger); - } - var downloadVersion = await selectedPackage.GetLatestVersion(); var installedVersion = new InstalledPackageVersion { IsPrerelease = false }; @@ -208,7 +242,7 @@ private async Task InstallPackage(BasePackage selectedPackage) var runner = new PackageModificationRunner { - ShowDialogOnStart = false, + ShowDialogOnStart = showProgressDialogOnStart, HideCloseButton = false, ModificationCompleteMessage = $"{selectedPackage.DisplayName} installed successfully", }; diff --git a/StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs index 97edd6be9..8c0a3a454 100644 --- a/StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/InstalledWorkflowsViewModel.cs @@ -84,11 +84,7 @@ private async Task LoadInstalledWorkflowsAsync() var count = 0; foreach ( - var workflowPath in Directory.EnumerateFiles( - settingsManager.WorkflowDirectory, - "*.json", - EnumerationOptionConstants.AllDirectories - ) + var workflowPath in LinkSafeFileSystem.EnumerateFiles(settingsManager.WorkflowDirectory, "*.json") ) { try diff --git a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs index 55e092056..b8be9d5af 100644 --- a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageCardViewModel.cs @@ -1143,8 +1143,7 @@ partial void OnIsSharedModelSymlinkChanged(bool oldValue, bool newValue) } else { - var basePackage = packageFactory[Package!.PackageName!]; - basePackage!.RemoveModelFolderLinks(Package.FullPath!, SharedFolderMethod.Symlink); + RemoveModelFolderLinks(SharedFolderMethod.Symlink); } } @@ -1166,11 +1165,31 @@ partial void OnIsSharedModelConfigChanged(bool oldValue, bool newValue) } else { - var basePackage = packageFactory[Package!.PackageName!]; - basePackage!.RemoveModelFolderLinks(Package.FullPath!, SharedFolderMethod.Configuration); + RemoveModelFolderLinks(SharedFolderMethod.Configuration); } } + private void RemoveModelFolderLinks(SharedFolderMethod sharedFolderMethod) + { + var basePackage = packageFactory[Package!.PackageName!]; + basePackage! + .RemoveModelFolderLinks(Package.FullPath!, sharedFolderMethod) + .SafeFireAndForget(ex => + { + logger.LogError( + ex, + "Failed to remove {Method} model folder links for {Package}", + sharedFolderMethod, + Package.DisplayName + ); + notificationService.Show( + "Failed to remove shared model links", + ex.Message, + NotificationType.Error + ); + }); + } + partial void OnIsSharedModelDisabledChanged(bool value) { if (value) diff --git a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageInstallDetailViewModel.cs b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageInstallDetailViewModel.cs index 890651013..03af4f190 100644 --- a/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageInstallDetailViewModel.cs +++ b/StabilityMatrix.Avalonia/ViewModels/PackageManager/PackageInstallDetailViewModel.cs @@ -231,9 +231,15 @@ x.PackageName is nameof(ComfyUI) or "ComfyUI-Zluda" InstallName = InstallName.Trim(); var installLocation = Path.Combine(settingsManager.LibraryDir, "Packages", InstallName); - if (Directory.Exists(installLocation)) + var installPath = new DirectoryPath(installLocation); + + // Require explicit confirmation before deleting an existing non-empty folder at the + // install location — it may be a manually placed installation the user meant to Import + if (!await DialogHelper.ConfirmDeleteExistingInstallDataAsync(installPath)) + return; + + if (installPath.Exists) { - var installPath = new DirectoryPath(installLocation); await installPath.DeleteVerboseAsync(logger); } diff --git a/StabilityMatrix.Avalonia/Views/Dialogs/ConfirmDeleteDialog.axaml b/StabilityMatrix.Avalonia/Views/Dialogs/ConfirmDeleteDialog.axaml index 4040720a7..e6b9c54b6 100644 --- a/StabilityMatrix.Avalonia/Views/Dialogs/ConfirmDeleteDialog.axaml +++ b/StabilityMatrix.Avalonia/Views/Dialogs/ConfirmDeleteDialog.axaml @@ -17,10 +17,8 @@ d:DesignWidth="500" x:DataType="dialogs:ConfirmDeleteDialogViewModel" mc:Ignorable="d"> - - + + + + + +