# Oberfläche — Leitfaden Für alle, die an `src/ClawdDotNet.Desktop` arbeiten. Stand: 2026-08-23. --- ## 1. Stand **Die Portierung ist abgeschlossen.** Es gibt keine Platzhalter mehr und keine WinForms-Vorlage, gegen die man vergleichen könnte: Die alte Oberfläche (`ClawdDotNet.csproj`, `frm_*.cs`, `UI/`, `Models/`, `EmbeddedUI/`) ist am 2026-08-23 aus dem Arbeitsbaum entfernt worden. Wer sie doch einmal braucht, findet sie im Tag `vor-fruehjahrsputz-2026-08`. Neun Bereiche, alle nativ in Avalonia: | Gruppe | Bereich | Ansichtsmodell | |---|---|---| | Arbeit | Chat, Agenten, Aufgaben | `ChatPageViewModel`, `AgentsPageViewModel`, `TasksPageViewModel` | | Analyse | Token-Verbrauch, Agenten-Chats | `TokenUsagePageViewModel`, `AgentChatsPageViewModel` | | System | Sicherung, Protokoll, Einstellungen, Info | `BackupPageViewModel`, `LogPageViewModel`, `SettingsPageViewModel`, `InfoPageViewModel` | Dazu die Fenster `InstancePickerWindow`, `LicenseWindow`, `TextEditorWindow` und die Dialoge zum Anlegen von Agent, Auftrag und Dienst. Das Erscheinungsbild folgt dem Entwurf in `Mockup/` — wer daran etwas ändert, liest zuerst `Mockup/extracted/mockup/Implementierungsleitfaden.md`. **Was in der Oberfläche noch fehlt**, siehe [Roadmap](Roadmap.md): die Freigabe-Ansicht für gestagte Aufrufe (A2) und die Schaltfläche „Fehler melden" (DC1). --- ## 2. Drei Regeln, die nicht verletzt werden dürfen ### 2.1 Der Schichtschnitt ``` src/ClawdDotNet.App ← Fachlogik. KEIN Verweis auf Avalonia. Niemals. src/ClawdDotNet.Desktop ← Oberfläche. Darf App und Core verwenden. ``` `ClawdDotNet.App` muss ohne Fenster laufen — darauf setzt der geplante systemd-Dienst auf. Sobald dort ein `using Avalonia…` steht, ist der Schnitt kaputt und fällt erst Wochen später auf. **Faustregel:** Alles, was Dateien liest, rechnet oder mit der Engine spricht, gehört nach `App`. Alles, was etwas anzeigt, nach `Desktop`. ### 2.2 Fäden Ereignisse aus `AgentEngine`, `TaskScanner`, `OpenRouterStatusService` und `BackupScheduler` kommen auf **Hintergrundfäden**. Eine `ObservableCollection` von dort aus zu ändern wirft entweder oder beschädigt still die Anzeige. ```csharp // Aus einem Ereignis der Fachschicht heraus: Dispatcher.UIThread.Post(() => Lines.Add(neu)); // Wenn ein Rückgabewert gebraucht wird: await Dispatcher.UIThread.InvokeAsync(() => …); ``` Ein `DispatcherTimer` läuft dagegen bereits auf dem Oberflächenfaden — dort ist kein Wechsel nötig (siehe `LogPageViewModel`). ### 2.3 Avalonia **12**, nicht 11 Praktisch alle Anleitungen im Netz sind für Avalonia 11 und lassen sich hier nicht übernehmen. Bekannte Unterschiede: - `BindingPlugins` ist nicht mehr öffentlich. Das übliche `DisableAvaloniaDataAnnotationValidation()` aus den 11er-Vorlagen **entfällt ersatzlos** — nicht nachbauen. - `ShutdownMode` voll qualifizieren: `Avalonia.Controls.ShutdownMode`. Diese Fehler brechen den Build. Das ist gut — sie fallen sofort auf. --- ## 3. Das Muster Der Logs-Bereich ist als vollständiges Beispiel gebaut. Drei Dateien, drei Aufgaben: **`src/ClawdDotNet.App/Services/LogTail.cs`** — die Fachlogik. Liest Dateien, kennt keine Oberfläche, wäre ohne Fenster lauffähig. **`src/ClawdDotNet.Desktop/ViewModels/LogPageViewModel.cs`** — das Ansichtsmodell. Erbt von `PageViewModel`, hält Zustand und Befehle. Kennt keine Steuerelemente. **`src/ClawdDotNet.Desktop/Views/LogPageView.axaml`** — die Ansicht. Nur Aufbau und Bindungen. ### Ein neuer Bereich in vier Schritten **1.** Ansichtsmodell anlegen, von `PageViewModel` erbend: ```csharp public sealed partial class InfoPageViewModel : PageViewModel { public InfoPageViewModel(AppHost? host) : base("Info") { … } } ``` `AppHost?` ist **nullbar** — der Entwurfsmodus des Editors erzeugt das Ansichtsmodell ohne laufenden Aufbau. Bei `null` einfach nichts starten und Beispielwerte zeigen. **2.** Ansicht anlegen: `Views/InfoPageView.axaml` + `.axaml.cs`. Der Name muss der Konvention folgen — `ViewLocator` sucht `…ViewModels.FooViewModel` → `…Views.FooView`. Passt der Name nicht, steht der gesuchte Typ im Fenster statt der Ansicht. **3.** In `MainWindowViewModel` den Platzhalter ersetzen: ```csharp new PlaceholderPageViewModel("Info", "…") // vorher new InfoPageViewModel(host) // nachher ``` **4.** `x:DataType` in der AXAML setzen. Ohne das greifen die kompilierten Bindungen nicht und Tippfehler in Bindungspfaden fallen erst zur Laufzeit auf. ### Werkzeugkasten - Zustand: `[ObservableProperty] private string _text = "";` → erzeugt `Text` samt Benachrichtigung. - Befehle: `[RelayCommand] private void Speichern() { … }` → bindbar als `SpeichernCommand`. - Formatierung gehört in `Styles/`, nicht an einzelne Steuerelemente. Seit der Umsetzung des Entwurfs aus `Mockup/` liegt sie in drei Dateien: `Theme.axaml` (Farben je Thema, Schriften), `Icons.axaml` (Symbolgeometrien), `Shell.axaml` (Steuerelement-Vorlagen und Stilklassen). Klassen: `h1`, `h2`, `kicker`, `label`, `caption`, `muted`, `mono`, `card`, `console`, `hr`, `sep`, `toolbar`, `thead`, `tr`, `th`, `td`, `tag`, `statusbar`, `topbar`, `sidebar`, `nav`; an Schaltflächen zusätzlich `primary`, `toolbar`, `ghost`, `flat`, `icon`. - Symbole über `Controls/StrokeIcon.cs` mit einer Geometrie aus `Icons.axaml`. Die Farbe wird geerbt — nicht gesetzt. - Rahmen mit Eckmarken über `Controls/BlueprintFrame.cs`. Sparsam: nur Dialoge und die Info-Karte. - Tabellen von Hand aus `Border.thead` + `ListBox.table`, nicht mit `DataGrid`. Das Paket ist nicht mehr referenziert. - **Avalonia 12 hat die Ressourcenschlüssel des Fluent-Themas umgebaut.** Die aus 11er-Anleitungen bekannten Namen (`ButtonBackground`, `TextControlBackground`, `ControlCornerRadius` …) existieren nicht mehr; ein Setter darauf ist wirkungslos und fällt nicht auf. Für neue Steuerelemente deshalb eine eigene `ControlTheme` in `Shell.axaml` schreiben statt zu versuchen, Fluent umzufärben. - Ordner öffnen: `Core.Storage.SystemShell.OpenFolder(pfad)`. **Kein** `explorer.exe`. - Dateinamen erzeugen: `Core.Storage.PortableFileName.Sanitize(name)`. --- ## 4. Prüfliste für die leisen Fehler Diese Klasse bricht weder den Build noch die Tests. Vor jeder Abgabe durchgehen: - [ ] **Fenster-Schließen behandelt?** Wartet der Code auf eine Antwort aus einem Fenster (`TaskCompletionSource`), muss `window.Closed` als Abbruch gelten. Sonst hängt der Ablauf lautlos für immer. - [ ] **Sammlungen nur vom Oberflächenfaden geändert?** Siehe 2.2. - [ ] **Wächst etwas unbegrenzt?** Listen, die im Betrieb volllaufen, brauchen eine Obergrenze (`LogPageViewModel.MaxLines = 2000` als Vorbild). - [ ] **Timer beendet?** `DispatcherTimer` in einem Ansichtsmodell läuft weiter, auch wenn der Bereich nicht sichtbar ist. Bei teuren Abfragen anhalten. - [ ] **Farben aus dem Thema?** Keine festen Farbwerte — die Anwendung läuft hell und dunkel. `{DynamicResource …}` verwenden. - [ ] **Keine relativen Pfade.** `./Backups` und Ähnliches hängt vom Arbeitsverzeichnis ab und zeigt unter Linux ins Leere. `AppPaths.DataDirectory` verwenden. - [ ] **Kein `MessageBox`, kein `System.Windows.Forms`, kein `System.Drawing`.** --- ## 5. Abnahme ```bash dotnet build ClawdDotNet.slnx ``` ```bash dotnet test tests/ClawdDotNet.Core.Tests/ClawdDotNet.Core.Tests.csproj ``` Beide müssen fehlerfrei sein — 567 Tests, keine neuen Fehlschläge. **Und dann tatsächlich starten.** Die Oberfläche hat keine Testabdeckung; die Fehler aus Abschnitt 4 fallen ausschließlich beim Laufen auf. ```bash dotnet run --project src/ClawdDotNet.Desktop ``` Hinweis: Ein Starttest hinterlässt unter Windows einen Prozess, der die `.exe` sperrt und den nächsten Build mit `MSB3021` scheitern lässt. Aufräumen mit: ```bash powershell -Command "Get-Process ClawdDotNet -EA SilentlyContinue | Stop-Process -Force" ``` --- ## 6. Wenn etwas unklar ist Lieber nachfragen als raten. Zwei Dinge sind besonders leicht falsch zu machen: - **Was gehört in welche Schicht?** Im Zweifel nach `App` — von dort kann die Oberfläche es holen, umgekehrt nicht. - **Wie kommen Daten aus der Engine in die Ansicht?** `AppHost` gibt `Engine`, `Scanner`, `Staging`, `Status` und `Usage` heraus; alle sind **nullbar**, wenn kein OpenRouter-Schlüssel hinterlegt ist. Diesen Fall mitdenken — die Anwendung läuft dann bewusst ohne Agenten.