using System.ComponentModel; using System.Diagnostics; using System.Text.Json; using ClawdDotNet.Core.Config; using ClawdDotNet.Core.Engine; using ClawdDotNet.Core.Scheduling; using ClawdDotNet.Core.Tools; using ClawdDotNet.Models; using ClawdDotNet.Services; using ClawdDotNet.UI; using Microsoft.Extensions.Logging; using Microsoft.Web.WebView2.Core; namespace ClawdDotNet; public partial class frm_main : Form { // ─── Abhängigkeiten ─── private readonly SettingsManager _settingsManager; private readonly InstanceConfig _instanceConfig; private readonly string _instancePath; private readonly string _logDirectory; private readonly ILoggerFactory _loggerFactory; private readonly ILogger _logger; private readonly ToolRegistry _toolRegistry; private readonly InstanceDirectoryManager _dirManager; private readonly AgentEngine? _agentEngine; private readonly AgentScheduler? _agentScheduler; private readonly ToolJobScheduler? _toolJobScheduler; // ─── Services ─── private LiveLogViewerService? _logViewer; private OpenRouterStatusService? _statusService; private JobHistoryService? _jobHistoryService; // ─── Chat ─── private WebViewBridge? _bridge; private string? _selectedAgentId; private readonly Dictionary _chatWindows = new(); // ─── ViewModels ─── private InstanceSettingsViewModel _instanceSettingsVm; private readonly BindingList _agentListEntries = new(); private readonly BindingList _jobEntries = new(); private readonly BindingList _serviceEntries = new(); private readonly BindingList _agentToolEntries = new(); private readonly BindingList _jobHistoryEntries = new(); // ─── LastRun-Timestamps (thread-safe via UI thread) ─── private readonly Dictionary _jobLastRunTimes = new(); private bool _suppressAgentSelectionChanged; public frm_main( SettingsManager settingsManager, InstanceConfig instanceConfig, string instancePath, string logDirectory, ILoggerFactory loggerFactory, ToolRegistry toolRegistry, InstanceDirectoryManager dirManager, AgentEngine? agentEngine, AgentScheduler? agentScheduler, ToolJobScheduler? toolJobScheduler = null) { _settingsManager = settingsManager; _instanceConfig = instanceConfig; _instancePath = instancePath; _logDirectory = logDirectory; _loggerFactory = loggerFactory; _logger = loggerFactory.CreateLogger("ClawdDotNet.Host.MainForm"); _toolRegistry = toolRegistry; _dirManager = dirManager; _agentEngine = agentEngine; _agentScheduler = agentScheduler; _toolJobScheduler = toolJobScheduler; _instanceSettingsVm = new InstanceSettingsViewModel(_instanceConfig); InitializeComponent(); SetupForm(); } // Parameterloser Konstruktor für Designer-Kompatibilität public frm_main() { _settingsManager = new SettingsManager(); _instanceConfig = new InstanceConfig(); _instancePath = ""; _logDirectory = "./Logs"; _loggerFactory = LoggerFactory.Create(_ => { }); _logger = _loggerFactory.CreateLogger("ClawdDotNet.Host.MainForm"); _toolRegistry = new ToolRegistry(); _dirManager = new InstanceDirectoryManager("./Instances"); _instanceSettingsVm = new InstanceSettingsViewModel(_instanceConfig); InitializeComponent(); } // ═══════════════════════════════════════════════════ // SETUP // ═══════════════════════════════════════════════════ private void SetupForm() { UpdateTitle(); Shown += OnFormShown; SetupLogTab(); SetupSettingsTab(); SetupAgentSettingsTab(); SetupWorkerTab(); SetupStatusStrip(); SetupBuildInfo(); WireEvents(); FormClosing += OnFormClosing; _logger.LogInformation("MainForm initialisiert"); } private void UpdateTitle() { Text = $"ClawdDotNet - {_instanceConfig.InstanceName}"; } /// /// Verdrahtet alle Designer-Controls mit ihren Event-Handlern. /// Die Controls selbst werden im Designer erstellt. /// private void WireEvents() { // ─── Engine: Live-Chat-Updates (ToolJobs, AgentComm, etc.) ─── if (_agentEngine is not null) { _agentEngine.OnChatEntryAdded += OnEngineChatEntryAdded; _agentEngine.OnRunCompleted += OnEngineRunCompleted; } // ─── Haupt-Toolbar ─── btn_showInstanceManager.Click += OnShowInstanceManagerClick; btn_showInstanceManager_menu.Click += OnShowInstanceManagerClick; // ─── Agent-Toolbar ─── btn_addAgent.Click += OnAddAgentClick; btn_removeAgent.Click += OnRemoveAgentClick; btn_runAgentNow.Click += OnRunAgentNowClick; btn_editIdentity.Click += OnEditIdentityClick; btn_editSoul.Click += OnEditSoulClick; btn_openAgentFolder.Click += OnOpenAgentFolderClick; // ─── Log-Toolbar ─── btn_logfolder.Click += OnBtnLogFolderClick; // ─── Worker-Toolbar (Jobs) ─── btn_addJob.Click += OnAddJobClick; btn_editJob.Click += OnEditJobClick; btn_removeJob.Click += OnRemoveJobClick; btn_runJob.Click += OnRunJobClick; btn_refreshJobs.Click += (_, _) => RefreshJobList(); dgv_Jobs.CellDoubleClick += (_, _) => OnEditJobClick(null, EventArgs.Empty); // ─── Worker-Toolbar (Services) ─── btn_addService.Click += OnAddServiceClick; btn_removeService.Click += OnRemoveServiceClick; btn_startService.Click += OnStartServiceClick; btn_stopService.Click += OnStopServiceClick; } private void OnShowInstanceManagerClick(object? sender, EventArgs e) { var manager = new frm_InstanceManager(_dirManager, _settingsManager); manager.ShowDialog(this); manager.Dispose(); } // ═══════════════════════════════════════════════════ // CHAT TAB (WebView2 + Bridge) // ═══════════════════════════════════════════════════ private void OnFormShown(object? sender, EventArgs e) { Shown -= OnFormShown; BeginInvoke(new Action(async () => await SetupChatTabAsync())); } private async Task SetupChatTabAsync() { try { _logger.LogInformation("WebView2 Initialisierung gestartet..."); await webView_chat.EnsureCoreWebView2Async(); var uiPath = EmbeddedUiManager.GetExtractedPath(); _logger.LogInformation("EmbeddedUI Pfad: {Path}", uiPath); webView_chat.CoreWebView2.SetVirtualHostNameToFolderMapping( "ui.clwd.internal", uiPath, CoreWebView2HostResourceAccessKind.DenyCors); _bridge = new WebViewBridge(webView_chat, _loggerFactory.CreateLogger("ClawdDotNet.UI.Bridge")); _bridge.MessageReceived += OnBridgeMessage; webView_chat.CoreWebView2.NavigationCompleted += async (_, args) => { if (args.IsSuccess) { _logger.LogInformation("WebView2 Navigation abgeschlossen"); await PushAgentListAsync(); } else { _logger.LogWarning("WebView2 Navigation fehlgeschlagen: {Status}", args.WebErrorStatus); } }; webView_chat.CoreWebView2.Navigate("https://ui.clwd.internal/overview.html"); } catch (Exception ex) { _logger.LogError(ex, "WebView2 Initialisierung fehlgeschlagen"); MessageBox.Show( $"WebView2 konnte nicht initialisiert werden:\n{ex.Message}", "Chat-Fehler", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } private async Task PushAgentListAsync() { if (_bridge is null) return; var agents = _instanceConfig.Agents.Select(a => new { agentId = a.AgentId, displayName = a.DisplayName, model = a.Model, status = _agentEngine?.IsRunning(a.AgentId) == true ? "running" : "idle" }).ToArray(); await _bridge.SendAsync(new BridgeMessage( Type: BridgeTypes.AgentListUpdate, Extra: agents)); } private async void OnBridgeMessage(BridgeMessage msg) { if (InvokeRequired) { Invoke(() => OnBridgeMessage(msg)); return; } _logger.LogInformation("OnBridgeMessage: type={Type}, agentId={AgentId}, content={Content}", msg.Type, msg.AgentId, msg.Content); switch (msg.Type) { case BridgeTypes.UserMessage: var targetAgentId = msg.AgentId ?? _selectedAgentId; _logger.LogInformation("UserMessage: targetAgentId={Target}, content={Content}, engineNull={EngineNull}", targetAgentId, msg.Content, _agentEngine is null); if (targetAgentId is null || msg.Content is null) break; _selectedAgentId = targetAgentId; await HandleUserChatMessageAsync(targetAgentId, msg.Content); break; case BridgeTypes.SelectAgent: _selectedAgentId = msg.AgentId; if (msg.AgentId is not null) await LoadChatHistoryAsync(msg.AgentId); break; case BridgeTypes.OpenAgentChat: if (msg.AgentId is not null) OpenChatWindow(msg.AgentId); break; case BridgeTypes.RunNow: if (msg.AgentId is not null) await RunAgentFromChatAsync(msg.AgentId); break; case BridgeTypes.AbortRun: if (msg.AgentId is not null) _agentEngine?.AbortChat(msg.AgentId); break; } } /// /// Wird vom AgentEngine bei jedem neuen ChatEntry ausgelöst (User + Assistant). /// Pusht die Nachricht live an die WebView — funktioniert für alle Quellen: /// User-Chat, ToolJob-Wakeups, AgentComm, AgentSpawn. /// private void OnEngineRunCompleted(string model, AgentRunResult result) { // Echte Aufteilung statt 50/50: In Agenten-Loops liegt das Verhältnis eher bei // 95:5, und da Ausgabe-Tokens ein Vielfaches kosten, war die alte Schätzung // um ein Mehrfaches daneben. _statusService?.RecordUsage(model, result.PromptTokens, result.CompletionTokens); if (IsDisposed || !IsHandleCreated) return; BeginInvoke(() => { // Track agent LastRun timestamp _jobLastRunTimes[$"agent_{result.AgentId}"] = DateTime.Now; // Add to job history var agentName = _instanceConfig.Agents.FirstOrDefault(a => a.AgentId == result.AgentId)?.DisplayName ?? result.AgentId; var historyEntry = new JobHistoryEntry { JobName = "Agent Wakeup", Agent = agentName, Time = DateTime.Now, JobDescription = $"Run ({result.StepCount} Steps, {result.TokensUsed:N0} Tokens)", Info = $"{result.Status} in {result.Duration:mm\\:ss}", Status = result.Status == AgentRunStatus.Completed ? "Success" : "Error" }; _jobHistoryService?.Add(historyEntry); _jobHistoryEntries.Insert(0, historyEntry); while (_jobHistoryEntries.Count > 200) _jobHistoryEntries.RemoveAt(_jobHistoryEntries.Count - 1); _dirManager.AppendTokenUsage(_instancePath, new TokenUsageRecord { AgentId = result.AgentId, AgentName = agentName, Model = model, PromptTokens = result.TokensUsed / 2, CompletionTokens = result.TokensUsed / 2, TotalTokens = result.TokensUsed, Status = result.Status.ToString(), StepCount = result.StepCount, DurationMs = (long)result.Duration.TotalMilliseconds }); }); } private async void OnEngineChatEntryAdded(string agentId, string role, string content, string? source) { if (_bridge is null) return; try { await _bridge.SendAsync(new BridgeMessage( Type: BridgeTypes.ChatMessage, AgentId: agentId, Content: content, Extra: new { role, timestamp = DateTime.Now, source })); } catch (Exception ex) { _logger.LogError(ex, "Fehler beim Senden der Chat-Nachricht an WebView: Agent={AgentId}", agentId); } } private async Task HandleUserChatMessageAsync(string agentId, string text) { if (_agentEngine is null || _bridge is null) return; var agentConfig = _instanceConfig.Agents.FirstOrDefault(a => a.AgentId == agentId); if (agentConfig is null) return; // Typing-Indicator anzeigen während der Agent arbeitet await _bridge.SendAsync(new BridgeMessage( Type: BridgeTypes.ChatTyping, AgentId: agentId)); // ChatAsync im Hintergrund — User- und Assistant-Nachrichten // kommen automatisch über OnChatEntryAdded an die WebView _ = Task.Run(async () => { try { await _agentEngine.ChatAsync( agentConfig, text, _instanceConfig.InstanceId, CancellationToken.None, source: ChatSource.WebView); } catch (Exception ex) { _logger.LogError(ex, "Chat-Fehler für Agent {AgentId}", agentId); await _bridge.SendAsync(new BridgeMessage( Type: BridgeTypes.ChatMessage, AgentId: agentId, Content: $"[Fehler: {ex.Message}]", Extra: new { role = "assistant", timestamp = DateTime.Now })); } }); } private async Task LoadChatHistoryAsync(string agentId) { if (_agentEngine is null || _bridge is null) return; var history = _agentEngine.GetChatHistory(agentId); await _bridge.SendAsync(new BridgeMessage( Type: BridgeTypes.ChatHistory, AgentId: agentId, Extra: history.Select(e => new { role = e.Role, content = e.Content, timestamp = e.Timestamp }).ToArray())); } private async Task RunAgentFromChatAsync(string agentId) { if (_agentEngine is null) return; var agentConfig = _instanceConfig.Agents.FirstOrDefault(a => a.AgentId == agentId); if (agentConfig is null) return; await HandleUserChatMessageAsync(agentId, "Führe deine zugewiesenen Aufgaben aus."); } private void OpenChatWindow(string agentId) { if (_chatWindows.TryGetValue(agentId, out var existing) && !existing.IsDisposed) { existing.BringToFront(); return; } var agentConfig = _instanceConfig.Agents.FirstOrDefault(a => a.AgentId == agentId); if (agentConfig is null) return; var frm = new frm_chat(agentConfig, _agentEngine!, _instanceConfig.InstanceId, _loggerFactory); frm.FormClosed += (_, _) => _chatWindows.Remove(agentId); _chatWindows[agentId] = frm; frm.Show(this); } // ═══════════════════════════════════════════════════ // LOG TAB // ═══════════════════════════════════════════════════ private void SetupLogTab() { rtb_log.ReadOnly = true; rtb_log.BackColor = Color.FromArgb(30, 30, 30); rtb_log.ForeColor = Color.LightGreen; rtb_log.Font = new Font("Consolas", 9f); cb_LogLevel.Items.AddRange(["Alle", "Info", "Warn", "Error"]); cb_LogLevel.SelectedIndex = 0; cb_LogLevel.SelectedIndexChanged += OnLogLevelFilterChanged; RefreshLogModuleList(); cb_LogModule.SelectedIndexChanged += OnLogModuleFilterChanged; var refreshInterval = _settingsManager.AppSettings.LogRefreshIntervalMs; var maxLines = _settingsManager.AppSettings.MaxLogLinesInUi; _logViewer = new LiveLogViewerService(_logDirectory, rtb_log, refreshInterval, maxLines); _logViewer.Start(); } private void RefreshLogModuleList() { cb_LogModule.Items.Clear(); if (_logViewer is not null) { foreach (var module in _logViewer.GetAvailableModules()) cb_LogModule.Items.Add(module); } else { cb_LogModule.Items.Add("Alle"); cb_LogModule.Items.Add("Core"); } cb_LogModule.SelectedIndex = 0; } private void OnLogModuleFilterChanged(object? sender, EventArgs e) { var selected = cb_LogModule.SelectedItem?.ToString() ?? "Alle"; _logViewer?.SetModuleFilter(selected); } private void OnLogLevelFilterChanged(object? sender, EventArgs e) { var selected = cb_LogLevel.SelectedItem?.ToString() ?? "Alle"; _logViewer?.SetLevelFilter(selected); } private void OnBtnLogFolderClick(object? sender, EventArgs e) { var fullPath = Path.GetFullPath(_logDirectory); if (Directory.Exists(fullPath)) { Process.Start(new ProcessStartInfo { FileName = fullPath, UseShellExecute = true }); } else { MessageBox.Show($"Log-Verzeichnis existiert nicht:\n{fullPath}", "Hinweis", MessageBoxButtons.OK, MessageBoxIcon.Information); } } // ═══════════════════════════════════════════════════ // SETTINGS TAB // ═══════════════════════════════════════════════════ private void SetupSettingsTab() { pg_appsettings.SelectedObject = _settingsManager.AppSettings; pg_appsettings.PropertyValueChanged += OnAppSettingsChanged; pg_instancesettings.SelectedObject = _instanceSettingsVm; pg_instancesettings.PropertyValueChanged += OnInstanceSettingsChanged; } private void OnAppSettingsChanged(object? sender, PropertyValueChangedEventArgs e) { _settingsManager.Save(); _logger.LogInformation("App-Settings geändert: {Property}", e.ChangedItem?.Label); } private void OnInstanceSettingsChanged(object? sender, PropertyValueChangedEventArgs e) { SaveInstanceConfig(); UpdateTitle(); _logger.LogInformation("Instance-Settings geändert: {Property}", e.ChangedItem?.Label); } // ═══════════════════════════════════════════════════ // AGENT SETTINGS TAB // ═══════════════════════════════════════════════════ private void SetupAgentSettingsTab() { RefreshAgentList(); dgv_agentlist.DataSource = _agentListEntries; dgv_agentlist.SelectionChanged += OnAgentSelectionChanged; pg_agentsettings.PropertyValueChanged += OnAgentSettingsChanged; dgv_agentTools.DataSource = _agentToolEntries; dgv_agentTools.SelectionChanged += OnToolSelectionChanged; dgv_agentTools.CurrentCellDirtyStateChanged += (_, _) => { if (dgv_agentTools.IsCurrentCellDirty) dgv_agentTools.CommitEdit(DataGridViewDataErrorContexts.Commit); }; dgv_agentTools.CellValueChanged += OnToolAssignedToggle; if (dgv_agentlist.Rows.Count > 0) dgv_agentlist.Rows[0].Selected = true; } private void RefreshAgentList() { _agentListEntries.Clear(); foreach (var agent in _instanceConfig.Agents) { var lastResult = _agentScheduler?.GetLastResult(agent.AgentId); _agentListEntries.Add(new AgentListDisplayEntry { AgentId = agent.AgentId, DisplayName = agent.DisplayName, Model = agent.Model, ToolCount = agent.Tools.Count, HasIdentity = !string.IsNullOrWhiteSpace(agent.Identity) ? "Ja" : "—", HasSoul = !string.IsNullOrWhiteSpace(agent.Soul) ? "Ja" : "—", Status = lastResult?.Status.ToString() ?? "Bereit" }); } } private void OnAgentSelectionChanged(object? sender, EventArgs e) { if (_suppressAgentSelectionChanged) return; if (dgv_agentlist.SelectedRows.Count == 0) { pg_agentsettings.SelectedObject = null; pg_agentToolSettings.SelectedObject = null; _agentToolEntries.Clear(); return; } var index = dgv_agentlist.SelectedRows[0].Index; if (index < 0 || index >= _instanceConfig.Agents.Count) return; var agentConfig = _instanceConfig.Agents[index]; pg_agentsettings.SelectedObject = new AgentSettingsViewModel(agentConfig); RefreshAgentToolList(agentConfig); } private void RefreshAgentListKeepSelection() { var selectedIndex = dgv_agentlist.SelectedRows.Count > 0 ? dgv_agentlist.SelectedRows[0].Index : -1; _suppressAgentSelectionChanged = true; RefreshAgentList(); if (selectedIndex >= 0 && selectedIndex < dgv_agentlist.Rows.Count) dgv_agentlist.Rows[selectedIndex].Selected = true; _suppressAgentSelectionChanged = false; } private void OnAgentSettingsChanged(object? sender, PropertyValueChangedEventArgs e) { SaveInstanceConfig(); RefreshAgentListKeepSelection(); _logger.LogInformation("Agent-Settings geändert: {Property}", e.ChangedItem?.Label); } private void OnAddAgentClick(object? sender, EventArgs e) { var name = Microsoft.VisualBasic.Interaction.InputBox( "Name des neuen Agenten:", "Agent hinzufügen", "NeuerAgent"); if (string.IsNullOrWhiteSpace(name)) return; try { _dirManager.CreateAgent(_instancePath, name); ReloadAgentsFromDisk(); _logger.LogInformation("Agent hinzugefügt: {Name}", name); } catch (Exception ex) { MessageBox.Show($"Fehler: {ex.Message}", "Agent erstellen", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void OnRemoveAgentClick(object? sender, EventArgs e) { if (dgv_agentlist.SelectedRows.Count == 0) return; var index = dgv_agentlist.SelectedRows[0].Index; if (index < 0 || index >= _instanceConfig.Agents.Count) return; var agent = _instanceConfig.Agents[index]; var folderName = InstanceDirectoryManager.BuildAgentFolderName(agent.DisplayName); var result = MessageBox.Show( $"Agent '{agent.DisplayName}' ({agent.AgentId}) wirklich entfernen?\n\n" + "Der gesamte Agent-Ordner wird gelöscht (inkl. Workspace, Logs, Soul, Identity)!", "Agent entfernen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (result != DialogResult.Yes) return; try { _dirManager.RemoveAgent(_instancePath, folderName); ReloadAgentsFromDisk(); _logger.LogInformation("Agent entfernt: {AgentId}", agent.AgentId); } catch (Exception ex) { MessageBox.Show($"Fehler: {ex.Message}", "Agent entfernen", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void OnEditIdentityClick(object? sender, EventArgs e) { var (agent, agentDir) = GetSelectedAgentAndDir(); if (agent is null || agentDir is null) return; var identityPath = Path.Combine(agentDir, "Identity.md"); OpenFileInEditor(identityPath); } private void OnEditSoulClick(object? sender, EventArgs e) { var (agent, agentDir) = GetSelectedAgentAndDir(); if (agent is null || agentDir is null) return; var soulPath = Path.Combine(agentDir, "Soul.md"); OpenFileInEditor(soulPath); } private void OnOpenAgentFolderClick(object? sender, EventArgs e) { var (agent, agentDir) = GetSelectedAgentAndDir(); if (agent is null || agentDir is null) return; if (Directory.Exists(agentDir)) { Process.Start(new ProcessStartInfo { FileName = agentDir, UseShellExecute = true }); } } private (AgentConfig? agent, string? agentDir) GetSelectedAgentAndDir() { if (dgv_agentlist.SelectedRows.Count == 0) return (null, null); var index = dgv_agentlist.SelectedRows[0].Index; if (index < 0 || index >= _instanceConfig.Agents.Count) return (null, null); var agent = _instanceConfig.Agents[index]; return (agent, agent.AgentDir); } private static void OpenFileInEditor(string filePath) { if (!File.Exists(filePath)) { MessageBox.Show($"Datei nicht gefunden:\n{filePath}", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } Process.Start(new ProcessStartInfo { FileName = filePath, UseShellExecute = true }); } private async void OnRunAgentNowClick(object? sender, EventArgs e) { if (_agentScheduler is null || _agentEngine is null) { MessageBox.Show("Kein API-Key konfiguriert – Agenten können nicht ausgeführt werden.", "Hinweis", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } if (dgv_agentlist.SelectedRows.Count == 0) return; var index = dgv_agentlist.SelectedRows[0].Index; if (index < 0 || index >= _instanceConfig.Agents.Count) return; var agentConfig = _instanceConfig.Agents[index]; // Vor dem Run: Identity/Soul von Disk nachladen var freshConfig = _dirManager.LoadAgentConfig(agentConfig.AgentDir); agentConfig.Identity = freshConfig.Identity; agentConfig.Soul = freshConfig.Soul; _logger.LogInformation("Manueller Run gestartet: {AgentId}", agentConfig.AgentId); try { using var cts = new CancellationTokenSource(agentConfig.LoopGuard.Timeout); var result = await _agentScheduler.RunNowAsync( agentConfig, "Manual execution triggered by user.", cts.Token); RefreshAgentList(); MessageBox.Show( $"Agent-Run abgeschlossen:\n" + $"Status: {result.Status}\n" + $"Schritte: {result.StepCount}\n" + $"Tokens: {result.TokensUsed}\n" + $"Dauer: {result.Duration:hh\\:mm\\:ss\\.fff}", "Run Ergebnis", MessageBoxButtons.OK, result.Status == AgentRunStatus.Completed ? MessageBoxIcon.Information : MessageBoxIcon.Warning); } catch (Exception ex) { _logger.LogError(ex, "Manueller Run fehlgeschlagen: {AgentId}", agentConfig.AgentId); MessageBox.Show($"Fehler: {ex.Message}", "Run fehlgeschlagen", MessageBoxButtons.OK, MessageBoxIcon.Error); } } private void ReloadAgentsFromDisk() { var freshConfig = _dirManager.LoadInstanceConfig(_instancePath); _instanceConfig.Agents.Clear(); foreach (var agent in freshConfig.Agents) _instanceConfig.Agents.Add(agent); RefreshAgentList(); } // ═══════════════════════════════════════════════════ // AGENT TOOLS // ═══════════════════════════════════════════════════ private void RefreshAgentToolList(AgentConfig agentConfig) { _agentToolEntries.Clear(); pg_agentToolSettings.SelectedObject = null; foreach (var tool in _toolRegistry.GetAll()) { _agentToolEntries.Add(new AgentToolDisplayEntry { Assigned = agentConfig.Tools.ContainsKey(tool.Name), ToolName = tool.Name, Description = tool.Description }); } } private void OnToolAssignedToggle(object? sender, DataGridViewCellEventArgs e) { if (e.RowIndex < 0 || e.ColumnIndex != dgv_agentTools.Columns["col_ToolAssigned2"]?.Index) return; var agentConfig = GetSelectedAgentConfig(); if (agentConfig is null) return; var entry = _agentToolEntries[e.RowIndex]; var isAssigned = entry.Assigned; if (isAssigned) { if (!agentConfig.Tools.ContainsKey(entry.ToolName)) agentConfig.Tools[entry.ToolName] = new Dictionary(); } else { agentConfig.Tools.Remove(entry.ToolName); pg_agentToolSettings.SelectedObject = null; } SaveInstanceConfig(); RefreshAgentListKeepSelection(); _logger.LogInformation("Tool {Tool} für Agent {Agent}: {State}", entry.ToolName, agentConfig.DisplayName, isAssigned ? "zugewiesen" : "entfernt"); } private void OnToolSelectionChanged(object? sender, EventArgs e) { if (dgv_agentTools.SelectedRows.Count == 0) { pg_agentToolSettings.SelectedObject = null; return; } var agentConfig = GetSelectedAgentConfig(); if (agentConfig is null) return; var toolEntry = _agentToolEntries[dgv_agentTools.SelectedRows[0].Index]; if (!agentConfig.Tools.TryGetValue(toolEntry.ToolName, out var config)) { pg_agentToolSettings.SelectedObject = null; return; } var viewModel = ToolSettingsFactory.CreateViewModel(toolEntry.ToolName, config); pg_agentToolSettings.SelectedObject = viewModel; pg_agentToolSettings.PropertyValueChanged -= OnToolSettingsChanged; pg_agentToolSettings.PropertyValueChanged += OnToolSettingsChanged; } private void OnToolSettingsChanged(object? sender, PropertyValueChangedEventArgs e) { var agentConfig = GetSelectedAgentConfig(); if (agentConfig is null) return; if (dgv_agentTools.SelectedRows.Count == 0) return; var toolEntry = _agentToolEntries[dgv_agentTools.SelectedRows[0].Index]; var newConfig = ToolSettingsFactory.ToConfig(toolEntry.ToolName, pg_agentToolSettings.SelectedObject); if (newConfig is not null) { agentConfig.Tools[toolEntry.ToolName] = newConfig; SaveInstanceConfig(); _logger.LogInformation("Tool-Settings geändert: {Tool}.{Property}", toolEntry.ToolName, e.ChangedItem?.Label); } } private AgentConfig? GetSelectedAgentConfig() { if (dgv_agentlist.SelectedRows.Count == 0) return null; var index = dgv_agentlist.SelectedRows[0].Index; return index >= 0 && index < _instanceConfig.Agents.Count ? _instanceConfig.Agents[index] : null; } // ═══════════════════════════════════════════════════ // WORKER TAB (Jobs & Services) // ═══════════════════════════════════════════════════ private void SetupWorkerTab() { // Columns are now defined in Designer — just bind data sources dgv_Jobs.DataSource = _jobEntries; dgv_services.DataSource = _serviceEntries; // ─── Job History ─── _jobHistoryService = new JobHistoryService(_instancePath); dgv_jobhistory.DataSource = _jobHistoryEntries; RefreshJobHistoryGrid(); // ─── ToolJobScheduler: OnJobTick → History + LastRun ─── if (_toolJobScheduler is not null) { _toolJobScheduler.OnJobTick += OnToolJobTick; } EnsureBuiltInServices(); RefreshJobList(); RefreshServiceList(); } private void OnToolJobTick(string agentId, string jobId, ToolJobResult result) { if (IsDisposed || !IsHandleCreated) return; BeginInvoke(() => { // Track LastRun timestamp _jobLastRunTimes[jobId] = DateTime.Now; // Add to history var agent = _instanceConfig.Agents.FirstOrDefault(a => a.AgentId == agentId); var jobConfig = agent?.ToolJobs.FirstOrDefault(j => j.JobId == jobId); var historyEntry = new JobHistoryEntry { JobName = jobConfig?.JobTypeId ?? jobId, Agent = agent?.DisplayName ?? agentId, Time = DateTime.Now, JobDescription = $"Tool Job: {jobConfig?.ToolName ?? "?"}", Info = result.LogSummary ?? (result.ShouldWakeAgent ? "Wake Agent" : "No Action"), Status = result.ShouldWakeAgent ? "Success" : (result.LogSummary?.Contains("Fehler") == true ? "Error" : "Success") }; _jobHistoryService?.Add(historyEntry); _jobHistoryEntries.Insert(0, historyEntry); // Trim UI list while (_jobHistoryEntries.Count > 200) _jobHistoryEntries.RemoveAt(_jobHistoryEntries.Count - 1); // Refresh job list to update LastRun column RefreshJobList(); }); } private void RefreshJobHistoryGrid() { _jobHistoryEntries.Clear(); if (_jobHistoryService is null) return; foreach (var entry in _jobHistoryService.GetAll().Take(200)) _jobHistoryEntries.Add(entry); } private void EnsureBuiltInServices() { var defaults = BuiltInServices.CreateDefaults(); var changed = false; foreach (var def in defaults) { if (_instanceConfig.Services.Any(s => s.ServiceId == def.ServiceId)) continue; _instanceConfig.Services.Insert(0, def); changed = true; } if (changed) SaveInstanceConfig(); } private void OnAddJobClick(object? sender, EventArgs e) { using var dlg = new frm_AddJob(_instanceConfig.Agents, _toolRegistry); if (dlg.ShowDialog(this) != DialogResult.OK) return; var agent = _instanceConfig.Agents.FirstOrDefault(a => a.AgentId == dlg.SelectedAgentId); if (agent is null) return; if (dlg.IsToolJob) { agent.ToolJobs.Add(new Core.Config.ToolJobConfig { ToolName = dlg.SelectedToolName, JobTypeId = dlg.SelectedJobTypeId, Cron = dlg.CronExpression, RunOnStart = dlg.RunOnStart, Enabled = true }); _logger.LogInformation("Tool-Job hinzugefügt: Agent={Agent}, Tool={Tool}, Cron={Cron}", agent.DisplayName, dlg.SelectedToolName, dlg.CronExpression); } else { agent.Scheduler = new Core.Config.SchedulerConfig { Cron = dlg.CronExpression, RunOnStart = dlg.RunOnStart, TaskMessage = dlg.TaskMessage }; _logger.LogInformation("Agent-Wakeup-Job hinzugefügt: Agent={Agent}, Cron={Cron}", agent.DisplayName, dlg.CronExpression); } SaveInstanceConfig(); RefreshJobList(); } private void OnEditJobClick(object? sender, EventArgs e) { if (dgv_Jobs.CurrentRow?.DataBoundItem is not JobDisplayEntry entry) return; var agent = _instanceConfig.Agents.FirstOrDefault(a => a.AgentId == entry.AgentId); if (agent is null) return; var isToolJob = entry.JobType == "Tool Job"; using var dlg = new frm_AddJob( _instanceConfig.Agents, _toolRegistry, agentId: entry.AgentId, cron: entry.CronExpression, taskMessage: entry.TaskMessage, runOnStart: entry.RunOnStart, isToolJob: isToolJob, toolName: isToolJob ? entry.ToolName : null, jobTypeId: isToolJob ? entry.TaskMessage : null); // TaskMessage enthält bei ToolJobs die JobTypeId if (dlg.ShowDialog(this) != DialogResult.OK) return; if (isToolJob) { var toolJob = agent.ToolJobs.FirstOrDefault(j => j.JobId == entry.JobId); if (toolJob is null) return; toolJob.Cron = dlg.CronExpression; toolJob.RunOnStart = dlg.RunOnStart; _logger.LogInformation("Tool-Job bearbeitet: Agent={Agent}, Job={JobId}, Cron={Cron}", agent.DisplayName, toolJob.JobId, dlg.CronExpression); } else { if (agent.Scheduler is null) return; agent.Scheduler.Cron = dlg.CronExpression; agent.Scheduler.TaskMessage = dlg.TaskMessage; agent.Scheduler.RunOnStart = dlg.RunOnStart; _logger.LogInformation("Agent-Wakeup-Job bearbeitet: Agent={Agent}, Cron={Cron}", agent.DisplayName, dlg.CronExpression); } SaveInstanceConfig(); RefreshJobList(); } private void OnRemoveJobClick(object? sender, EventArgs e) { if (dgv_Jobs.CurrentRow?.DataBoundItem is not JobDisplayEntry entry) return; var agent = _instanceConfig.Agents.FirstOrDefault(a => a.AgentId == entry.AgentId); if (agent is null) return; var label = entry.JobType == "Tool Job" ? $"Tool-Job '{entry.ToolName}' für Agent '{agent.DisplayName}'" : $"Agent-Wakeup-Job für '{agent.DisplayName}'"; var result = MessageBox.Show( $"{label} wirklich entfernen?", "Job entfernen", MessageBoxButtons.YesNo, MessageBoxIcon.Question); if (result != DialogResult.Yes) return; if (entry.JobType == "Tool Job") { agent.ToolJobs.RemoveAll(j => j.JobId == entry.JobId); } else { agent.Scheduler = null; } SaveInstanceConfig(); RefreshJobList(); } private async void OnRunJobClick(object? sender, EventArgs e) { if (dgv_Jobs.CurrentRow?.DataBoundItem is not JobDisplayEntry entry) return; var agent = _instanceConfig.Agents.FirstOrDefault(a => a.AgentId == entry.AgentId); if (agent is null) return; if (entry.JobType == "Tool Job") { if (_toolJobScheduler is null) { MessageBox.Show("Tool-Job-Scheduler ist nicht aktiv.", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } var jobConfig = agent.ToolJobs.FirstOrDefault(j => j.JobId == entry.JobId); if (jobConfig is null) return; btn_runJob.Enabled = false; btn_runJob.Text = "⏳ Läuft..."; try { var result = await _toolJobScheduler.TriggerJobAsync(agent, jobConfig, CancellationToken.None); _jobLastRunTimes[jobConfig.JobId] = DateTime.Now; RefreshJobList(); var status = result.ShouldWakeAgent ? "Wake → Agent gestartet" : (result.LogSummary ?? "Keine Aktion"); _jobHistoryService?.Add(new JobHistoryEntry { JobName = jobConfig.JobTypeId, Agent = agent.DisplayName, Time = DateTime.Now, JobDescription = $"Tool Job: {jobConfig.ToolName}", Info = status, Status = "Manual" }); RefreshJobHistoryGrid(); MessageBox.Show( $"Job '{jobConfig.JobTypeId}' abgeschlossen.\n\nErgebnis: {status}", "Job ausgeführt", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { _jobHistoryService?.Add(new JobHistoryEntry { JobName = jobConfig.JobTypeId, Agent = agent.DisplayName, Time = DateTime.Now, JobDescription = $"Tool Job: {jobConfig.ToolName}", Info = ex.Message, Status = "Error" }); RefreshJobHistoryGrid(); MessageBox.Show($"Fehler beim Ausführen:\n{ex.Message}", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { btn_runJob.Text = "▶ Jetzt ausführen"; btn_runJob.Enabled = true; } } else { // Agent Wakeup Job if (_agentScheduler is null || _agentEngine is null) { MessageBox.Show("Agent-Scheduler ist nicht aktiv.", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Warning); return; } var taskMessage = agent.Scheduler?.TaskMessage ?? "Führe deine zugewiesenen Aufgaben aus."; btn_runJob.Enabled = false; btn_runJob.Text = "⏳ Läuft..."; try { var result = await _agentScheduler.RunNowAsync(agent, taskMessage, CancellationToken.None); _jobLastRunTimes[$"agent_{agent.AgentId}"] = DateTime.Now; RefreshJobList(); _jobHistoryService?.Add(new JobHistoryEntry { JobName = "Agent Wakeup", Agent = agent.DisplayName, Time = DateTime.Now, JobDescription = taskMessage, Info = $"{result.Status} ({result.StepCount} Steps, {result.TokensUsed:N0} Tokens)", Status = "Manual" }); RefreshJobHistoryGrid(); MessageBox.Show( $"Agent '{agent.DisplayName}' abgeschlossen.\n\nStatus: {result.Status}\nTokens: {result.TokensUsed:N0}", "Job ausgeführt", MessageBoxButtons.OK, MessageBoxIcon.Information); } catch (Exception ex) { _jobHistoryService?.Add(new JobHistoryEntry { JobName = "Agent Wakeup", Agent = agent.DisplayName, Time = DateTime.Now, JobDescription = taskMessage, Info = ex.Message, Status = "Error" }); RefreshJobHistoryGrid(); MessageBox.Show($"Fehler beim Ausführen:\n{ex.Message}", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Error); } finally { btn_runJob.Text = "▶ Jetzt ausführen"; btn_runJob.Enabled = true; } } } private void RefreshJobList() { _jobEntries.Clear(); foreach (var agent in _instanceConfig.Agents) { // Agent Wakeup Jobs if (agent.Scheduler is not null) { var lastResult = _agentScheduler?.GetLastResult(agent.AgentId); string nextRun = "—"; if (!string.IsNullOrWhiteSpace(agent.Scheduler.Cron)) { try { var cron = CronExpression.Parse(agent.Scheduler.Cron); var next = cron.GetNextOccurrence(DateTime.Now); nextRun = next?.ToString("yyyy-MM-dd HH:mm") ?? "—"; } catch { nextRun = "Ungültig"; } } var agentLastRun = _jobLastRunTimes.TryGetValue($"agent_{agent.AgentId}", out var agentLastTime) ? agentLastTime.ToString("yyyy-MM-dd HH:mm:ss") : (lastResult is not null ? "Letzter Lauf bekannt" : "—"); _jobEntries.Add(new JobDisplayEntry { JobType = "Agent Wakeup", AgentId = agent.AgentId, AgentName = agent.DisplayName, CronExpression = agent.Scheduler.Cron, TaskMessage = agent.Scheduler.TaskMessage, RunOnStart = agent.Scheduler.RunOnStart, NextRun = nextRun, LastRun = agentLastRun, LastStatus = lastResult?.Status.ToString() ?? "—", Status = _agentScheduler is not null ? "Aktiv" : "Inaktiv" }); } // Tool Jobs foreach (var toolJob in agent.ToolJobs) { string nextRun = "—"; if (!string.IsNullOrWhiteSpace(toolJob.Cron)) { try { var cron = CronExpression.Parse(toolJob.Cron); var next = cron.GetNextOccurrence(DateTime.Now); nextRun = next?.ToString("yyyy-MM-dd HH:mm") ?? "—"; } catch { nextRun = "Ungültig"; } } var lastToolResult = _toolJobScheduler?.GetLastResult(toolJob.JobId); var toolLastRun = _jobLastRunTimes.TryGetValue(toolJob.JobId, out var lastTime) ? lastTime.ToString("yyyy-MM-dd HH:mm:ss") : "—"; _jobEntries.Add(new JobDisplayEntry { JobType = "Tool Job", JobId = toolJob.JobId, AgentId = agent.AgentId, AgentName = agent.DisplayName, ToolName = toolJob.ToolName, CronExpression = toolJob.Cron, TaskMessage = toolJob.JobTypeId, RunOnStart = toolJob.RunOnStart, NextRun = nextRun, LastRun = toolLastRun, LastStatus = lastToolResult?.LogSummary ?? "—", Status = toolJob.Enabled ? "Aktiv" : "Deaktiviert" }); } } } private void RefreshServiceList() { _serviceEntries.Clear(); foreach (var svc in _instanceConfig.Services) { _serviceEntries.Add(new ServiceDisplayEntry { ServiceId = svc.ServiceId, Name = svc.Name, Type = svc.Type, Port = svc.Port, Status = svc.Enabled ? "Bereit" : "Deaktiviert", Description = svc.Description }); } } private void OnAddServiceClick(object? sender, EventArgs e) { using var dlg = new frm_AddService(); if (dlg.ShowDialog(this) != DialogResult.OK) return; var svcConfig = new ServiceConfig { Name = dlg.ServiceName, Type = dlg.ServiceType, Port = dlg.ServicePort, Enabled = true, Description = dlg.ServiceDescription }; _instanceConfig.Services.Add(svcConfig); SaveInstanceConfig(); RefreshServiceList(); _logger.LogInformation("Service hinzugefügt: {Name} ({Type}, Port {Port})", svcConfig.Name, svcConfig.Type, svcConfig.Port); } private void OnRemoveServiceClick(object? sender, EventArgs e) { if (dgv_services.SelectedRows.Count == 0) return; var index = dgv_services.SelectedRows[0].Index; if (index < 0 || index >= _instanceConfig.Services.Count) return; var svc = _instanceConfig.Services[index]; if (svc.BuiltIn) { MessageBox.Show("Eingebaute Services können nicht entfernt werden.", "Hinweis", MessageBoxButtons.OK, MessageBoxIcon.Information); return; } var result = MessageBox.Show( $"Service '{svc.Name}' ({svc.Type}, Port {svc.Port}) wirklich entfernen?", "Service entfernen", MessageBoxButtons.YesNo, MessageBoxIcon.Warning); if (result != DialogResult.Yes) return; _instanceConfig.Services.RemoveAt(index); SaveInstanceConfig(); RefreshServiceList(); _logger.LogInformation("Service entfernt: {ServiceId}", svc.ServiceId); } private void OnStartServiceClick(object? sender, EventArgs e) { if (dgv_services.SelectedRows.Count == 0) return; var index = dgv_services.SelectedRows[0].Index; if (index < 0 || index >= _serviceEntries.Count) return; var entry = _serviceEntries[index]; entry.Status = "Läuft"; entry.StartedAt = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); _serviceEntries.ResetItem(index); _logger.LogInformation("Service gestartet: {Name}", entry.Name); } private void OnStopServiceClick(object? sender, EventArgs e) { if (dgv_services.SelectedRows.Count == 0) return; var index = dgv_services.SelectedRows[0].Index; if (index < 0 || index >= _serviceEntries.Count) return; var entry = _serviceEntries[index]; entry.Status = "Gestoppt"; entry.StartedAt = "—"; _serviceEntries.ResetItem(index); _logger.LogInformation("Service gestoppt: {Name}", entry.Name); } // ═══════════════════════════════════════════════════ // BUILD INFO // ═══════════════════════════════════════════════════ private void SetupBuildInfo() { var sb = new System.Text.StringBuilder(); foreach (var asm in AppDomain.CurrentDomain.GetAssemblies() .Where(a => a.GetName().Name?.StartsWith("ClawdDotNet") == true) .OrderBy(a => a.GetName().Name)) { var name = asm.GetName().Name!; var shortName = name.Replace("ClawdDotNet.", "").Replace("Tools.", ""); var buildDate = asm.GetCustomAttributes(typeof(System.Reflection.AssemblyMetadataAttribute), false) .OfType() .FirstOrDefault(a => a.Key == "BuildDate")?.Value ?? "?"; var buildInfoType = asm.GetType(asm.GetName().Name + ".BuildInfo"); var buildNum = buildInfoType?.GetField("Build")?.GetValue(null) as int? ?? 0; var changes = buildInfoType?.GetField("Changes")?.GetValue(null) as string ?? ""; sb.AppendLine($"{shortName} #B{buildNum} ({buildDate}) – {changes}"); } label_Softwareinfo.Text = sb.ToString().TrimEnd(); label_Softwareinfo.AutoSize = true; } // ═══════════════════════════════════════════════════ // STATUS STRIP // ═══════════════════════════════════════════════════ private void SetupStatusStrip() { if (string.IsNullOrWhiteSpace(_instanceConfig.OpenRouterApiKey)) { label_openRouterStatus.Text = "| Kein API-Key konfiguriert"; label_openRouterStatus.ForeColor = Color.Orange; label_openRouterCredits.Text = ""; label_openRouterCreditBalance.Text = ""; return; } var checkInterval = _settingsManager.AppSettings.StatusCheckIntervalSeconds; _statusService = new OpenRouterStatusService( _instanceConfig.OpenRouterApiKey, _settingsManager.AppSettings.OpenRouterBaseUrl, checkInterval); label_openRouterCreditBalance.Text = "Lade..."; _statusService.OnStatusUpdated += () => { if (IsDisposed || !IsHandleCreated) return; BeginInvoke(() => { label_openRouterStatus.Text = _statusService.StatusText; label_openRouterStatus.ForeColor = _statusService.IsApiReachable ? Color.FromArgb(0, 128, 0) : Color.Red; label_openRouterCredits.Text = _statusService.CreditsText; label_openRouterCredits.ToolTipText = _statusService.CreditsTooltip; // Credit Balance anzeigen if (_statusService.CreditRemaining.HasValue) label_openRouterCreditBalance.Text = $"Balance: ${_statusService.CreditRemaining.Value:F4}"; else label_openRouterCreditBalance.Text = "Balance: ---"; }); }; _statusService.Start(); } // ═══════════════════════════════════════════════════ // CLOSING / PERSISTENZ // ═══════════════════════════════════════════════════ private void OnFormClosing(object? sender, FormClosingEventArgs e) { if (e.CloseReason == CloseReason.UserClosing) { var result = MessageBox.Show( "ClawdDotNet ist für den 24/7-Betrieb ausgelegt.\n\n" + "Möchten Sie die Anwendung wirklich beenden?\n" + "Alle laufenden Agenten werden gestoppt.", $"ClawdDotNet – {_instanceConfig.InstanceName} beenden?", MessageBoxButtons.YesNo, MessageBoxIcon.Question, MessageBoxDefaultButton.Button2); if (result != DialogResult.Yes) { e.Cancel = true; return; } } _bridge?.Dispose(); _logViewer?.Dispose(); _statusService?.Dispose(); _logger.LogInformation("MainForm wird geschlossen"); } private void SaveInstanceConfig() { if (string.IsNullOrWhiteSpace(_instancePath)) return; try { _dirManager.SaveInstanceConfig(_instancePath, _instanceConfig); foreach (var agent in _instanceConfig.Agents) { if (!string.IsNullOrWhiteSpace(agent.AgentDir) && Directory.Exists(agent.AgentDir)) _dirManager.SaveAgentSettings(agent.AgentDir, agent); } _logger.LogDebug("Instance-Config gespeichert: {Path}", _instancePath); } catch (Exception ex) { _logger.LogError(ex, "Fehler beim Speichern der Instance-Config"); MessageBox.Show($"Config konnte nicht gespeichert werden:\n{ex.Message}", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Warning); } } private void btn_openAgentFolder_Click(object sender, EventArgs e) { Process.Start("explorer.exe", _instancePath + "\\Agents"); } private void btn_showInstanceFolder_Click(object sender, EventArgs e) { Process.Start("explorer.exe", _instancePath); } private void btn_ShowLogFolder2_Click(object sender, EventArgs e) { Process.Start("explorer.exe", Environment.CurrentDirectory + "/Logs"); } private void dgv_Jobs_CellContentClick(object sender, DataGridViewCellEventArgs e) { } } // ═══════════════════════════════════════════════════ // Hilfsklasse für die Agent-DataGridView // ═══════════════════════════════════════════════════ public sealed class AgentListDisplayEntry { public string AgentId { get; set; } = ""; public string DisplayName { get; set; } = ""; public string Model { get; set; } = ""; public int ToolCount { get; set; } public string HasIdentity { get; set; } = "—"; public string HasSoul { get; set; } = "—"; public string Status { get; set; } = "Bereit"; }