Fruehjahrsputz: WinForms-Altlast entfernt, Dokumentation nachgezogen
Die Avalonia-Portierung ist abgeschlossen, damit ist die in ClawdDotNet.slnx angekuendigte Aufgabe "WinForms-Oberflaeche entfernen" faellig. Der Stand davor liegt unter dem Tag vor-fruehjahrsputz-2026-08. Entfernt (56 Dateien, seit dem Herausloesen der Anwendungsschicht nicht mehr Teil des Builds): ClawdDotNet.csproj, Program.cs, sieben frm_*-Formulare, UI/, Models/, EmbeddedUI/, Properties/, Resources/, Services/, das alte Anwendungssymbol und Deploy-Build.ps1 (ersetzt durch deploy/publish.py). Dazu configs/*.json - Beispielkonfigurationen aus der Zeit vor dem Instanzverzeichnis, auf die nur noch die alten Prompts verwiesen. Die vier Entwicklungs-Prompts der Anfangszeit ziehen nach docs/archiv/ um, mit README, das ihren Stand einordnet. Eine Regel darin gilt weiter - die Pflichtfelder fetchedAt/dataAsOf/source der Internet-Tools -, deshalb Archiv statt Loeschen; der WebSearch-Plan verweist auf den neuen Pfad. Toter Code - PlaceholderPageViewModel samt Ansicht: Es gibt keinen Platzhalter-Bereich mehr, seit alle neun Seiten portiert sind. - Snappier als direkter Paketverweis: MongoDB.Driver loest es ohnehin auf dieselbe Fassung auf, der Verweis hob nichts an. Zwei Fehler, die dabei sichtbar wurden - Die taegliche Sicherung lief ins Leere. Die Oberflaeche bot sie an und schrieb Uhrzeit, Zielordner und Anzahl in die Einstellungen, aber der BackupScheduler wurde nirgends erzeugt. Jetzt am AppHost verdrahtet und in den geordneten Abbau aufgenommen. - SettingsPageViewModel hielt die vier Sicherungs-Einstellungen doppelt. Aus der Ansicht waren sie laengst verschwunden, gelesen und beim Speichern zurueckgeschrieben wurden sie weiter: Wer die Uhrzeit auf der Sicherungs-Seite aenderte und danach die Einstellungen speicherte, bekam den alten Wert zurueck. Pakete: keine bekannten Sicherheitsluecken mehr - SQLitePCLRaw.bundle_e_sqlite3 auf 2.1.13 angehoben. Microsoft.Data.Sqlite bringt 2.1.11 mit, darin steckt GHSA-2m69-gcr7-jv3q (NU1903, hoch). - SharpCompress bleibt als direkter Verweis stehen. Beim Aufraeumen erst als ungenutzt entfernt - dabei kam die von MongoDB.Driver gezogene Fassung 0.30.1 mit GHSA-6c8g-7p36-r338 zurueck. Der Verweis ist eine Anhebung, kein Ballast; das steht jetzt als Kommentar dabei. Dokumentation - Roadmap mit Statusblock: A1 und A3 erledigt, A2 nur zur Haelfte - Gate, Policy und Dienst greifen, aber keine Ansicht ruft ApproveAsync auf, ein gestagter Aufruf liegt unbeantwortet. Das ist jetzt Punkt 1 der Reihung. Rocket.Chat steht und kollidiert mit A5 (Matrix) - Entscheidung faellig. - Avalonia-Portierungsleitfaden -> Oberflaechen-Leitfaden: kein Auftrag mehr, sondern Beschreibung des Stands. - Bestandsaufnahme und Linux-Analyse als datierte Befunde gekennzeichnet; der teure Teil der Linux-Analyse (8.900 Zeilen WinForms) ist hinfaellig. - Verweise auf frm_*, WebView2 und ClawdDotNet.csproj in den lebenden Dokumenten richtiggestellt. Build fehlerfrei, 585 Tests gruen (6 uebersprungen). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,792 @@
|
||||
# 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.**
|
||||
Reference in New Issue
Block a user