Files
ClawdDotNet/ClawdDotNet_Prompt_WebviewChatWinForms.md
T
RichardandClaude Opus 4.8 2fed388c99 Initial commit: ClawdDotNet
Import des bestehenden Projektstands in Git.
- .NET 10 WinForms Anwendung (Multi-Agent / Tool-System)
- .gitignore fuer Build-Artefakte, Secrets und Runtime-Daten ergaenzt

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-26 18:21:46 +02:00

793 lines
26 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# ClawdDotNet Prompt-Anhang: WinForms & WebView2 Integration
Dieser Abschnitt ergänzt den Haupt-Entwicklungsprompt und behandelt ausschließlich
die WinForms-UI-Schicht mit WebView2. Er baut auf den bereits definierten Core-Typen
(AgentConfig, InstanceConfig, AgentEngine, IAgentTool etc.) auf.
---
## Übersicht: Zwei WebView2-Kontexte
Es gibt genau zwei WebView2-Kontexte im Host. Sie sind vollständig getrennt
und haben unterschiedliche Sicherheits-Scopes:
| Kontext | Control | Form | Zweck |
|---|---|---|---|
| `webView_chat` | `WebView2` in `frm_main` | Hauptfenster | Agentenübersicht + Auswahl + Chat mit einem Agenten |
| `webView_chat2` | `WebView2` in `frm_chat` | Einzelchat-Fenster | Chat mit genau einem Agenten, mehrfach öffenbar |
`frm_chat` ist bewusst ein eigenständiges, nicht-modales Fenster — es kann mehrfach
instanziiert werden, sodass der Nutzer mehrere Agenten-Chats nebeneinander
auf dem Bildschirm überwachen kann. Jede `frm_chat`-Instanz kennt genau einen `AgentId`.
---
## Sicherheitsarchitektur: Physische Trennung der WebRoots
### Zwei Hostnamen, zwei Quellen — niemals überlappend
```
Assembly (Embedded Resources) Disk (vom FileRW-Tool beschreibbar)
────────────────────────────── ──────────────────────────────────
Host/EmbeddedUI/ data/{instanceId}/
overview.html ← frm_main wwwroot/ ← Kestrel-Root
overview.css webView_chat index.html
overview.js styles/
chat.html ← frm_chat data/
chat.css webView_chat2 assets/
bridge.js
```
**Kernregel:** `EmbeddedUI/` existiert nur als Assembly-Resource.
Sie hat keinen Dateisystempfad, auf den ein Tool zeigen könnte.
Kein `FileRW`-Tool bekommt jemals einen `rootPath`, der auf `EmbeddedUI/` zeigt.
### WebView2 Virtual Host Mapping
```csharp
// Beide Mappings werden in InitWebViewAsync() jeder Form gesetzt:
// Intern aus Assembly-Stream (temporär extrahiert beim Start)
webView.CoreWebView2.SetVirtualHostNameToFolderMapping(
"ui.clwd.internal",
EmbeddedUiManager.GetExtractedPath(), // einmalig beim App-Start nach temp/
CoreWebView2HostResourceAccessKind.DenyCors);
// Extern Agent-generierte Inhalte auf Disk
webView.CoreWebView2.SetVirtualHostNameToFolderMapping(
"dash.clwd.local",
_instanceConfig.WwwRootPath,
CoreWebView2HostResourceAccessKind.Allow);
```
`ui.clwd.internal` → nur lesbar, kein Cross-Origin-Zugriff von außen
`dash.clwd.local` → lesbar für den WebView, schreibbar nur durch FileRW-Tool
### Startvalidierung (Pflicht, einmalig in Program.cs)
```csharp
// Sicherheitscheck beim App-Start Exception wenn verletzt:
var wwwAbs = Path.GetFullPath(instanceConfig.WwwRootPath);
var uiAbs = Path.GetFullPath(EmbeddedUiManager.GetExtractedPath());
if (wwwAbs.StartsWith(uiAbs) || uiAbs.StartsWith(wwwAbs))
throw new InvalidOperationException(
"SECURITY: WwwRootPath and EmbeddedUI path must never overlap.");
```
---
## EmbeddedUiManager
**Datei: `Host/UI/EmbeddedUiManager.cs`**
Aufgabe: HTML/CSS/JS-Dateien aus den Assembly Embedded Resources einmalig beim
Programmstart in einen temporären Ordner extrahieren. WebView2 kann nur auf
Dateisystempfade mappen, nicht direkt auf Streams.
```csharp
namespace ClawdDotNet.Host.UI;
public static class EmbeddedUiManager
{
private static string? _extractedPath;
// Einmalig beim App-Start aufrufen (vor Application.Run)
public static string ExtractToTemp()
{
if (_extractedPath != null) return _extractedPath;
var tempDir = Path.Combine(Path.GetTempPath(), "ClawdDotNet_UI",
Assembly.GetExecutingAssembly()
.GetName().Version?.ToString() ?? "dev");
Directory.CreateDirectory(tempDir);
var asm = Assembly.GetExecutingAssembly();
// Alle Embedded Resources im Namespace "ClawdDotNet.Host.EmbeddedUI"
foreach (var name in asm.GetManifestResourceNames()
.Where(n => n.Contains(".EmbeddedUI.")))
{
// "ClawdDotNet.Host.EmbeddedUI.chat.css" → "chat.css"
var fileName = name.Split(".EmbeddedUI.").Last();
var dest = Path.Combine(tempDir, fileName);
using var stream = asm.GetManifestResourceStream(name)!;
using var file = File.Create(dest);
stream.CopyTo(file);
}
_extractedPath = tempDir;
return tempDir;
}
public static string GetExtractedPath()
=> _extractedPath ?? throw new InvalidOperationException(
"EmbeddedUiManager.ExtractToTemp() must be called first.");
}
```
Embedded Resources werden in der `.csproj` so eingebunden:
```xml
<ItemGroup>
<EmbeddedResource Include="EmbeddedUI\**\*" />
</ItemGroup>
```
---
## C#JavaScript Bridge
**Datei: `Host/UI/WebViewBridge.cs`**
Eine Bridge-Instanz pro WebView2-Control. Kapselt die gesamte
bidirektionale Kommunikation. Keine rohen `ExecuteScriptAsync`-Aufrufe
außerhalb dieser Klasse.
```csharp
namespace ClawdDotNet.Host.UI;
public sealed class WebViewBridge : IDisposable
{
private readonly Microsoft.Web.WebView2.WinForms.WebView2 _wv;
private readonly ILogger<WebViewBridge> _logger;
// Eingehende Nachrichten vom Browser → C#
public event Action<BridgeMessage>? MessageReceived;
public WebViewBridge(
Microsoft.Web.WebView2.WinForms.WebView2 webView,
ILogger<WebViewBridge> logger)
{
_wv = webView;
_logger = logger;
_wv.CoreWebView2.WebMessageReceived += OnWebMessageReceived;
}
// C# → Browser: typisiert, immer als JSON
public async Task SendAsync(BridgeMessage message, CancellationToken ct = default)
{
var json = JsonSerializer.Serialize(message, BridgeJsonOptions.Default);
// Muss auf dem UI-Thread ausgeführt werden
await _wv.InvokeAsync(async () =>
await _wv.CoreWebView2.ExecuteScriptAsync(
$"window.__bridge?.receive({json})"));
}
private void OnWebMessageReceived(object? sender,
CoreWebView2WebMessageReceivedEventArgs e)
{
try
{
var msg = JsonSerializer.Deserialize<BridgeMessage>(
e.WebMessageAsJson, BridgeJsonOptions.Default);
if (msg != null) MessageReceived?.Invoke(msg);
}
catch (Exception ex)
{
_logger.LogError(ex, "Bridge: failed to deserialize incoming message");
}
}
public void Dispose()
=> _wv.CoreWebView2.WebMessageReceived -= OnWebMessageReceived;
}
```
### BridgeMessage Nachrichtenformat
**Datei: `Host/UI/BridgeMessage.cs`**
Alle Nachrichten in beide Richtungen verwenden diesen Typ.
Das `Type`-Feld bestimmt, was in `Payload` steckt.
```csharp
namespace ClawdDotNet.Host.UI;
public sealed record BridgeMessage(
string Type, // siehe Konstanten unten
string? AgentId = null,
string? Content = null, // Chat-Text, HTML-Snippet
string? Status = null, // "running" | "idle" | "error"
int? StepCount = null,
int? TokenCount = null,
string? Error = null,
object? Extra = null // type-spezifische Zusatzdaten
);
// Typ-Konstanten (C# → Browser)
public static class BridgeTypes
{
// frm_main: overview.html
public const string AgentListUpdate = "agent_list_update"; // Alle Agenten initial laden
public const string AgentStatusUpdate = "agent_status"; // Statusänderung eines Agenten
public const string SelectAgent = "select_agent"; // Agenten im Chat auswählen
// frm_main + frm_chat: chat.html
public const string ChatMessage = "chat_message"; // Neue Nachricht anzeigen
public const string ChatTyping = "chat_typing"; // Tipp-Indikator an/aus
public const string ChatHistory = "chat_history"; // Verlauf beim Öffnen laden
public const string RunStarted = "run_started"; // Agent-Run begann
public const string RunFinished = "run_finished"; // Agent-Run beendet
// Browser → C# (eingehend)
public const string UserMessage = "user_message"; // Nutzer hat Enter gedrückt
public const string OpenAgentChat = "open_agent_chat"; // "Eigenes Fenster öffnen"
public const string RunNow = "run_now"; // Manueller Run-Trigger
public const string AbortRun = "abort_run"; // Run abbrechen
}
```
---
## frm_main Hauptfenster
**Datei: `Host/Forms/frm_main.cs`**
`frm_main` enthält `webView_chat` (bereits angelegt). Dieses WebView zeigt
`overview.html`: eine Seitenleiste mit allen Agenten und einen Chat-Bereich
für den aktuell ausgewählten Agenten.
```csharp
namespace ClawdDotNet.Host.Forms;
public partial class frm_main : Form
{
private readonly InstanceConfig _instance;
private readonly AgentEngine _engine;
private readonly AgentScheduler _scheduler;
private readonly ILogger<frm_main> _logger;
private WebViewBridge? _bridge;
private string? _selectedAgentId;
// Offene Einzelchat-Fenster: AgentId → frm_chat
private readonly Dictionary<string, frm_chat> _chatWindows = new();
public frm_main(InstanceConfig instance, AgentEngine engine,
AgentScheduler scheduler, ILogger<frm_main> logger)
{
InitializeComponent();
_instance = instance;
_engine = engine;
_scheduler = scheduler;
_logger = logger;
}
private async void frm_main_Load(object sender, EventArgs e)
{
await InitWebViewAsync();
_scheduler.RunStatusChanged += OnRunStatusChanged; // Event aus Core
}
private async Task InitWebViewAsync()
{
await webView_chat.EnsureCoreWebView2Async();
// Virtual Host Mappings
webView_chat.CoreWebView2.SetVirtualHostNameToFolderMapping(
"ui.clwd.internal",
EmbeddedUiManager.GetExtractedPath(),
CoreWebView2HostResourceAccessKind.DenyCors);
webView_chat.CoreWebView2.SetVirtualHostNameToFolderMapping(
"dash.clwd.local",
_instance.WwwRootPath,
CoreWebView2HostResourceAccessKind.Allow);
_bridge = new WebViewBridge(webView_chat, /* logger */);
_bridge.MessageReceived += OnBridgeMessage;
webView_chat.CoreWebView2.Navigate(
"https://ui.clwd.internal/overview.html");
// Kurz warten bis DOM bereit, dann Agentenliste senden
await Task.Delay(300);
await PushAgentListAsync();
}
private async Task PushAgentListAsync()
{
// Alle AgentConfigs als Liste → overview.html baut die Sidebar auf
var agents = _instance.Agents.Select(a => new
{
agentId = a.AgentId,
displayName = a.DisplayName,
model = a.Model,
status = _engine.GetStatus(a.AgentId) // "idle"|"running"|"error"
});
await _bridge!.SendAsync(new BridgeMessage(
Type: BridgeTypes.AgentListUpdate,
Extra: agents));
}
private async void OnBridgeMessage(BridgeMessage msg)
{
// Immer auf UI-Thread
if (InvokeRequired) { Invoke(() => OnBridgeMessage(msg)); return; }
switch (msg.Type)
{
case BridgeTypes.UserMessage:
// Nutzer hat im Chat Enter gedrückt
if (_selectedAgentId is null || msg.Content is null) break;
await HandleUserMessageAsync(_selectedAgentId, msg.Content);
break;
case BridgeTypes.SelectAgent:
// Agenten in der Sidebar angeklickt → Chat-Verlauf laden
_selectedAgentId = msg.AgentId;
await LoadChatHistoryAsync(msg.AgentId!);
break;
case BridgeTypes.OpenAgentChat:
// "Eigenes Fenster" Button → frm_chat öffnen oder fokussieren
OpenChatWindow(msg.AgentId!);
break;
case BridgeTypes.RunNow:
_ = _engine.RunAsync(msg.AgentId!, CancellationToken.None);
break;
case BridgeTypes.AbortRun:
_engine.Abort(msg.AgentId!);
break;
}
}
private void OpenChatWindow(string agentId)
{
if (_chatWindows.TryGetValue(agentId, out var existing)
&& !existing.IsDisposed)
{
existing.BringToFront();
return;
}
var agentConfig = _instance.Agents.First(a => a.AgentId == agentId);
var frm = new frm_chat(agentConfig, _engine, /* logger */);
frm.FormClosed += (_, _) => _chatWindows.Remove(agentId);
_chatWindows[agentId] = frm;
frm.Show(this); // nicht-modal, Elternfenster = frm_main
}
private void OnRunStatusChanged(string agentId, AgentRunStatus status)
{
// Vom Scheduler/Engine gefeuert auf UI-Thread pushen
this.InvokeAsync(async () =>
await _bridge!.SendAsync(new BridgeMessage(
Type: BridgeTypes.AgentStatusUpdate,
AgentId: agentId,
Status: status.ToString().ToLower(),
StepCount: status.StepCount,
TokenCount: status.TokensUsed)));
}
private async Task HandleUserMessageAsync(string agentId, string text)
{
// Eigene Nachricht sofort anzeigen
await _bridge!.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatMessage,
AgentId: agentId,
Content: text,
Extra: new { role = "user", timestamp = DateTime.Now }));
// Tipp-Indikator an
await _bridge.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatTyping, AgentId: agentId));
// Chat-Run starten (non-blocking)
_ = Task.Run(async () =>
{
var result = await _engine.ChatAsync(agentId, text, CancellationToken.None);
await _bridge.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatMessage,
AgentId: agentId,
Content: result.FinalMessage,
Extra: new { role = "agent", timestamp = DateTime.Now }));
});
}
private async Task LoadChatHistoryAsync(string agentId)
{
var history = await _engine.GetChatHistoryAsync(agentId);
await _bridge!.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatHistory,
AgentId: agentId,
Extra: history));
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
_bridge?.Dispose();
_scheduler.RunStatusChanged -= OnRunStatusChanged;
base.OnFormClosed(e);
}
}
```
---
## frm_chat Einzelchat-Fenster
**Datei: `Host/Forms/frm_chat.cs`**
`frm_chat` enthält `webView_chat2` (bereits angelegt). Dieses Fenster zeigt
den Chat mit genau einem Agenten. Es kann beliebig oft gleichzeitig geöffnet
sein — jede Instanz ist vollständig unabhängig.
```csharp
namespace ClawdDotNet.Host.Forms;
public partial class frm_chat : Form
{
private readonly AgentConfig _agentConfig;
private readonly AgentEngine _engine;
private readonly ILogger<frm_chat> _logger;
private WebViewBridge? _bridge;
public frm_chat(AgentConfig agentConfig, AgentEngine engine,
ILogger<frm_chat> logger)
{
InitializeComponent();
_agentConfig = agentConfig;
_engine = engine;
_logger = logger;
// Fenstertitel = Agent-Name
Text = $"Chat {agentConfig.DisplayName}";
}
private async void frm_chat_Load(object sender, EventArgs e)
=> await InitWebViewAsync();
private async Task InitWebViewAsync()
{
await webView_chat2.EnsureCoreWebView2Async();
// Identische Virtual Host Mappings wie frm_main
webView_chat2.CoreWebView2.SetVirtualHostNameToFolderMapping(
"ui.clwd.internal",
EmbeddedUiManager.GetExtractedPath(),
CoreWebView2HostResourceAccessKind.DenyCors);
webView_chat2.CoreWebView2.SetVirtualHostNameToFolderMapping(
"dash.clwd.local",
// WwwRootPath kommt vom InstanceConfig über DI/Singleton
ServiceLocator.Get<InstanceConfig>().WwwRootPath,
CoreWebView2HostResourceAccessKind.Allow);
_bridge = new WebViewBridge(webView_chat2, /* logger */);
_bridge.MessageReceived += OnBridgeMessage;
// chat.html lädt für einen bestimmten Agenten
webView_chat2.CoreWebView2.Navigate(
$"https://ui.clwd.internal/chat.html?agent={_agentConfig.AgentId}");
await Task.Delay(300);
// AgentInfo und Verlauf initial senden
await _bridge.SendAsync(new BridgeMessage(
Type: BridgeTypes.AgentListUpdate,
AgentId: _agentConfig.AgentId,
Extra: new { agents = new[] { new {
agentId = _agentConfig.AgentId,
displayName = _agentConfig.DisplayName,
model = _agentConfig.Model
}}}));
await LoadChatHistoryAsync();
}
private async void OnBridgeMessage(BridgeMessage msg)
{
if (InvokeRequired) { Invoke(() => OnBridgeMessage(msg)); return; }
switch (msg.Type)
{
case BridgeTypes.UserMessage:
await HandleUserMessageAsync(msg.Content ?? "");
break;
case BridgeTypes.RunNow:
_ = _engine.RunAsync(_agentConfig.AgentId, CancellationToken.None);
break;
case BridgeTypes.AbortRun:
_engine.Abort(_agentConfig.AgentId);
break;
}
}
private async Task HandleUserMessageAsync(string text)
{
await _bridge!.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatMessage,
AgentId: _agentConfig.AgentId,
Content: text,
Extra: new { role = "user", timestamp = DateTime.Now }));
await _bridge.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatTyping, AgentId: _agentConfig.AgentId));
_ = Task.Run(async () =>
{
var result = await _engine.ChatAsync(
_agentConfig.AgentId, text, CancellationToken.None);
await _bridge!.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatMessage,
AgentId: _agentConfig.AgentId,
Content: result.FinalMessage,
Extra: new { role = "agent", timestamp = DateTime.Now }));
});
}
private async Task LoadChatHistoryAsync()
{
var history = await _engine.GetChatHistoryAsync(_agentConfig.AgentId);
await _bridge!.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatHistory,
AgentId: _agentConfig.AgentId,
Extra: history));
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
_bridge?.Dispose();
base.OnFormClosed(e);
}
}
```
---
## Embedded HTML/JS/CSS Dateistruktur
Alle Dateien liegen in `Host/EmbeddedUI/`. Build Action: `Embedded Resource`.
### overview.html (für webView_chat in frm_main)
Dieses HTML baut die komplette Ansicht aus dem Mockup auf:
- Linke Sidebar: Agentenliste (wird via Bridge befüllt)
- Rechter Bereich: Chat mit dem aktuell ausgewählten Agenten
- "Eigenes Fenster"-Button pro Agent → sendet `open_agent_chat`-Nachricht
Kommunikationsprotokoll (JavaScript-Seite):
```javascript
// bridge.js wird von beiden HTML-Seiten eingebunden
window.__bridge = {
// Eingehend von C#
receive(msg) {
document.dispatchEvent(
new CustomEvent('bridge:' + msg.type, { detail: msg }));
},
// Ausgehend zu C#
send(msg) {
window.chrome.webview.postMessage(JSON.stringify(msg));
}
};
// Beispiel: auf Agentenliste reagieren
document.addEventListener('bridge:agent_list_update', e => {
renderSidebar(e.detail.extra.agents);
});
// Beispiel: Nachricht senden
function sendUserMessage(agentId, text) {
window.__bridge.send({
type: 'user_message',
agentId: agentId,
content: text
});
}
```
### chat.html (für webView_chat2 in frm_chat)
Vereinfachte Version ohne Sidebar — nur der Chat-Bereich.
Liest den `?agent=`-URL-Parameter beim Laden und stellt sich
damit auf den entsprechenden Agenten ein.
```javascript
// chat.html Init
const agentId = new URLSearchParams(location.search).get('agent');
document.addEventListener('bridge:chat_history', e => {
if (e.detail.agentId !== agentId) return;
renderHistory(e.detail.extra);
});
document.addEventListener('bridge:chat_message', e => {
if (e.detail.agentId !== agentId) return;
appendBubble(e.detail.extra.role, e.detail.content,
e.detail.extra.timestamp);
});
document.addEventListener('bridge:chat_typing', e => {
if (e.detail.agentId !== agentId) return;
showTypingIndicator();
});
```
---
## Program.cs Startup-Reihenfolge
```csharp
// Host/Program.cs
[STAThread]
static async Task Main(string[] args)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
// 1. Config laden (--config Argument oder default)
var configPath = GetConfigPath(args);
var instance = InstanceConfig.LoadFromFile(configPath);
// 2. Sicherheitscheck: Pfade dürfen sich nicht überlappen
var uiPath = EmbeddedUiManager.ExtractToTemp(); // extrahiert EmbeddedUI
var wwwPath = Path.GetFullPath(instance.WwwRootPath);
if (wwwPath.StartsWith(uiPath) || uiPath.StartsWith(wwwPath))
throw new InvalidOperationException(
"SECURITY: WwwRootPath and EmbeddedUI path must never overlap.");
// 3. DI-Container aufbauen
var services = new ServiceCollection();
services.AddSingleton(instance);
services.AddSingleton<ToolRegistry>();
services.AddSingleton<PermissionGate>();
services.AddSingleton<AgentEngine>();
services.AddSingleton<AgentScheduler>();
services.AddSingleton<OpenRouterClient>();
services.AddLogging(b => b.AddConsole());
// 4. Tools registrieren (Host ist der einzige Ort, der Tool-Typen kennt)
var provider = services.BuildServiceProvider();
var registry = provider.GetRequiredService<ToolRegistry>();
registry.Register(new DatabaseTool());
registry.Register(new FileRwTool());
registry.Register(new MailTool());
// 5. Scheduler starten
var scheduler = provider.GetRequiredService<AgentScheduler>();
await scheduler.StartAsync(CancellationToken.None);
// 6. WinForms starten
var mainForm = provider.GetRequiredService<frm_main>();
Application.Run(mainForm);
// 7. Cleanup
await scheduler.StopAsync(CancellationToken.None);
}
```
---
## Core-Erweiterungen für Chat-Support
Der `AgentEngine` im Core benötigt zwei zusätzliche Methoden für
den interaktiven Chat-Modus (ergänze Phase 1.7):
```csharp
// AgentEngine zusätzliche Methoden
// Interaktiver Chat: eine Nutzer-Nachricht → Agent-Antwort
// Unterschied zum autonomen Run: kein Scheduler-Trigger,
// Verlauf wird an bestehende Konversation angehängt
Task<AgentRunResult> ChatAsync(string agentId, string userMessage, CancellationToken ct);
// Chat-Verlauf aus dem StateManager laden
// Rückgabe: Liste von { role, content, timestamp }
Task<IReadOnlyList<ChatEntry>> GetChatHistoryAsync(string agentId);
// Aktuellen Run-Status abrufen (für Statusanzeige in der Sidebar)
AgentRunStatus GetStatus(string agentId);
// Laufenden Run abbrechen
void Abort(string agentId);
```
```csharp
// Neue Typen in Core/Engine/
public sealed record ChatEntry(
string Role, // "user" | "agent" | "tool"
string Content,
DateTime Timestamp
);
public sealed record AgentRunStatus(
string State, // "idle" | "running" | "error"
int StepCount,
int TokensUsed,
string? LastError
);
```
Der StateManager (Phase 1.2) speichert den Chat-Verlauf pro AgentId
persistent in der konfigurierten Datenbank oder als JSON-Datei,
sodass Verläufe auch nach Programm-Neustart verfügbar sind.
---
## Entwicklungsregeln: WinForms-spezifisch
- **Kein UI-Thread-Blocking:** Alle `await`-Aufrufe in Forms immer mit
`ConfigureAwait(false)` oder explizitem `InvokeAsync`. Bridge-Callbacks
kommen auf beliebigen Threads — immer per `InvokeRequired` prüfen.
- **WebView2 ist async:** `EnsureCoreWebView2Async()` muss abgewartet sein
bevor `CoreWebView2`-Eigenschaften gesetzt werden.
- **Keine direkte Form-zu-Form-Kommunikation:** `frm_chat` kommuniziert
ausschließlich über `AgentEngine` und `WebViewBridge`. Kein direkter
Methodenaufruf zwischen Form-Instanzen.
- **frm_chat ist nicht-modal:** Immer `frm.Show(owner)` statt
`frm.ShowDialog()`. Mehrere Instanzen mit demselben AgentId: nur
eine öffnen, fokussieren wenn vorhanden (Dictionary-Check in frm_main).
- **Bridge-Nachrichten immer typisiert:** Kein rohes JSON-String-Bauen
außerhalb von `WebViewBridge` und `BridgeMessage`.
- **EmbeddedUI ist readonly:** Keine dynamischen Schreibzugriffe auf
die extrahierten UI-Dateien zur Laufzeit. UI-Änderungen erfordern
Neu-Kompilierung.
---
## NuGet-Pakete (Host-Projekt)
```xml
<PackageReference Include="Microsoft.Web.WebView2" Version="*" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="*" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="*" />
```
---
## Implementierungsreihenfolge (für Claude Code)
Bearbeite diesen Abschnitt nach Abschluss der Core-Phase (Phasen 12):
1. `EmbeddedUiManager` implementieren und Sicherheitscheck in `Program.cs` einbauen
2. `BridgeMessage` + `BridgeTypes` + `WebViewBridge` implementieren
3. Core: `ChatAsync`, `GetChatHistoryAsync`, `GetStatus`, `Abort` zu `AgentEngine` hinzufügen
4. Core: `ChatEntry` und `AgentRunStatus` Records anlegen
5. `frm_main`: `InitWebViewAsync`, `PushAgentListAsync`, Bridge-Handler implementieren
6. `frm_chat`: vollständig implementieren
7. `EmbeddedUI/bridge.js` erstellen (gemeinsame Bridge-Logik für beide HTML-Seiten)
8. `EmbeddedUI/overview.html` + `overview.css` erstellen (Sidebar + Chat-Bereich)
9. `EmbeddedUI/chat.html` + `chat.css` erstellen (Einzelchat, agentId aus URL-Parameter)
10. `Program.cs` Startup-Reihenfolge implementieren
11. xUnit-Tests: Bridge-Serialisierung, Pfad-Overlap-Check, frm_chat Isolation
**Beginne mit Schritt 1 dieses Abschnitts.**