Zweiter Teil von B4. Die Preise standen fest im Code — eine Tabelle mit einer Handvoll Modelle, die bereits veraltet war. Ausgerechnet das Standardmodell der Agenten fehlte darin, und CalculateCost gab fuer unbekannte Modelle stillschweigend 0 zurueck. Die Kostenanzeige war damit nicht bloss ungenau, sondern blind: Sie meldete 0 Euro, waehrend echte Kosten anfielen. ModelInfo traegt jetzt die Preisangaben aus dem /models-Endpunkt. OpenRouter liefert sie als Text und pro einzelnem Token; die Umrechnung auf eine Million Token laeuft ueber InvariantCulture, sonst wuerde eine deutsche Systemsprache 0.000003 als drei lesen. Halbe Angaben werden verworfen: Ein Modell, bei dem nur der Eingabepreis vorliegt, ergaebe eine plausibel aussehende, aber falsche Summe. ModelPricingCatalog haelt die Preise und liefert eine Kostenschaetzung, die ausweist, ob sie belastbar ist. Der Statusdienst laedt den Katalog beim Start und markiert Laeufe ohne Preisangabe sichtbar — in der Zeile mit einem Warnzeichen, im Tooltip mit den betroffenen Modellnamen und dem Hinweis, dass die Summe unvollstaendig ist. 284 Tests gruen (136 Core, 148 Tools). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1579 lines
58 KiB
C#
1579 lines
58 KiB
C#
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();
|
||
|
||
// 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)
|
||
{
|
||
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";
|
||
}
|