Skip to content
Merged
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
10 changes: 10 additions & 0 deletions App.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -52,13 +52,23 @@ public override void OnFrameworkInitializationCompleted()
}

public void OnOpenDashboardClick(object? sender, EventArgs e)
{
ShowMainWindow();
}

public void ShowMainWindow()
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
if (desktop.MainWindow == null)
{
desktop.MainWindow = new MainWindow();
}
if (desktop.MainWindow.DataContext is LocalLLMServerManager.Shared.ViewModels.MainViewModel vm)
{
vm.IsWindowVisible = true;
_ = vm.RefreshStatusCommand.ExecuteAsync(null);
}
try
{
desktop.MainWindow.Show();
Expand Down
21 changes: 17 additions & 4 deletions LocalLLMServerManager.Shared/ViewModels/CivitaiSearchViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Net.Http;
using System.Threading.Tasks;
using CommunityToolkit.Mvvm.ComponentModel;
Expand Down Expand Up @@ -88,8 +90,17 @@ private void InitStarterModels()

public void UpdateHardwareTelemetry(double totalVramMb, double totalRamMb)
{
if (totalVramMb > 0) TotalVramMb = totalVramMb;
if (totalRamMb > 0) TotalRamMb = totalRamMb;
double nextVram = totalVramMb > 0 ? totalVramMb : TotalVramMb;
double nextRam = totalRamMb > 0 ? totalRamMb : TotalRamMb;
if (nextVram == TotalVramMb && nextRam == TotalRamMb &&
CivitaiResults.All(r => r.FitBadge != null) &&
StarterModels.All(s => s.FitBadge != null))
{
return;
}

TotalVramMb = nextVram;
TotalRamMb = nextRam;

RecomputeBadges();
}
Expand Down Expand Up @@ -238,7 +249,8 @@ public async Task LoadDefaultModelsAsync(string apiBase, HttpClient http)
[RelayCommand]
public async Task SearchCivitaiAsync()
{
await SearchCivitaiAsync(ApiBase, HttpHelper.CreateClient(ApiBase));
using var client = HttpHelper.CreateClient(ApiBase);
await SearchCivitaiAsync(ApiBase, client);
}

public async Task SearchCivitaiAsync(string apiBase, HttpClient http)
Expand Down Expand Up @@ -269,7 +281,8 @@ public async Task SearchCivitaiAsync(string apiBase, HttpClient http)
[RelayCommand]
public async Task DownloadCivitaiModelAsync(CivitaiModelItem item)
{
await DownloadCivitaiModelAsync(item, ApiBase, HttpHelper.CreateClient(ApiBase));
using var client = HttpHelper.CreateClient(ApiBase);
await DownloadCivitaiModelAsync(item, ApiBase, client);
}

public async Task DownloadCivitaiModelAsync(CivitaiModelItem item, string apiBase, HttpClient http)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -96,8 +96,15 @@ public HuggingFaceSearchViewModel(

public void UpdateHardwareTelemetry(double totalVramMb, double totalRamMb)
{
if (totalVramMb > 0) TotalVramMb = totalVramMb;
if (totalRamMb > 0) TotalRamMb = totalRamMb;
double nextVram = totalVramMb > 0 ? totalVramMb : TotalVramMb;
double nextRam = totalRamMb > 0 ? totalRamMb : TotalRamMb;
if (nextVram == TotalVramMb && nextRam == TotalRamMb && HuggingFaceResults.All(r => r.FitBadge != null))
{
return;
}

TotalVramMb = nextVram;
TotalRamMb = nextRam;

RecomputeBadges();
}
Expand Down Expand Up @@ -497,7 +504,8 @@ public void InspectQuantFile(HfFileQuantItem? file)
[RelayCommand]
public async Task SearchHuggingFaceAsync()
{
await SearchHuggingFaceAsync(ApiBase, HttpHelper.CreateClient(ApiBase));
using var client = HttpHelper.CreateClient(ApiBase);
await SearchHuggingFaceAsync(ApiBase, client);
}

[RelayCommand]
Expand Down Expand Up @@ -602,14 +610,16 @@ public void OpenInBrowser(string? repoId)
public async Task OpenHfModalAsync(HuggingFaceRepoItem? item)
{
if (item == null || string.IsNullOrWhiteSpace(item.Id)) return;
await OpenHfModalAsync(item.Id, ApiBase, HttpHelper.CreateClient(ApiBase));
using var client = HttpHelper.CreateClient(ApiBase);
await OpenHfModalAsync(item.Id, ApiBase, client);
}

[RelayCommand]
public async Task DownloadHfFileAsync(HfFileQuantItem? file)
{
if (file == null || string.IsNullOrWhiteSpace(file.Filename) || string.IsNullOrWhiteSpace(ModalRepoId)) return;
await DownloadHfFileAsync(file, ApiBase, HttpHelper.CreateClient(ApiBase));
using var client = HttpHelper.CreateClient(ApiBase);
await DownloadHfFileAsync(file, ApiBase, client);
}

public async Task DownloadHfFileAsync(HfFileQuantItem file, string apiBase, HttpClient http)
Expand Down
30 changes: 26 additions & 4 deletions LocalLLMServerManager.Shared/ViewModels/MainViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1078,6 +1078,18 @@ public async Task LaunchTestFlightAsync(object? modalityParam = null)
}
}

[ObservableProperty] private bool _isWindowVisible = true;

public void ReleaseTransientBuffers()
{
StudioGeneratedImageBytes = null;
AttachedImageBytes = null;
StickerStudio.GeneratedPngBytes = null;
StickerStudio.InputImageBytes = null;
}

private int _pollTickCount = 0;

private async Task StartBackgroundPollingAsync()
{
while (EnableAutomaticPolling)
Expand All @@ -1086,17 +1098,27 @@ private async Task StartBackgroundPollingAsync()
{
await Task.Delay(5000);
if (!EnableAutomaticPolling) break;
await RefreshStatusAsync();
if (!IsWindowVisible) continue;
_pollTickCount++;
await RefreshStatusCoreAsync(isBackgroundPoll: true);
}
catch { }
}
}

[RelayCommand]
public async Task RefreshStatusAsync()
public Task RefreshStatusAsync() => RefreshStatusCoreAsync(isBackgroundPoll: false);

private async Task RefreshStatusCoreAsync(bool isBackgroundPoll)
{
await Telemetry.RefreshStatusAsync(ApiBase, ComfyUiUrl, Http);
await Ollama.LoadInstalledModelsAsync(ApiBase, Http);
var apiBase = ApiBase;
var comfyUiUrl = ComfyUiUrl;
var http = Http;
await Telemetry.RefreshStatusAsync(apiBase, comfyUiUrl, http);
if (!isBackgroundPoll || Ollama.InstalledModels.Count == 0 || _pollTickCount % 12 == 0)
{
await Ollama.LoadInstalledModelsAsync(apiBase, http);
}
if (Telemetry != null)
{
IsForgeOnline = Telemetry.IsForgeOnline;
Expand Down
95 changes: 79 additions & 16 deletions LocalLLMServerManager.Shared/ViewModels/OllamaLibraryViewModel.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Text.Json;
using System.Text.Json.Nodes;
Expand Down Expand Up @@ -63,6 +64,8 @@ public partial class OllamaLibraryViewModel : ObservableObject
public Action<string, string>? OnInspectModelRequested { get; set; }
public Action<string, int>? OnApplyModelContextRequested { get; set; }

private bool _suppressFilter;

public OllamaLibraryViewModel(IOllamaModelService ollamaModelService)
: this(ollamaModelService, new CanIRunItService(), null)
{
Expand All @@ -76,26 +79,44 @@ public OllamaLibraryViewModel(
_ollamaModelService = ollamaModelService;
_canIRunItService = canIRunItService ?? new CanIRunItService();
_telemetryService = telemetryService;
InstalledModels.CollectionChanged += (s, e) => ApplyFilter();
InstalledModels.CollectionChanged += (s, e) =>
{
if (!_suppressFilter) ApplyFilter();
};
RecalculateKvCache();
}

public void UpdateHardwareTelemetry(double totalVramMb, double totalRamMb)
{
if (totalVramMb > 0) TotalVramMb = totalVramMb;
if (totalRamMb > 0) TotalRamMb = totalRamMb;
double nextVram = totalVramMb > 0 ? totalVramMb : TotalVramMb;
double nextRam = totalRamMb > 0 ? totalRamMb : TotalRamMb;
if (nextVram == TotalVramMb && nextRam == TotalRamMb && InstalledModels.All(m => m.FitBadge != null))
{
return;
}

TotalVramMb = nextVram;
TotalRamMb = nextRam;

RecomputeBadges();
RecalculateKvCache();
}

public void RecomputeBadges()
{
for (int i = 0; i < InstalledModels.Count; i++)
_suppressFilter = true;
try
{
for (int i = 0; i < InstalledModels.Count; i++)
{
var m = InstalledModels[i];
var badge = _canIRunItService.EvaluateQuickFit(m.Name, m.SizeBytes > 0 ? m.SizeBytes : null, "LLM", (long)TotalVramMb, (long)TotalRamMb);
InstalledModels[i] = m with { FitBadge = badge };
}
}
finally
{
var m = InstalledModels[i];
var badge = _canIRunItService.EvaluateQuickFit(m.Name, m.SizeBytes > 0 ? m.SizeBytes : null, "LLM", (long)TotalVramMb, (long)TotalRamMb);
InstalledModels[i] = m with { FitBadge = badge };
_suppressFilter = false;
}
ApplyFilter();
}
Expand Down Expand Up @@ -246,10 +267,18 @@ public void SelectModel(OllamaModelItem? model)
{
if (model == null) return;
SelectedInstalledModel = model;
for (int i = 0; i < InstalledModels.Count; i++)
_suppressFilter = true;
try
{
for (int i = 0; i < InstalledModels.Count; i++)
{
var m = InstalledModels[i];
InstalledModels[i] = m with { IsSelected = (m.Name == model.Name) };
}
}
finally
{
var m = InstalledModels[i];
InstalledModels[i] = m with { IsSelected = (m.Name == model.Name) };
_suppressFilter = false;
}
ApplyFilter();
RecalculateKvCache();
Expand Down Expand Up @@ -311,12 +340,44 @@ public async Task LoadInstalledModelsAsync(string apiBase, HttpClient http)
try
{
var models = await _ollamaModelService.LoadInstalledModelsAsync(apiBase, http);
InstalledModels.Clear();
foreach (var m in models)
bool isSame = models.Count == InstalledModels.Count;
if (isSame)
{
var badge = _canIRunItService.EvaluateQuickFit(m.Name, m.SizeBytes > 0 ? m.SizeBytes : null, "LLM", (long)TotalVramMb, (long)TotalRamMb);
InstalledModels.Add(m with { FitBadge = badge });
for (int i = 0; i < models.Count; i++)
{
var existing = InstalledModels[i];
var incoming = models[i];
if (existing.Name != incoming.Name ||
existing.SizeBytes != incoming.SizeBytes ||
existing.CapabilityTag != incoming.CapabilityTag ||
existing.FormatSize != incoming.FormatSize)
{
isSame = false;
break;
}
}
}

if (isSame)
{
return;
}

_suppressFilter = true;
try
{
InstalledModels.Clear();
foreach (var m in models)
{
var badge = _canIRunItService.EvaluateQuickFit(m.Name, m.SizeBytes > 0 ? m.SizeBytes : null, "LLM", (long)TotalVramMb, (long)TotalRamMb);
InstalledModels.Add(m with { FitBadge = badge });
}
}
finally
{
_suppressFilter = false;
}

if (SelectedInstalledModel == null && InstalledModels.Count > 0)
{
SelectModel(InstalledModels[0]);
Expand Down Expand Up @@ -381,7 +442,8 @@ public async Task DeleteModelAsync(OllamaModelItem? item)
ToastService.Instance.Show($"Deleting model '{item.Name}'...", ToastType.Info);
try
{
var success = await _ollamaModelService.DeleteModelAsync(ApiBase, item.Name, HttpHelper.CreateClient(ApiBase));
using var client = HttpHelper.CreateClient(ApiBase);
var success = await _ollamaModelService.DeleteModelAsync(ApiBase, item.Name, client);
if (success)
{
InstalledModels.Remove(item);
Expand All @@ -402,7 +464,8 @@ public async Task DeleteModelAsync(OllamaModelItem? item)
[RelayCommand]
public async Task UnloadAllVramAsync()
{
await UnloadAllVramAsync(ApiBase, HttpHelper.CreateClient(ApiBase));
using var client = HttpHelper.CreateClient(ApiBase);
await UnloadAllVramAsync(ApiBase, client);
}

public async Task UnloadAllVramAsync(string apiBase, HttpClient http)
Expand Down
17 changes: 17 additions & 0 deletions LocalLLMServerManager.Tests/MainViewModelTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -669,5 +669,22 @@ public void MainViewModel_OrchestratesNavigationRailAndStickerStudio()
Assert.True(vm.IsCopilotSidebarOpen);
Assert.Equal("Assistant", vm.SelectedCopilotTab);
}

[Fact]
public void ReleaseTransientBuffers_ClearsImageByteArrays()
{
var vm = new MainViewModel();
vm.StudioGeneratedImageBytes = new byte[] { 1, 2, 3 };
vm.AttachedImageBytes = new byte[] { 4, 5, 6 };
vm.StickerStudio.GeneratedPngBytes = new byte[] { 7, 8, 9 };
vm.StickerStudio.InputImageBytes = new byte[] { 10, 11, 12 };

vm.ReleaseTransientBuffers();

Assert.Null(vm.StudioGeneratedImageBytes);
Assert.Null(vm.AttachedImageBytes);
Assert.Null(vm.StickerStudio.GeneratedPngBytes);
Assert.Null(vm.StickerStudio.InputImageBytes);
}
}

Loading
Loading