Files
ClawdDotNet/frm_main.cs
T

1586 lines
58 KiB
C#
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
/// <summary>
/// Der einzige periodische Treiber (A1). Ersetzt die früheren AgentScheduler/
/// ToolJobScheduler — geplante Läufe und Tool-Job-Polls sind jetzt Tasks.
/// </summary>
private readonly ClawdDotNet.Core.Tasks.TaskScanner? _taskScanner;
/// <summary>
/// A2: Freigabe-/Ablehnungs-Dienst. Die Review-Oberfläche (Liste offener Vorschläge,
/// Freigeben/Ablehnen) ruft <see cref="ClawdDotNet.Core.Staging.StagingService"/> auf —
/// die Anbindung ist die verbleibende Integration; die Dienst-API steht bereit.
/// </summary>
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<string, frm_chat> _chatWindows = new();
// ─── ViewModels ───
private InstanceSettingsViewModel _instanceSettingsVm;
private readonly BindingList<AgentListDisplayEntry> _agentListEntries = new();
private readonly BindingList<JobDisplayEntry> _jobEntries = new();
private readonly BindingList<ServiceDisplayEntry> _serviceEntries = new();
private readonly BindingList<AgentToolDisplayEntry> _agentToolEntries = new();
private readonly BindingList<JobHistoryEntry> _jobHistoryEntries = new();
// ─── LastRun-Timestamps (thread-safe via UI thread) ───
private readonly Dictionary<string, DateTime> _jobLastRunTimes = new();
private bool _suppressAgentSelectionChanged;
/// <summary>Setzt die Beenden-Rückfrage außer Kraft (Lizenz-Sperre/-Deaktivierung).</summary>
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();
}
/// <summary>
/// Verdrahtet alle Designer-Controls mit ihren Event-Handlern.
/// Die Controls selbst werden im Designer erstellt.
/// </summary>
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;
}
}
/// <summary>
/// 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.
/// </summary>
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<string, object?>();
}
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-<agent>-<job>); 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<System.Reflection.AssemblyMetadataAttribute>()
.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";
}