Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,25 @@ 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))
### 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
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down
79 changes: 79 additions & 0 deletions StabilityMatrix.Avalonia/DialogHelper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;

Expand Down Expand Up @@ -638,6 +646,77 @@ public static TaskDialog CreateTaskDialog(string title, string description)
XamlRoot = App.VisualRoot,
};
}

/// <summary>
/// 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.
/// </summary>
public static async Task<bool> ConfirmDeleteExistingInstallDataAsync(DirectoryPath installLocation)
{
if (!installLocation.Exists || !installLocation.Info.EnumerateFileSystemInfos().Any())
return true;

var vmFactory = App.Services.GetRequiredService<IServiceManager<ViewModelBase>>();

var vm = vmFactory.Get<ConfirmDeleteDialogViewModel>();
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;
}

/// <summary>
/// Gets the total file count and size of a directory, without following symbolic links / junctions.
/// </summary>
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,18 @@ public partial class ConfirmDeleteDialogViewModel(ILogger<ConfirmDeleteDialogVie
[ObservableProperty]
private string title = "Confirm Delete";

/// <summary>
/// Optional description shown below the title, for extra context on what is being deleted and why.
/// </summary>
[ObservableProperty]
private string? description;

/// <summary>
/// Optional total size / file count line shown below the paths list.
/// </summary>
[ObservableProperty]
private string? totalSizeText;

[ObservableProperty]
[NotifyPropertyChangedFor(nameof(ConfirmDeleteButtonText))]
[NotifyPropertyChangedFor(nameof(IsPermanentDelete))]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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
Expand All @@ -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");
Expand All @@ -50,4 +57,23 @@ private async Task CopyExpectedPackageName()
{
await App.Clipboard?.SetTextAsync(ExpectedPackageName);
}

/// <inheritdoc />
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
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<IPackageStep>
Expand All @@ -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 };

Expand Down Expand Up @@ -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",
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}

Expand All @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}

Expand Down
Loading
Loading