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>
793 lines
26 KiB
Markdown
793 lines
26 KiB
Markdown
# 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 1–2):
|
||
|
||
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.**
|