Files
LehrerApp/LehrerApp.Desktop/Services/Mcp/AvaloniaMcpConfirmationService.cs
T
adminandClaude Sonnet 5 9567d8d616 feat: lokaler MCP-Server, Phase 2 (Write-Tools + Bestätigungsdialog + Lesson-Plans)
Neue Write-Tools create_time_entry, create_grade_entry und
update_student_group_assignment schreiben nie direkt: jeder Aufruf zeigt
zuerst einen menschenlesbaren Bestätigungsdialog (bestehender
ConfirmDialog, über Dispatcher.UIThread aus dem Pipe-Session-Thread
angezeigt) und schreibt erst nach Bestätigung, mit 2-Minuten-Timeout
gegen eine hängende Session. Zusätzliches Read-Tool get_lesson_plans.

create_note bewusst nicht umgesetzt (kollidiert mit dem bestehenden
Dokumentations-Ausschluss aus Phase 1), create_lesson_plan/
update_lesson_plan wegen der Modellkomplexität von Lesson zurückgestellt
(siehe TODO.md 4.5.26).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-11 20:14:51 +02:00

50 lines
2.2 KiB
C#

using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Threading;
using LehrerApp.Desktop.Views.Shared;
namespace LehrerApp.Desktop.Services.Mcp;
/// <summary>
/// Produktive <see cref="IMcpConfirmationService"/>-Implementierung: zeigt den bestehenden
/// <see cref="ConfirmDialog"/> (Views/Shared) über dem Hauptfenster an. Der aufrufende Tool-Handler
/// läuft auf einem Hintergrund-Thread (MCP-Pipe-Session in <see cref="McpServerHostedService"/>),
/// deshalb Marshalling über <see cref="Dispatcher.UIThread"/>.
///
/// Ohne Reaktion des Nutzers würde die Pipe-Session (und damit der wartende KI-Client) unbegrenzt
/// hängen bleiben — nach <see cref="Timeout"/> wird der Dialog automatisch geschlossen und die
/// Änderung als abgelehnt gewertet.
/// </summary>
public sealed class AvaloniaMcpConfirmationService : IMcpConfirmationService
{
private static readonly TimeSpan Timeout = TimeSpan.FromMinutes(2);
public async Task<bool> ConfirmAsync(string title, string message, CancellationToken ct)
{
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.
return await Dispatcher.UIThread.InvokeAsync(async () =>
{
var dialog = new ConfirmDialog
{
DataContext = new ConfirmDialogInfo { Title = title, Message = message, ConfirmText = "Übernehmen" },
};
var dialogTask = dialog.ShowDialog<bool>(owner);
var timeoutTask = Task.Delay(Timeout, ct);
var completed = await Task.WhenAny(dialogTask, timeoutTask);
if (completed != dialogTask)
{
dialog.Close(false);
return false;
}
return await dialogTask;
});
}
}