Import des bestehenden Projektstands in Git. - .NET 10 WinForms Anwendung (Multi-Agent / Tool-System) - .gitignore fuer Build-Artefakte, Secrets und Runtime-Daten ergaenzt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1571 lines
57 KiB
C#
1571 lines
57 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)
|
||
{
|
||
_statusService?.RecordUsage(model, result.TokensUsed / 2, result.TokensUsed / 2);
|
||
|
||
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";
|
||
}
|