Files
ClawdDotNet/frm_main.cs
T
RichardandClaude Opus 4.8 69b5704add Prompt-Caching, Tool-Ergebnis-Kappung und Kostenerfassung
T1 — Prompt-Caching. Bisher wurde bei jedem Schritt eines Runs der komplette
Prompt neu berechnet, inklusive Tool-Definitionen und System-Prompt, die sich nie
aendern. Bei zehn Schritten und einem 15k-Praefix sind das 150.000 statt 15.000
Eingabe-Tokens.

ChatMessage bekommt dafuer einen eigenen JsonConverter: Der Inhalt geht weiterhin
als String raus, bei gesetztem CacheBreakpoint jedoch als Blockarray mit
cache_control. Beim Lesen werden beide Formate akzeptiert, damit bestehende
ChatContext.json weiter geladen werden koennen.

PromptCache setzt zwei Breakpoints: einen auf den System-Prompt (deckt
Tool-Definitionen und System-Prompt ab) und einen rollierenden auf die letzte
Nachricht mit Inhalt. Vorherige Markierungen werden vorher entfernt, damit sie
sich nicht ansammeln. Aktivierung ueber promptCaching: auto (Default, aktiv fuer
Modelle mit Unterstuetzung), on oder off.

Der wichtigste Test dazu prueft die Praefix-Stabilitaet: Der System-Prompt muss
ueber alle Schritte zeichengleich serialisiert werden. Ein einziger Zeitstempel
darin wuerde den Cache still verwerfen — die Kosten blieben unveraendert, ohne
dass es irgendwo auffiele.

T9 — Usage liest prompt_tokens_details.cached_tokens; die Zahl wird bis in
AgentRunResult durchgereicht. Ohne sie liesse sich die Wirkung nicht belegen.

T2 — Tool-Ergebnisse werden jetzt zentral in ExecuteToolCallAsync gekappt
(maxToolResultChars, Default 16.000). Bisher konnte ein einzelner WebFetch mit
dem 512-KB-Standardlimit rund 130.000 Tokens in EINER Antwort erzeugen; die
Compaction griff erst danach, bezahlt war der Request laengst.

T3 — Die Zusammenfassung beim Kompaktieren laeuft ueber ein konfigurierbares
summaryModel (Default gemini-2.5-flash) statt ueber das teure Agentenmodell.

B4 — AgentRunResult fuehrt Prompt- und Completion-Tokens getrennt; die
Kostenanzeige schaetzte bisher 50/50, real liegt das Verhaeltnis eher bei 95:5.
Die veraltete Preistabelle bleibt offen.

Neue Einstellungen sind im PropertyGrid sichtbar und werden vom AgentEditor bei
neuen Agenten mitgeschrieben.

Alle 91 Tests gruen.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-27 18:32:17 +02:00

1574 lines
57 KiB
C#
Raw 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.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<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;
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}";
}
/// <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)
{
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<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();
// ─── 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<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();
}
// ═══════════════════════════════════════════════════
// 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";
}