using System.Runtime.CompilerServices; using Avalonia; using Avalonia.Controls; using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Threading; using LehrerApp.Core.Services; using LehrerApp.Desktop.Views.Mcp; namespace LehrerApp.Desktop.Services.Mcp; /// /// Produktive -Implementierung: zeigt /// über dem Hauptfenster an. Der aufrufende Tool-Handler läuft auf einem Hintergrund-Thread /// (MCP-Pipe-Session in ), deshalb Marshalling über /// . /// /// Nutzer-Feedback: der Dialog fiel zu wenig auf, wenn LehrerApp im Hintergrund lief (naheliegend, /// da der Anstoß von einem KI-Client in einem anderen Fenster kommt) — deshalb wird das Hauptfenster /// vor dem Anzeigen aus einer möglichen Minimierung geholt und aktiviert, und der Dialog selbst /// läuft `Topmost`. /// /// Nutzer-Feedback (Nachtrag): bei vielen gleichartigen Vorschlägen in Folge (z.B. 17x /// "add_lesson_phase" für eine neu generierte Unterrichtseinheit) einzeln nachfragen zu müssen, ist /// unzumutbar. Der Dialog bietet deshalb zwei Sitzungsfreigaben an ("diese Aktion" / "alle /// Aktionen"), die als reines In-Memory-Bookkeeping auf dieser Singleton-Instanz leben — sie gelten /// bis zum Beenden der App (neuer Prozess = neue Instanz = wieder alles ungetraut) und werden nie /// persistiert. ist der Name der aufrufenden Tool-Methode (siehe /// ), nicht der MCP-Wire-Name. /// /// Ohne Reaktion des Nutzers würde die Pipe-Session (und damit der wartende KI-Client) unbegrenzt /// hängen bleiben — nach wird der Dialog automatisch geschlossen und die /// Änderung als abgelehnt gewertet. /// public sealed class AvaloniaMcpConfirmationService(AppLogger logger) : IMcpConfirmationService { private static readonly TimeSpan Timeout = TimeSpan.FromMinutes(2); private readonly object _trustLock = new(); private readonly HashSet _trustedOperations = []; private bool _trustAll; public async Task ConfirmAsync(string title, string message, CancellationToken ct, [CallerMemberName] string operationKey = "", bool allowSessionTrust = true) { if (allowSessionTrust) { bool alreadyTrusted; lock (_trustLock) alreadyTrusted = _trustAll || _trustedOperations.Contains(operationKey); if (alreadyTrusted) { logger.Info($"MCP: „{title}“ automatisch bestätigt (Sitzungsfreigabe für " + $"{(_trustAll ? "alle Aktionen" : operationKey)})."); return true; } } if (Application.Current?.ApplicationLifetime is not IClassicDesktopStyleApplicationLifetime { MainWindow: { } owner }) return false; // Die gesamte Warte-/Timeout-Logik läuft als ein Stück innerhalb des UI-Thread-Callbacks: // Avalonias Dispatcher-Synchronisationskontext sorgt dafür, dass die Fortsetzung nach // "await Task.WhenAny(...)" wieder auf dem UI-Thread läuft, sodass dialog.Close() dort // sicher aufgerufen werden kann. var result = await Dispatcher.UIThread.InvokeAsync(async () => { if (owner.WindowState == WindowState.Minimized) owner.WindowState = WindowState.Normal; owner.Activate(); var dialog = new McpConfirmDialog { DataContext = new McpConfirmDialogInfo { Title = title, Message = message, ConfirmText = "Übernehmen", AllowSessionTrust = allowSessionTrust, }, }; var dialogTask = dialog.ShowDialog(owner); var timeoutTask = Task.Delay(Timeout, ct); var completed = await Task.WhenAny(dialogTask, timeoutTask); if (completed != dialogTask) { dialog.Close(McpConfirmDialogResult.Rejected); return McpConfirmDialogResult.Rejected; } return await dialogTask; }); if (result.Approved && allowSessionTrust) { lock (_trustLock) { if (result.TrustAll) _trustAll = true; else if (result.TrustOperation) _trustedOperations.Add(operationKey); } } return result.Approved; } }