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

26 KiB
Raw Blame History

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 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)

<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.