feat: prominenterer Bestätigungsdialog + move_lesson/delete_lesson (Nutzer-Feedback)
Nach dem ersten Live-Test mit echtem Bridge-Prozess: der bisherige ConfirmDialog fiel zu wenig auf, wenn LehrerApp im Hintergrund lief (Normalfall, da der Anstoß vom KI-Client in einem anderen Fenster kommt). Neuer, eigenständiger McpConfirmDialog statt Änderung am geteilten ConfirmDialog (hätte alle anderen Aufrufer mitbetroffen): breiter, auffälliger Kopfbereich, Topmost. AvaloniaMcpConfirmationService holt das Hauptfenster zusätzlich aus einer möglichen Minimierung und aktiviert es vor dem Anzeigen. move_lesson kapselt die bereits vorhandene LessonSchedulingService.Move (shiftFollowingLessons öffnet eine Lücke für eine neue Stunde, indem spätere Stunden derselben Einheit mitverschoben werden) - keine neue Terminlogik, nur Wiederverwendung. delete_lesson ist eine bewusste, gezielte Ausnahme von "v1 ohne Lösch-Tools" auf expliziten Nutzerwunsch: eigene AllowedDestructiveWriteTools-Liste, Destructive=true-Annotation, Bestätigungstext betont ausdrücklich die fehlende Papierkorb-Deckung für Lessons. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -29,12 +29,18 @@ public sealed class McpToolsTests
|
||||
new[]
|
||||
{
|
||||
"add_lesson_attachment", "add_lesson_phase", "create_grade_entry", "create_lesson",
|
||||
"create_time_entry", "create_unit", "remove_lesson_phase", "update_lesson",
|
||||
"update_lesson_phase", "update_student_group_assignment", "update_unit",
|
||||
"create_time_entry", "create_unit", "move_lesson", "remove_lesson_phase",
|
||||
"update_lesson", "update_lesson_phase", "update_student_group_assignment", "update_unit",
|
||||
},
|
||||
McpToolScope.AllowedWriteTools.OrderBy(n => n, StringComparer.Ordinal));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllowedDestructiveWriteTools_EnthaeltNurDeleteLesson()
|
||||
{
|
||||
Assert.Equal(["delete_lesson"], McpToolScope.AllowedDestructiveWriteTools);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AllowedTools_EnthaeltKeineDokumentationstypen()
|
||||
{
|
||||
@@ -463,6 +469,110 @@ public sealed class McpToolsTests
|
||||
Assert.Equal(0, confirmation.CallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MoveLesson_NutzerBestaetigt_VerschiebtDatum()
|
||||
{
|
||||
var lesson = new Lesson { Topic = "Brechung", Date = new DateOnly(2026, 1, 5) };
|
||||
var lessons = new FakeLessons();
|
||||
lessons.Add(lesson);
|
||||
var tool = BuildLessonPlanTools(lessons: lessons);
|
||||
|
||||
var result = await tool.MoveLesson(lesson.Id, new DateOnly(2026, 1, 12));
|
||||
|
||||
Assert.True(result.Applied);
|
||||
Assert.Equal(new DateOnly(2026, 1, 12), lessons.GetById(lesson.Id)!.Date);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MoveLesson_ShiftFollowing_VerschiebtSpaetereStundenDerselbenEinheitMit()
|
||||
{
|
||||
var unitId = Guid.NewGuid();
|
||||
var moved = new Lesson { UnitId = unitId, Topic = "Brechung", Date = new DateOnly(2026, 1, 5) };
|
||||
var later = new Lesson { UnitId = unitId, Topic = "Reflexion", Date = new DateOnly(2026, 1, 7) };
|
||||
var earlier = new Lesson { UnitId = unitId, Topic = "Einstieg", Date = new DateOnly(2026, 1, 1) };
|
||||
var lessons = new FakeLessons();
|
||||
lessons.Add(moved); lessons.Add(later); lessons.Add(earlier);
|
||||
var tool = BuildLessonPlanTools(lessons: lessons);
|
||||
|
||||
// moved: 5.1. -> 12.1. (Delta +7 Tage), later (7.1., danach) muss mitwandern, earlier (1.1., davor) nicht.
|
||||
var result = await tool.MoveLesson(moved.Id, new DateOnly(2026, 1, 12), shiftFollowingLessons: true);
|
||||
|
||||
Assert.True(result.Applied);
|
||||
Assert.Equal(new DateOnly(2026, 1, 14), lessons.GetById(later.Id)!.Date);
|
||||
Assert.Equal(new DateOnly(2026, 1, 1), lessons.GetById(earlier.Id)!.Date);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MoveLesson_ZielStundeBelegt_LiefertFehlerNachBestaetigung()
|
||||
{
|
||||
var groupId = Guid.NewGuid();
|
||||
var moved = new Lesson { GroupId = groupId, Topic = "Brechung", Date = new DateOnly(2026, 1, 5), LessonNumber = 1 };
|
||||
var occupying = new Lesson { GroupId = groupId, Topic = "Anderer Kurs", Date = new DateOnly(2026, 1, 12), LessonNumber = 1 };
|
||||
var lessons = new FakeLessons();
|
||||
lessons.Add(moved); lessons.Add(occupying);
|
||||
var confirmation = new FakeMcpConfirmation();
|
||||
var tool = BuildLessonPlanTools(lessons: lessons, confirmation: confirmation);
|
||||
|
||||
var result = await tool.MoveLesson(moved.Id, new DateOnly(2026, 1, 12), newPeriod: 1);
|
||||
|
||||
Assert.False(result.Applied);
|
||||
Assert.Equal(1, confirmation.CallCount); // Bestätigung lief bereits, Konflikt zeigt sich erst beim Speichern.
|
||||
Assert.Equal(new DateOnly(2026, 1, 5), lessons.GetById(moved.Id)!.Date); // unverändert
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task MoveLesson_UnbekannteStunde_LiefertFehlerOhneNachfrage()
|
||||
{
|
||||
var confirmation = new FakeMcpConfirmation();
|
||||
var tool = BuildLessonPlanTools(confirmation: confirmation);
|
||||
|
||||
var result = await tool.MoveLesson(Guid.NewGuid(), new DateOnly(2026, 1, 12));
|
||||
|
||||
Assert.False(result.Applied);
|
||||
Assert.Equal(0, confirmation.CallCount);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteLesson_NutzerBestaetigt_LoeschtEndgueltig()
|
||||
{
|
||||
var lesson = new Lesson { Topic = "Brechung" };
|
||||
var lessons = new FakeLessons();
|
||||
lessons.Add(lesson);
|
||||
var tool = BuildLessonPlanTools(lessons: lessons);
|
||||
|
||||
var result = await tool.DeleteLesson(lesson.Id);
|
||||
|
||||
Assert.True(result.Applied);
|
||||
Assert.Null(lessons.GetById(lesson.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteLesson_NutzerLehntAb_BleibtErhalten()
|
||||
{
|
||||
var lesson = new Lesson { Topic = "Brechung" };
|
||||
var lessons = new FakeLessons();
|
||||
lessons.Add(lesson);
|
||||
var confirmation = new FakeMcpConfirmation { Response = false };
|
||||
var tool = BuildLessonPlanTools(lessons: lessons, confirmation: confirmation);
|
||||
|
||||
var result = await tool.DeleteLesson(lesson.Id);
|
||||
|
||||
Assert.False(result.Applied);
|
||||
Assert.NotNull(lessons.GetById(lesson.Id));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task DeleteLesson_UnbekannteStunde_LiefertFehlerOhneNachfrage()
|
||||
{
|
||||
var confirmation = new FakeMcpConfirmation();
|
||||
var tool = BuildLessonPlanTools(confirmation: confirmation);
|
||||
|
||||
var result = await tool.DeleteLesson(Guid.NewGuid());
|
||||
|
||||
Assert.False(result.Applied);
|
||||
Assert.Equal(0, confirmation.CallCount);
|
||||
}
|
||||
|
||||
// ── GroupMembershipTools ─────────────────────────────────────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
|
||||
@@ -2,15 +2,20 @@ using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Threading;
|
||||
using LehrerApp.Desktop.Views.Shared;
|
||||
using LehrerApp.Desktop.Views.Mcp;
|
||||
|
||||
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"/>.
|
||||
/// Produktive <see cref="IMcpConfirmationService"/>-Implementierung: zeigt <see cref="McpConfirmDialog"/>
|
||||
/// ü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"/>.
|
||||
///
|
||||
/// 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`.
|
||||
///
|
||||
/// 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
|
||||
@@ -31,9 +36,12 @@ public sealed class AvaloniaMcpConfirmationService : IMcpConfirmationService
|
||||
// sicher aufgerufen werden kann.
|
||||
return await Dispatcher.UIThread.InvokeAsync(async () =>
|
||||
{
|
||||
var dialog = new ConfirmDialog
|
||||
if (owner.WindowState == WindowState.Minimized) owner.WindowState = WindowState.Normal;
|
||||
owner.Activate();
|
||||
|
||||
var dialog = new McpConfirmDialog
|
||||
{
|
||||
DataContext = new ConfirmDialogInfo { Title = title, Message = message, ConfirmText = "Übernehmen" },
|
||||
DataContext = new McpConfirmDialogInfo { Title = title, Message = message, ConfirmText = "Übernehmen" },
|
||||
};
|
||||
var dialogTask = dialog.ShowDialog<bool>(owner);
|
||||
var timeoutTask = Task.Delay(Timeout, ct);
|
||||
|
||||
@@ -8,7 +8,7 @@ using ModelContextProtocol.Server;
|
||||
namespace LehrerApp.Desktop.Services.Mcp;
|
||||
|
||||
/// <summary>
|
||||
/// In-Process-MCP-Server (Phase 1–3, siehe Planungsdokument). Lauscht auf der Named Pipe
|
||||
/// In-Process-MCP-Server (siehe Planungsdokument und TODO.md 4.5.25ff.). Lauscht auf der Named Pipe
|
||||
/// <see cref="McpPipeConstants.PipeName"/> und bedient jede eingehende Verbindung (eine je
|
||||
/// LehrerApp.McpBridge-Instanz) als eigene MCP-Session über <see cref="StreamServerTransport"/> —
|
||||
/// ein <see cref="NamedPipeServerStream"/> ist ein normaler <see cref="Stream"/> und kann direkt
|
||||
@@ -129,7 +129,8 @@ public sealed class McpServerHostedService : IAsyncDisposable
|
||||
|
||||
// Write-Tools schreiben nie direkt - jede Handler-Methode ruft selbst erst
|
||||
// IMcpConfirmationService auf (siehe die jeweilige Tool-Klasse). ReadOnly bewusst false,
|
||||
// Destructive bewusst false (keine der Phase-2-Schreiboperationen löscht etwas).
|
||||
// Destructive bewusst false (keine dieser Schreiboperationen löscht etwas) - für die
|
||||
// Ausnahme siehe AddDestructiveWriteTool/McpToolScope.AllowedDestructiveWriteTools.
|
||||
void AddWriteTool(Delegate handler, string name, string description)
|
||||
{
|
||||
toolCollection.Add(McpServerTool.Create(handler, new McpServerToolCreateOptions
|
||||
@@ -141,6 +142,17 @@ public sealed class McpServerHostedService : IAsyncDisposable
|
||||
}));
|
||||
}
|
||||
|
||||
void AddDestructiveWriteTool(Delegate handler, string name, string description)
|
||||
{
|
||||
toolCollection.Add(McpServerTool.Create(handler, new McpServerToolCreateOptions
|
||||
{
|
||||
Name = name,
|
||||
Description = description,
|
||||
ReadOnly = false,
|
||||
Destructive = true,
|
||||
}));
|
||||
}
|
||||
|
||||
AddReadTool(studentTools.GetStudents, "get_students",
|
||||
"Listet Schüler, optional gefiltert nach Lerngruppe.");
|
||||
AddReadTool(examTools.GetExams, "get_exams",
|
||||
@@ -182,10 +194,16 @@ public sealed class McpServerHostedService : IAsyncDisposable
|
||||
"Entfernt eine Verlaufsplan-Phase aus einer Einzelstunde (Bestätigung durch den Nutzer nötig).");
|
||||
AddWriteTool(lessonPlanTools.AddLessonAttachment, "add_lesson_attachment",
|
||||
"Fügt einer Einzelstunde ein neues Material als Base64-kodierten Anhang hinzu (Bestätigung durch den Nutzer nötig).");
|
||||
AddWriteTool(lessonPlanTools.MoveLesson, "move_lesson",
|
||||
"Verschiebt eine Einzelstunde auf ein neues Datum, optional mit Mitverschieben späterer Stunden derselben Einheit (Bestätigung durch den Nutzer nötig).");
|
||||
|
||||
AddDestructiveWriteTool(lessonPlanTools.DeleteLesson, "delete_lesson",
|
||||
"Löscht eine Einzelstunde endgültig, ohne Papierkorb (Bestätigung durch den Nutzer nötig).");
|
||||
|
||||
System.Diagnostics.Debug.Assert(
|
||||
toolCollection.Select(t => t.ProtocolTool.Name).OrderBy(n => n)
|
||||
.SequenceEqual(McpToolScope.AllowedReadTools.Concat(McpToolScope.AllowedWriteTools).OrderBy(n => n)),
|
||||
.SequenceEqual(McpToolScope.AllowedReadTools.Concat(McpToolScope.AllowedWriteTools)
|
||||
.Concat(McpToolScope.AllowedDestructiveWriteTools).OrderBy(n => n)),
|
||||
"Registrierte MCP-Tools weichen von McpToolScope ab.");
|
||||
|
||||
return new McpServerOptions
|
||||
|
||||
@@ -41,5 +41,16 @@ public static class McpToolScope
|
||||
"update_lesson_phase",
|
||||
"remove_lesson_phase",
|
||||
"add_lesson_attachment",
|
||||
"move_lesson",
|
||||
];
|
||||
|
||||
/// <summary>Löschende Write-Tools — eine bewusste, gezielte Ausnahme von der sonst geltenden
|
||||
/// "v1 ohne Lösch-Tools"-Regel (siehe Planungsdokument), nicht deren Aufhebung. Getrennt von
|
||||
/// <see cref="AllowedWriteTools"/> aufgeführt, damit diese Ausnahme beim Lesen sofort auffällt.
|
||||
/// <see cref="McpServerHostedService"/> registriert diese Tools zusätzlich mit
|
||||
/// <c>Destructive = true</c>.</summary>
|
||||
public static readonly IReadOnlyCollection<string> AllowedDestructiveWriteTools =
|
||||
[
|
||||
"delete_lesson",
|
||||
];
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using System.ComponentModel;
|
||||
using System.Text;
|
||||
using LehrerApp.Core.Interfaces;
|
||||
using LehrerApp.Core.Models;
|
||||
using LehrerApp.Core.Services;
|
||||
|
||||
namespace LehrerApp.Desktop.Services.Mcp.Tools;
|
||||
|
||||
@@ -16,7 +17,12 @@ namespace LehrerApp.Desktop.Services.Mcp.Tools;
|
||||
/// Stunde) und liefern einen für den Bestätigungsdialog tatsächlich lesbaren Diff statt eines
|
||||
/// kompletten Objekt-Dumps. Ein Tool, das die ganze Stunde überschreibt, ist bewusst NICHT
|
||||
/// vorgesehen; wo es fehlt, ist die Kombination aus update_lesson (Metadaten) +
|
||||
/// add/update/remove_lesson_phase (je eine Phase) der vorgesehene Weg.</summary>
|
||||
/// add/update/remove_lesson_phase (je eine Phase) der vorgesehene Weg.
|
||||
///
|
||||
/// "delete_lesson" ist (Stand Nutzer-Nachtrag) das einzige Lösch-Tool im gesamten MCP-Katalog —
|
||||
/// eine bewusste, gezielte Ausnahme von der sonst geltenden "v1 ohne Lösch-Tools"-Regel (siehe
|
||||
/// Planungsdokument), nicht deren Aufhebung. Entsprechend als "Destructive" annotiert
|
||||
/// (siehe McpServerHostedService) und mit besonders deutlicher Bestätigungsnachricht.</summary>
|
||||
public class LessonPlanTools(
|
||||
IUnitRepository units, ILessonRepository lessons, IGroupRepository groups,
|
||||
IAttachmentStorage attachments, IMcpConfirmationService confirmation)
|
||||
@@ -294,6 +300,63 @@ public class LessonPlanTools(
|
||||
return new WriteResultDto(true, phase.Id, "Phase entfernt.");
|
||||
}
|
||||
|
||||
// ── Einzelstunden (Lesson) — Verschieben & Löschen ───────────────────────────────────────
|
||||
|
||||
[Description("Verschiebt eine Einzelstunde auf ein neues Datum (und optional eine neue Stundennummer). Mit shiftFollowingLessons=true verschieben sich alle noch nicht durchgeführten, späteren Stunden derselben Einheit um denselben Tages-Versatz mit — so lässt sich eine Lücke für eine neue Stunde öffnen: diese Stunde auf den Termin der übernächsten verschieben (mit shiftFollowingLessons), dann create_lesson auf das dadurch freigewordene ursprüngliche Datum. Muss der Nutzer erst bestätigen.")]
|
||||
public async Task<WriteResultDto> MoveLesson(
|
||||
[Description("ID der zu verschiebenden Einzelstunde.")] Guid lessonId,
|
||||
[Description("Neues Datum, Format YYYY-MM-DD.")] DateOnly newDate,
|
||||
[Description("Neue Stundennummer im Tagesraster. Unverändert lassen: weglassen.")] int? newPeriod = null,
|
||||
[Description("Alle späteren, noch nicht durchgeführten Stunden derselben Einheit um denselben Tages-Versatz mitverschieben.")] bool shiftFollowingLessons = false,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var lesson = lessons.GetById(lessonId);
|
||||
if (lesson is null) return new WriteResultDto(false, null, "Unbekannte Stunden-ID.");
|
||||
|
||||
var oldDate = lesson.Date;
|
||||
var affectedCount = shiftFollowingLessons
|
||||
? lessons.GetByUnit(lesson.UnitId).Count(l => l.Id != lesson.Id && l.Status != LessonStatus.Conducted && l.Date > oldDate)
|
||||
: 0;
|
||||
|
||||
var message = $"„{lesson.Topic}“ von {oldDate:dd.MM.yyyy} auf {newDate:dd.MM.yyyy} verschieben?" +
|
||||
(newPeriod is not null ? $"\nNeue Stundennummer: {newPeriod}." : "") +
|
||||
(shiftFollowingLessons
|
||||
? affectedCount > 0
|
||||
? $"\n{affectedCount} spätere Stunde(n) derselben Einheit verschieben sich um denselben Versatz mit."
|
||||
: "\nKeine späteren, noch offenen Stunden derselben Einheit betroffen."
|
||||
: "");
|
||||
|
||||
if (!await confirmation.ConfirmAsync("Stunde verschieben?", message, ct))
|
||||
return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt.");
|
||||
|
||||
try
|
||||
{
|
||||
new LessonSchedulingService(lessons).Move(lesson, newDate, newPeriod, shiftFollowingLessons);
|
||||
}
|
||||
catch (InvalidOperationException ex)
|
||||
{
|
||||
return new WriteResultDto(false, null, ex.Message);
|
||||
}
|
||||
return new WriteResultDto(true, lesson.Id, "Stunde verschoben.");
|
||||
}
|
||||
|
||||
[Description("Löscht eine Einzelstunde endgültig, inklusive ihrer Anhänge. Anders als die meisten anderen Löschvorgänge in LehrerApp landet eine gelöschte Stunde NICHT im Papierkorb — nicht rückgängig zu machen. Muss der Nutzer erst bestätigen.")]
|
||||
public async Task<WriteResultDto> DeleteLesson(
|
||||
[Description("ID der zu löschenden Einzelstunde.")] Guid lessonId,
|
||||
CancellationToken ct = default)
|
||||
{
|
||||
var lesson = lessons.GetById(lessonId);
|
||||
if (lesson is null) return new WriteResultDto(false, null, "Unbekannte Stunden-ID.");
|
||||
|
||||
var message = $"Die Stunde „{lesson.Topic}“ vom {lesson.Date:dd.MM.yyyy} wird endgültig gelöscht. " +
|
||||
"Das kann NICHT rückgängig gemacht werden (kein Papierkorb für Stunden).";
|
||||
if (!await confirmation.ConfirmAsync("Stunde endgültig löschen?", message, ct))
|
||||
return new WriteResultDto(false, null, "Vom Nutzer abgelehnt oder nicht bestätigt.");
|
||||
|
||||
lessons.Delete(lessonId);
|
||||
return new WriteResultDto(true, lessonId, "Stunde gelöscht.");
|
||||
}
|
||||
|
||||
private static LessonDto ToDto(Lesson l) => new(
|
||||
l.Id, l.UnitId, l.GroupId, l.Date, l.LessonNumber, l.Topic, l.Homework, l.Status,
|
||||
l.Phases.Select(p => new LessonPhaseDto(p.Id, p.Name, p.DurationMinutes, p.Activity, p.Material, p.Shorthand)).ToList(),
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="LehrerApp.Desktop.Views.Mcp.McpConfirmDialog"
|
||||
x:CompileBindings="False"
|
||||
Title="{Binding Title}"
|
||||
Width="520" MinHeight="240" SizeToContent="Height"
|
||||
CanResize="True" WindowStartupLocation="CenterOwner"
|
||||
Topmost="True" ShowInTaskbar="True">
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
<Border Grid.Row="0" Background="#FFF4E5" BorderBrush="#D97706" BorderThickness="0,0,0,3" Padding="24,16">
|
||||
<StackPanel Spacing="4">
|
||||
<TextBlock Text="Vorschlag deines KI-Assistenten — bitte prüfen" FontSize="12" FontWeight="SemiBold" Foreground="#8A5A00"/>
|
||||
<TextBlock Text="{Binding Title}" FontSize="20" FontWeight="Bold" TextWrapping="Wrap"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<ScrollViewer Grid.Row="1" MaxHeight="380" Margin="24,18">
|
||||
<TextBlock Text="{Binding Message}" TextWrapping="Wrap" FontSize="14" LineHeight="20"/>
|
||||
</ScrollViewer>
|
||||
<Grid Grid.Row="2" ColumnDefinitions="*,10,*" Margin="24,0,24,20">
|
||||
<Button Grid.Column="0" Content="Ablehnen" HorizontalAlignment="Stretch" Padding="0,12" FontSize="14" Click="OnCancel"/>
|
||||
<Button Grid.Column="2" Content="{Binding ConfirmText}" HorizontalAlignment="Stretch" Padding="0,12" FontSize="14"
|
||||
FontWeight="SemiBold" Background="#D97706" Foreground="White" Click="OnConfirm"/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
@@ -0,0 +1,12 @@
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Interactivity;
|
||||
|
||||
namespace LehrerApp.Desktop.Views.Mcp;
|
||||
|
||||
public partial class McpConfirmDialog : Window
|
||||
{
|
||||
public McpConfirmDialog() => InitializeComponent();
|
||||
|
||||
private void OnConfirm(object? sender, RoutedEventArgs e) => Close(true);
|
||||
private void OnCancel(object? sender, RoutedEventArgs e) => Close(false);
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
namespace LehrerApp.Desktop.Views.Mcp;
|
||||
|
||||
/// <summary>DataContext für <see cref="McpConfirmDialog"/> — bewusst ein eigener, auffälligerer
|
||||
/// Dialog statt Wiederverwendung von <see cref="Views.Shared.ConfirmDialog"/>: Letzterer wird an
|
||||
/// vielen Stellen der App für routinemäßige Bestätigungen verwendet, eine Änderung an dessen
|
||||
/// Größe/Optik hätte all diese Stellen mitbetroffen. Ein von der KI vorgeschlagener Schreibzugriff
|
||||
/// soll sich dagegen bewusst absetzen (Nutzer-Feedback: der ursprüngliche gemeinsame Dialog fiel zu
|
||||
/// wenig auf, wenn LehrerApp gerade nicht im Vordergrund war).</summary>
|
||||
public class McpConfirmDialogInfo
|
||||
{
|
||||
public string Title { get; init; } = "";
|
||||
public string Message { get; init; } = "";
|
||||
public string ConfirmText { get; init; } = "Übernehmen";
|
||||
}
|
||||
@@ -2650,6 +2650,40 @@ folgenden Punkte gehören direkt in `LehrerApp.Desktop`:
|
||||
ohne erneutes `get_lesson_plans` möglich ist. 4 neue Tests in
|
||||
[McpToolsTests.cs](LehrerApp.Desktop.Tests/McpToolsTests.cs) (jetzt 32).
|
||||
|
||||
- [x] **4.5.31** Bestätigungsdialog-Prominenz + `move_lesson`/`delete_lesson` (2026-09-12,
|
||||
Nutzer-Feedback nach erstem Live-Test mit echtem Bridge-Prozess).
|
||||
- **Dialog fiel zu wenig auf:** der bisher wiederverwendete `ConfirmDialog` (Views/Shared,
|
||||
an vielen anderen Stellen der App für Routine-Bestätigungen im Einsatz) ist nur zum
|
||||
Hauptfenster modal — läuft LehrerApp im Hintergrund, während der Anstoß von einem
|
||||
KI-Client in einem anderen Fenster kommt (der Normalfall bei MCP), geht der Dialog leicht
|
||||
unter. Neuer, eigenständiger
|
||||
[McpConfirmDialog](LehrerApp.Desktop/Views/Mcp/McpConfirmDialog.axaml) statt einer
|
||||
Style-Änderung am geteilten `ConfirmDialog` (hätte alle dessen bestehenden Aufrufer
|
||||
mitbetroffen): breiter (520 statt 420), auffälliger Kopfbereich ("Vorschlag deines
|
||||
KI-Assistenten — bitte prüfen"), `Topmost`. `AvaloniaMcpConfirmationService` holt das
|
||||
Hauptfenster zusätzlich aus einer möglichen Minimierung und aktiviert es
|
||||
(`WindowState`/`Activate()`), bevor der Dialog erscheint.
|
||||
- **`move_lesson`:** deckt sowohl "eine Stunde verschieben" als auch "Lücke für eine neue
|
||||
Stunde öffnen" ab, indem es die bereits vorhandene, bislang nur UI-seitig genutzte
|
||||
[LessonSchedulingService.Move](LehrerApp.Core/Services/LessonSchedulingService.cs) kapselt
|
||||
(`shiftFollowingLessons=true` verschiebt alle späteren, noch nicht durchgeführten Stunden
|
||||
derselben Einheit um denselben Tages-Versatz mit — "Lücke öffnen" = die Stunde hinter der
|
||||
Lücke so verschieben, danach `create_lesson` auf das freigewordene Datum). Keine neue
|
||||
Terminlogik geschrieben, nur wiederverwendet.
|
||||
- **`delete_lesson` — bewusste, gezielte Ausnahme von "v1 ohne Lösch-Tools":** explizite
|
||||
Nutzeranforderung. `Lesson` hängt (anders als die meisten anderen Entitäten) nicht am
|
||||
Papierkorb-System — `ILessonRepository.Delete` löscht endgültig, kein `Restore`. Deshalb:
|
||||
eigene `McpToolScope.AllowedDestructiveWriteTools`-Liste getrennt von den normalen
|
||||
Write-Tools (fällt beim Lesen sofort auf), `Destructive = true`-Annotation im
|
||||
MCP-Protokoll, und eine Bestätigungsnachricht, die explizit "NICHT rückgängig zu machen"
|
||||
sagt (Wortlaut an den bestehenden Lösch-Dialog in `PlanningTabView.axaml.cs`
|
||||
angelehnt: "wird endgültig gelöscht").
|
||||
- 8 neue Tests in [McpToolsTests.cs](LehrerApp.Desktop.Tests/McpToolsTests.cs) (jetzt 40),
|
||||
u.a.: `shiftFollowingLessons` verschiebt nur Stunden NACH der bewegten, nicht davor;
|
||||
ein belegtes Ziel liefert nach bereits erfolgter Bestätigung einen Fehler statt
|
||||
stillschweigend zu überschreiben (die bestehende `EnsureTargetIsFree`-Prüfung in
|
||||
`LessonSchedulingService` greift unverändert).
|
||||
|
||||
**Wichtige Abweichung von der ursprünglichen Planung (5.2):** Vor der Umsetzung zeigte sich,
|
||||
dass 5.2 wie ursprünglich beschrieben eine zweite, parallele Fehlzeiten-Erfassung neben dem
|
||||
bereits bestehenden Anwesenheits-Tracking aus Kapitel 3 (`ParticipationEntry.Attendance`,
|
||||
|
||||
Reference in New Issue
Block a user