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.Storage;
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;
///
/// Der einzige periodische Treiber (A1). Ersetzt die früheren AgentScheduler/
/// ToolJobScheduler — geplante Läufe und Tool-Job-Polls sind jetzt Tasks.
///
private readonly ClawdDotNet.Core.Tasks.TaskScanner? _taskScanner;
///
/// A2: Freigabe-/Ablehnungs-Dienst. Die Review-Oberfläche (Liste offener Vorschläge,
/// Freigeben/Ablehnen) ruft auf —
/// die Anbindung ist die verbleibende Integration; die Dienst-API steht bereit.
///
private readonly ClawdDotNet.Core.Staging.StagingService? _stagingService;
// Der Lizenz-Teil ist mit der Deploymentcenter-Umstellung aus dieser Fassung
// verschwunden: Startprüfung, laufende Nachprüfung und Notausschalter sitzen jetzt
// in ClawdDotNet.App (LicenseGate, LicenseWatch) und in der Avalonia-Oberfläche.
// Diese Datei ist nur noch Vorlage für die Portierung — siehe ClawdDotNet.slnx.
// ─── Services ───
private LiveLogViewerService? _logViewer;
private OpenRouterStatusService? _statusService;
private JobHistoryService? _jobHistoryService;
private BackupScheduler? _backupScheduler;
// ─── 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;
/// Setzt die Beenden-Rückfrage außer Kraft (Lizenz-Sperre/-Deaktivierung).
private bool _forceClose;
public frm_main(
SettingsManager settingsManager,
InstanceConfig instanceConfig,
string instancePath,
string logDirectory,
ILoggerFactory loggerFactory,
ToolRegistry toolRegistry,
InstanceDirectoryManager dirManager,
AgentEngine? agentEngine,
ClawdDotNet.Core.Tasks.TaskScanner? taskScanner = null,
ClawdDotNet.Core.Staging.StagingService? stagingService = null)
{
_settingsManager = settingsManager;
_instanceConfig = instanceConfig;
_instancePath = instancePath;
_logDirectory = logDirectory;
_loggerFactory = loggerFactory;
_logger = loggerFactory.CreateLogger("ClawdDotNet.Host.MainForm");
_toolRegistry = toolRegistry;
_dirManager = dirManager;
_agentEngine = agentEngine;
_taskScanner = taskScanner;
_stagingService = stagingService;
_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();
SetupBackupTab();
SetupStatusStrip();
SetupBuildInfo();
WireEvents();
FormClosing += OnFormClosing;
_logger.LogInformation("MainForm initialisiert");
}
private void UpdateTitle()
{
Text = $"ClawdDotNet - {_instanceConfig.InstanceName}";
}
// ═══════════════════════════════════════════════════
// BACKUP TAB
// ═══════════════════════════════════════════════════
private void SetupBackupTab()
{
backupPanel.Initialize(
_instancePath,
_instanceConfig.InstanceName,
_settingsManager,
_loggerFactory.CreateLogger("ClawdDotNet.Backup"));
_backupScheduler = new BackupScheduler(
_instancePath,
_instanceConfig.InstanceName,
_settingsManager,
_loggerFactory.CreateLogger("ClawdDotNet.Backup.Scheduler"));
_backupScheduler.OnBackupCreated += _ => BeginInvoke(() => backupPanel.RefreshFromOutside());
_backupScheduler.Start();
}
///
/// 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)
{
_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 = _agentEngine?.IsRunning(agent.AgentId) == true ? "Läuft" : "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 (_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 _agentEngine.RunAsync(
agentConfig, "Manual execution triggered by user.", _instanceConfig.InstanceId, 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();
// Tool-Job-Ticks laufen jetzt über den Scanner (Tasks); ihre Historie ergibt sich
// aus dem Audit-Log/den Receipts (A3), nicht mehr aus einem Scheduler-Event.
EnsureBuiltInServices();
RefreshJobList();
RefreshServiceList();
}
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 (_taskScanner is null)
{
MessageBox.Show("Scanner 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
{
// Poll-Jobs sind jetzt Tasks (Id: tj--); der Scanner führt sie aus.
var ran = await _taskScanner.RunTaskNowAsync(
$"tj-{agent.AgentId}-{jobConfig.JobId}", CancellationToken.None);
_jobLastRunTimes[jobConfig.JobId] = DateTime.Now;
RefreshJobList();
var status = ran ? "Ausgeführt" : "Nicht bereit (läuft evtl. schon oder ist deaktiviert)";
_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 (_agentEngine is null)
{
MessageBox.Show("Agent-Engine 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 _agentEngine.RunAsync(agent, taskMessage, _instanceConfig.InstanceId, 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)
{
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")
: "—";
_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 = "—",
Status = _taskScanner 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 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 = "—",
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();
// Modellpreise einmalig laden. Ohne sie weist die Anzeige alle Kosten als
// unbekannt aus, statt stillschweigend 0 € zu melden.
if (ModelTypeConverter.Client is { } client)
_ = _statusService.LoadPricingAsync(client);
}
// ═══════════════════════════════════════════════════
// CLOSING / PERSISTENZ
// ═══════════════════════════════════════════════════
private void OnFormClosing(object? sender, FormClosingEventArgs e)
{
// Erzwungenes Schließen (Lizenz-Sperre/-Deaktivierung) überspringt die Rückfrage.
if (e.CloseReason == CloseReason.UserClosing && !_forceClose)
{
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();
_backupScheduler?.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)
{
SystemShell.OpenFolder(Path.Combine(_instancePath, "Agents"));
}
private void btn_showInstanceFolder_Click(object sender, EventArgs e)
{
SystemShell.OpenFolder(_instancePath);
}
private void btn_ShowLogFolder2_Click(object sender, EventArgs e)
{
// _logDirectory statt CurrentDirectory + "/Logs": Das Arbeitsverzeichnis ist
// nicht zwingend das Programmverzeichnis, und die Instanz kann ein eigenes
// Log-Ziel haben — geöffnet werden soll der Ordner, in den auch geschrieben wird.
SystemShell.OpenFolder(Path.GetFullPath(_logDirectory));
}
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";
}