Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
f9c82d5
docs: add design spec for floating documentation window and Hugging F…
spelech Sep 18, 2026
298b7c3
docs: add implementation plan for floating documentation and Hugging …
spelech Sep 18, 2026
0cd8c46
feat(docs): implement hybrid floating documentation overlay and nativ…
spelech Sep 18, 2026
b963c69
feat(hub): overhaul filter states, starter models, and active button …
spelech Sep 18, 2026
c6e3bf5
docs(spec): add design spec for collaborative UI bug hunting via Aval…
spelech Sep 19, 2026
d3d91ad
docs(plan): add implementation plan for collaborative UI debugging vi…
spelech Sep 19, 2026
bcd52d3
docs(spec): add design spec for collaborative UI bug hunting via Aval…
spelech Sep 19, 2026
13dd6dc
docs(plan): add implementation plan for collaborative UI debugging vi…
spelech Sep 19, 2026
1c07632
feat(debug): integrate AvaloniaMcp diagnostics, F12 DevTools, and UiD…
spelech Sep 19, 2026
967c39e
fix(debug): wire UiDiagnosticTraceListener to trace logging, register…
spelech Sep 19, 2026
7f8e543
merge: integrate collaborative debugging tools from main
spelech Sep 19, 2026
a31c9c9
fix(ui): use vector path icons for tab headers and fix AI Copilot con…
spelech Sep 19, 2026
916de05
fix(windowing): use DirectComposition and BorderOnly decorations to e…
spelech Sep 20, 2026
adec4a6
feat: Rebrand Copilot to AI Assist, add Pop-out Window, and dynamic L…
spelech Sep 20, 2026
4452be3
feat: Add LAN IP detection, LiteLLM default endpoint, and API key set…
spelech Sep 20, 2026
05b313b
feat: real engine test flight execution and UI enhancements
spelech Sep 20, 2026
61dce16
Enhance Documentation tab pop-out functionality
spelech Sep 20, 2026
678ab64
Implement real engine test flight, interactive telemetry badges, and …
spelech Sep 20, 2026
161fedb
feat: complete AI Assist rebrand, pop-out support, and dynamic LiteLL…
spelech Sep 20, 2026
3af4681
fix: integration test assertion for LaunchTestFlightAsync
spelech Sep 20, 2026
71cd7d2
Merge branch 'worktree/settings-lan-ip' into feature/fixes-and-adjust…
spelech Sep 20, 2026
f541ff5
Merge branch 'worktree/ai-assist-popout-litellm' into feature/fixes-a…
spelech Sep 20, 2026
c95de54
Merge branch 'worktree/test-flight-real-engine' into feature/fixes-an…
spelech Sep 20, 2026
831c660
fix(desktop): connect native pop-outs, service health modals, and win…
spelech Sep 20, 2026
f8c8afb
docs: add magnetic snapped companion windows design specification
spelech Sep 20, 2026
0ebe5ad
docs: add magnetic companion windows implementation plan
spelech Sep 20, 2026
14541f2
feat: implement WindowSnapManager flank math and position calculations
spelech Sep 20, 2026
0079c3c
feat: implement WindowSnapManager lockstep sync, detach and proximity…
spelech Sep 20, 2026
a950447
fix(snap-manager): deduplicate window event hooks and harden argument…
spelech Sep 20, 2026
ac3ad5a
feat: add magnetic snap button and styling to companion window titlebars
spelech Sep 20, 2026
18b76db
feat: wire companion windows to WindowSnapManager in MainWindow
spelech Sep 20, 2026
b46caf1
fix: decouple companion tabs from TabControl and add explicit event u…
spelech Sep 20, 2026
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@
# dotenv files
.env

# Worktrees
.worktrees/
worktrees/

# User-specific files
*.rsuser
*.suo
Expand Down
12 changes: 6 additions & 6 deletions Endpoints/ModelProxyEndpoints.cs
Original file line number Diff line number Diff line change
Expand Up @@ -187,24 +187,24 @@ async Task<IResult> HandleDeleteModel(HttpContext context, IHttpClientFactory cl
var tag = !string.IsNullOrWhiteSpace(pipeline_tag) ? pipeline_tag : pipeline_tags;

string requestUrl;
var searchParam = !string.IsNullOrWhiteSpace(query) ? $"search={Uri.EscapeDataString(query)}&" : "";
if (!string.IsNullOrWhiteSpace(tag))
{
if (tag.Equals("gguf", StringComparison.OrdinalIgnoreCase))
{
var qParam = string.IsNullOrWhiteSpace(query) ? "llama" : query;
requestUrl = $"https://huggingface.co/api/models?search={Uri.EscapeDataString(qParam)}&filter=gguf&sort=downloads&direction=-1&limit=25";
var qFallback = string.IsNullOrWhiteSpace(query) ? "search=llama&" : searchParam;
requestUrl = $"https://huggingface.co/api/models?{qFallback}filter=gguf&sort=downloads&direction=-1&limit=25";
}
else
{
var qParam = Uri.EscapeDataString(query);
var tagParam = Uri.EscapeDataString(tag);
requestUrl = $"https://huggingface.co/api/models?search={qParam}&pipeline_tag={tagParam}&sort=downloads&direction=-1&limit=25";
requestUrl = $"https://huggingface.co/api/models?{searchParam}pipeline_tag={tagParam}&sort=downloads&direction=-1&limit=25";
}
}
else
{
var qParam = string.IsNullOrWhiteSpace(query) ? "llama" : query;
requestUrl = $"https://huggingface.co/api/models?search={Uri.EscapeDataString(qParam)}&filter=gguf&sort=downloads&direction=-1&limit=25";
var qFallback = string.IsNullOrWhiteSpace(query) ? "search=llama&" : searchParam;
requestUrl = $"https://huggingface.co/api/models?{qFallback}filter=gguf&sort=downloads&direction=-1&limit=25";
}

using var req = new HttpRequestMessage(HttpMethod.Get, requestUrl);
Expand Down
2 changes: 1 addition & 1 deletion LocalLLMServerManager.Shared/Interfaces/IAiAppTools.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
namespace LocalLLMServerManager.Shared.Interfaces;

/// <summary>
/// Natural Language Integration Bridge: allows the AI Copilot to query telemetry,
/// Natural Language Integration Bridge: allows the AI Assist to query telemetry,
/// manage engines, update settings, trigger workflows, and query documentation.
/// </summary>
public interface IAiAppTools
Expand Down
4 changes: 3 additions & 1 deletion LocalLLMServerManager.Shared/Models/StudioPresetModels.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,9 @@ public enum StudioModality
{
Image,
Video,
Audio
Audio,
Text,
Mesh
}

/// <summary>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -78,8 +78,9 @@ public async Task<List<HuggingFaceRepoItem>> SearchRepositoriesAsync(string apiB
public async Task<List<HuggingFaceRepoItem>> SearchModelsAsync(string query, string? pipelineTag = null, System.Threading.CancellationToken ct = default)
{
using var http = new HttpClient();
var tagParam = string.IsNullOrWhiteSpace(pipelineTag) ? "" : $"&pipeline_tag={Uri.EscapeDataString(pipelineTag)}";
var requestUrl = $"https://huggingface.co/api/models?search={Uri.EscapeDataString(query)}{tagParam}&sort=downloads&direction=-1&limit=20";
var searchParam = string.IsNullOrWhiteSpace(query) ? "" : $"search={Uri.EscapeDataString(query)}&";
var tagParam = string.IsNullOrWhiteSpace(pipelineTag) ? "" : $"pipeline_tag={Uri.EscapeDataString(pipelineTag)}&";
var requestUrl = $"https://huggingface.co/api/models?{searchParam}{tagParam}sort=downloads&direction=-1&limit=20";
var result = new List<HuggingFaceRepoItem>();
try
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -151,8 +151,8 @@ private async Task<string> LoadPromptDocumentAsync(string fileName, string fallb
return fallbackContent;
}

private const string FallbackSystemPrompt = @"# LocalLLMServerManager AI Copilot — System Prompt
You are the LocalLLMServerManager Copilot, an expert AI engineer integrated directly into the application.
private const string FallbackSystemPrompt = @"# LocalLLMServerManager AI Assist — System Prompt
You are the LocalLLMServerManager AI Assist, an expert AI engineer integrated directly into the application.
Guide users on models, workflows, and settings, and proactively use your tools to perform actions in the app.";

private const string FallbackCapabilitiesPrompt = @"# Application Capabilities Reference
Expand Down
132 changes: 132 additions & 0 deletions LocalLLMServerManager.Shared/Services/UiDiagnosticLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace LocalLLMServerManager.Shared.Services;

/// <summary>
/// Represents a captured UI binding or diagnostic error entry.
/// </summary>
public record BindingDiagnosticEntry(DateTime Timestamp, string Message, string? Source = null);

/// <summary>
/// Interface for thread-safe UI diagnostic error logging.
/// </summary>
public interface IUiDiagnosticLogger
{
/// <summary>
/// Records a new UI binding error.
/// </summary>
void RecordBindingError(string message, string? source = null);

/// <summary>
/// Retrieves the most recent binding diagnostic entries up to <paramref name="maxCount"/>.
/// </summary>
IReadOnlyList<BindingDiagnosticEntry> GetRecentErrors(int maxCount = 50);

/// <summary>
/// Clears all recorded diagnostic entries.
/// </summary>
void Clear();

/// <summary>
/// Exports a formatted diagnostic summary string.
/// </summary>
string ExportLogSummary();
}

/// <summary>
/// Thread-safe bounded diagnostic logger service for UI binding errors.
/// </summary>
public class UiDiagnosticLogger : IUiDiagnosticLogger
{
public const int DefaultMaxEntries = 500;

public static UiDiagnosticLogger Instance { get; } = new();

private readonly int _maxCapacity;
private readonly object _lock = new();
private readonly Queue<BindingDiagnosticEntry> _entries;

public int MaxCapacity => _maxCapacity;

public int Count
{
get
{
lock (_lock)
{
return _entries.Count;
}
}
}

public UiDiagnosticLogger(int maxCapacity = DefaultMaxEntries)
{
_maxCapacity = maxCapacity > 0 ? maxCapacity : DefaultMaxEntries;
_entries = new Queue<BindingDiagnosticEntry>(_maxCapacity);
}

public void RecordBindingError(string message, string? source = null)
{
lock (_lock)
{
while (_entries.Count >= _maxCapacity)
{
_entries.Dequeue();
}

_entries.Enqueue(new BindingDiagnosticEntry(DateTime.UtcNow, message ?? string.Empty, source));
}
}

public IReadOnlyList<BindingDiagnosticEntry> GetRecentErrors(int maxCount = 50)
{
if (maxCount <= 0)
{
return Array.Empty<BindingDiagnosticEntry>();
}

lock (_lock)
{
if (_entries.Count == 0)
{
return Array.Empty<BindingDiagnosticEntry>();
}

int count = Math.Min(maxCount, _entries.Count);
int skip = _entries.Count - count;
return _entries.Skip(skip).Take(count).ToList().AsReadOnly();
}
}

public void Clear()
{
lock (_lock)
{
_entries.Clear();
}
}

public string ExportLogSummary()
{
lock (_lock)
{
if (_entries.Count == 0)
{
return "No binding errors recorded.";
}

var sb = new StringBuilder();
sb.AppendLine($"=== UI Diagnostic Summary ({_entries.Count} error(s)) ===");
foreach (var entry in _entries)
{
var sourceInfo = string.IsNullOrWhiteSpace(entry.Source) ? string.Empty : $" [{entry.Source}]";
sb.AppendLine($"[{entry.Timestamp:yyyy-MM-dd HH:mm:ss.fff}]{sourceInfo}: {entry.Message}");
}

return sb.ToString().TrimEnd();
}
}
}
47 changes: 47 additions & 0 deletions LocalLLMServerManager.Shared/Services/UiDiagnosticTraceListener.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
using System;
using System.Diagnostics;

namespace LocalLLMServerManager.Shared.Services;

/// <summary>
/// TraceListener that intercepts Avalonia UI binding diagnostic and warning messages
/// and records them into an IUiDiagnosticLogger instance.
/// </summary>
public class UiDiagnosticTraceListener : TraceListener
{
private readonly IUiDiagnosticLogger _logger;

public UiDiagnosticTraceListener(IUiDiagnosticLogger logger)
: base("UiDiagnosticTraceListener")
{
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}

public override void Write(string? message)
{
ProcessMessage(message);
}

public override void WriteLine(string? message)
{
ProcessMessage(message);
}

private void ProcessMessage(string? message)
{
if (string.IsNullOrWhiteSpace(message))
{
return;
}

// Intercept messages related to Avalonia data binding errors and warnings
if (message.Contains("[Binding]", StringComparison.OrdinalIgnoreCase) ||
message.Contains("Error in binding", StringComparison.OrdinalIgnoreCase) ||
message.Contains("Binding error", StringComparison.OrdinalIgnoreCase) ||
(message.Contains("Binding", StringComparison.OrdinalIgnoreCase) &&
(message.Contains("Error", StringComparison.OrdinalIgnoreCase) || message.Contains("Warning", StringComparison.OrdinalIgnoreCase))))
{
_logger.RecordBindingError(message.Trim(), "Avalonia.Binding");
}
}
}
11 changes: 11 additions & 0 deletions LocalLLMServerManager.Shared/Styles/DesignTokens.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,15 @@
<!-- Solid Base Background & Legacy Fallbacks -->
<SolidColorBrush x:Key="GlassBackgroundBrush" Color="{StaticResource BgDarkColor}" />
<SolidColorBrush x:Key="PrimaryGradientBrush" Color="{StaticResource PrimaryColor}" />

<!-- Vector Icon Geometries for Crisp Tab Navigation -->
<StreamGeometry x:Key="IconModels">M21,16.5C21,16.88 20.79,17.21 20.47,17.38L12.57,21.82C12.41,21.94 12.21,22 12,22C11.79,22 11.59,21.94 11.43,21.82L3.53,17.38C3.21,17.21 3,16.88 3,16.5V7.5C3,7.12 3.21,6.79 3.53,6.62L11.43,2.18C11.59,2.06 11.79,2 12,2C12.21,2 12.41,2.06 12.57,2.18L20.47,6.62C20.79,6.79 21,7.12 21,7.5V16.5M12,4.15L6.04,7.5L12,10.85L17.96,7.5L12,4.15M5,15.91L11,19.29V12.58L5,9.21V15.91M19,15.91V9.21L13,12.58V19.29L19,15.91Z</StreamGeometry>
<StreamGeometry x:Key="IconWorkflows">M13 2L3 14h6l-2 8 10-12h-6l2-8z</StreamGeometry>
<StreamGeometry x:Key="IconCanIRunIt">M6 2v2H4a2 2 0 0 0-2 2v2h2v2H2v2h2v2H2v2a2 2 0 0 0 2 2h2v2h2v-2h2v2h2v-2h2v2h2a2 2 0 0 0 2-2v-2h2v-2h-2v-2h2v-2h-2V6a2 2 0 0 0-2-2h-2V2h-2v2h-2V2h-2v2H8V2H6zm0 4h12v12H6V6zm2 2v8h8V8H8z</StreamGeometry>
<StreamGeometry x:Key="IconAiAssist">M12 2a2 2 0 0 1 2 2v1h3a3 3 0 0 1 3 3v8a3 3 0 0 1-3 3H7a3 3 0 0 1-3-3V8a3 3 0 0 1 3-3h3V4a2 2 0 0 1 2-2zm-4 8a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm8 0a1.5 1.5 0 1 0 0 3 1.5 1.5 0 0 0 0-3zm-6 5a1 1 0 0 0 0 2h4a1 1 0 1 0 0-2H10z</StreamGeometry>
<StreamGeometry x:Key="IconDocumentation">M18 2H6c-1.1 0-2 .9-2 2v16c0 1.1.9 2 2 2h12c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zM6 4h5v8l-2.5-1.5L6 12V4zm12 16H6v-2h12v2zm0-4H6v-2h12v2zm0-4h-5V4h5v8z</StreamGeometry>
<StreamGeometry x:Key="IconSettings">M19.14 12.94c.04-.3.06-.61.06-.94 0-.32-.02-.64-.07-.94l2.03-1.58a.49.49 0 0 0 .12-.61l-1.92-3.32a.49.49 0 0 0-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54A.48.48 0 0 0 14 2h-4a.48.48 0 0 0-.48.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96a.49.49 0 0 0-.59.22L2.64 8.47a.49.49 0 0 0 .12.61l2.03 1.58c-.05.3-.07.62-.07.94s.02.64.07.94l-2.03 1.58a.49.49 0 0 0-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h4c.24 0 .44-.17.48-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32a.49.49 0 0 0-.12-.61l-2.03-1.58zM12 15.5a3.5 3.5 0 1 1 0-7 3.5 3.5 0 0 1 0 7z</StreamGeometry>
<StreamGeometry x:Key="IconDownloaded">M19 9h-4V3H9v6H5l7 7 7-7zM5 18v2h14v-2H5z</StreamGeometry>
<StreamGeometry x:Key="IconHub">M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10 10-4.48 10-10S17.52 2 12 2zm-1 17.93c-3.95-.49-7-3.85-7-7.93 0-.62.08-1.21.21-1.79L9 15v1c0 1.1.9 2 2 2v1.93zm6.9-2.54c-.26-.81-1-1.39-1.9-1.39h-1v-3c0-.55-.45-1-1-1H8v-2h2c.55 0 1-.45 1-1V7h2c1.1 0 2-.9 2-2v-.41c2.93 1.19 5 4.06 5 7.41 0 2.08-.8 3.97-2.1 5.39z</StreamGeometry>
<StreamGeometry x:Key="IconCivitai">M12 2C6.49 2 2 6.49 2 12s4.49 10 10 10a2.5 2.5 0 0 0 2.5-2.5c0-.61-.23-1.2-.64-1.67-.4-.46-.61-1.04-.61-1.63 0-1.38 1.12-2.5 2.5-2.5H17c2.76 0 5-2.24 5-5 0-4.74-4.48-8.7-10-8.7zm-5.5 9a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm3-4a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm5 0a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3zm3 4a1.5 1.5 0 1 1 0-3 1.5 1.5 0 0 1 0 3z</StreamGeometry>
</ResourceDictionary>
61 changes: 59 additions & 2 deletions LocalLLMServerManager.Shared/Styles/MatteTheme.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://LocalLLMServerManager.Shared/Styles/DesignTokens.axaml" />
</ResourceDictionary.MergedDictionaries>
<SolidColorBrush x:Key="SurfaceHoverBrush" Color="#21262d" />
<SolidColorBrush x:Key="PrimaryAccentBrush" Color="#4f46e5" />
</ResourceDictionary>
</Styles.Resources>

Expand Down Expand Up @@ -115,8 +117,11 @@
Margin="0,0,0,16"
Padding="4"
HorizontalAlignment="Left">
<ItemsPresenter Name="PART_ItemsPresenter"
ItemsPanel="{TemplateBinding ItemsPanel}" />
<StackPanel Orientation="Horizontal" Spacing="0">
<ItemsPresenter Name="PART_ItemsPresenter"
ItemsPanel="{TemplateBinding ItemsPanel}" />
<ContentPresenter Content="{TemplateBinding Tag}" />
</StackPanel>
</Border>
<ContentPresenter Name="PART_SelectedContentHost"
Margin="{TemplateBinding Padding}"
Expand Down Expand Up @@ -157,4 +162,56 @@
<Setter Property="BorderThickness" Value="1" />
<Setter Property="Foreground" Value="{StaticResource TextMainBrush}" />
</Style>

<!-- Navigation pop-out action buttons styled seamlessly inside segmented tab bar -->
<Style Selector="Button.nav-tab-btn">
<Setter Property="Background" Value="Transparent" />
<Setter Property="Foreground" Value="{StaticResource TextMutedBrush}" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Padding" Value="14,8" />
<Setter Property="Margin" Value="2,0" />
<Setter Property="MinHeight" Value="34" />
<Setter Property="FontWeight" Value="SemiBold" />
<Setter Property="FontSize" Value="13" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="VerticalAlignment" Value="Center" />
</Style>
<Style Selector="Button.nav-tab-btn /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="CornerRadius" Value="6" />
</Style>
<Style Selector="Button.nav-tab-btn:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource BgCardBrush}" />
<Setter Property="Foreground" Value="{StaticResource TextMainBrush}" />
</Style>
<Style Selector="Button.nav-tab-btn:pressed /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource BgSurfaceBrush}" />
<Setter Property="Opacity" Value="0.85" />
</Style>

<!-- Magnet Snap Button for Companion Windows -->
<Style Selector="Button.magnet-snap-btn">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="{StaticResource GlassBorderBrush}" />
<Setter Property="BorderThickness" Value="1" />
<Setter Property="CornerRadius" Value="6" />
<Setter Property="Padding" Value="8,4" />
<Setter Property="Cursor" Value="Hand" />
</Style>
<Style Selector="Button.magnet-snap-btn:pointerover">
<Setter Property="Background" Value="{StaticResource SurfaceHoverBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource PrimaryAccentBrush}" />
</Style>
<Style Selector="Button.magnet-snap-btn:pointerover /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="{StaticResource SurfaceHoverBrush}" />
<Setter Property="BorderBrush" Value="{StaticResource PrimaryAccentBrush}" />
</Style>
<Style Selector="Button.magnet-snap-btn.snapped">
<Setter Property="Background" Value="#1a4f46e5" />
<Setter Property="BorderBrush" Value="#4f46e5" />
</Style>
<Style Selector="Button.magnet-snap-btn.snapped /template/ ContentPresenter#PART_ContentPresenter">
<Setter Property="Background" Value="#1a4f46e5" />
<Setter Property="BorderBrush" Value="#4f46e5" />
</Style>
</Styles>
Loading
Loading