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>
26 KiB
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
// 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)
// 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.
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:
<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.
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.
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.
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.
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):
// 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.
// 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
// 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):
// 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);
// 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 mitConfigureAwait(false)oder explizitemInvokeAsync. Bridge-Callbacks kommen auf beliebigen Threads — immer perInvokeRequiredprüfen. -
WebView2 ist async:
EnsureCoreWebView2Async()muss abgewartet sein bevorCoreWebView2-Eigenschaften gesetzt werden. -
Keine direkte Form-zu-Form-Kommunikation:
frm_chatkommuniziert ausschließlich überAgentEngineundWebViewBridge. Kein direkter Methodenaufruf zwischen Form-Instanzen. -
frm_chat ist nicht-modal: Immer
frm.Show(owner)stattfrm.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
WebViewBridgeundBridgeMessage. -
EmbeddedUI ist readonly: Keine dynamischen Schreibzugriffe auf die extrahierten UI-Dateien zur Laufzeit. UI-Änderungen erfordern Neu-Kompilierung.
NuGet-Pakete (Host-Projekt)
<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):
EmbeddedUiManagerimplementieren und Sicherheitscheck inProgram.cseinbauenBridgeMessage+BridgeTypes+WebViewBridgeimplementieren- Core:
ChatAsync,GetChatHistoryAsync,GetStatus,AbortzuAgentEnginehinzufügen - Core:
ChatEntryundAgentRunStatusRecords anlegen frm_main:InitWebViewAsync,PushAgentListAsync, Bridge-Handler implementierenfrm_chat: vollständig implementierenEmbeddedUI/bridge.jserstellen (gemeinsame Bridge-Logik für beide HTML-Seiten)EmbeddedUI/overview.html+overview.csserstellen (Sidebar + Chat-Bereich)EmbeddedUI/chat.html+chat.csserstellen (Einzelchat, agentId aus URL-Parameter)Program.csStartup-Reihenfolge implementieren- xUnit-Tests: Bridge-Serialisierung, Pfad-Overlap-Check, frm_chat Isolation
Beginne mit Schritt 1 dieses Abschnitts.