feat(ui): complete Avalonia UI port with 7 main pages, tool settings & top MenuBar

This commit is contained in:
Richard
2026-08-10 10:48:34 +02:00
parent a0e18d2a57
commit b5bf97ae74
187 changed files with 20054 additions and 882 deletions
+53 -71
View File
@@ -4,6 +4,7 @@ using System.Text.Json;
using ClawdDotNet.Core.Config;
using ClawdDotNet.Core.Engine;
using ClawdDotNet.Core.Scheduling;
using ClawdDotNet.Core.Storage;
using ClawdDotNet.Core.Tools;
using ClawdDotNet.Models;
using ClawdDotNet.Services;
@@ -25,8 +26,24 @@ public partial class frm_main : Form
private readonly ToolRegistry _toolRegistry;
private readonly InstanceDirectoryManager _dirManager;
private readonly AgentEngine? _agentEngine;
private readonly AgentScheduler? _agentScheduler;
private readonly ToolJobScheduler? _toolJobScheduler;
/// <summary>
/// Der einzige periodische Treiber (A1). Ersetzt die früheren AgentScheduler/
/// ToolJobScheduler — geplante Läufe und Tool-Job-Polls sind jetzt Tasks.
/// </summary>
private readonly ClawdDotNet.Core.Tasks.TaskScanner? _taskScanner;
/// <summary>
/// A2: Freigabe-/Ablehnungs-Dienst. Die Review-Oberfläche (Liste offener Vorschläge,
/// Freigeben/Ablehnen) ruft <see cref="ClawdDotNet.Core.Staging.StagingService"/> auf —
/// die Anbindung ist die verbleibende Integration; die Dienst-API steht bereit.
/// </summary>
private readonly ClawdDotNet.Core.Staging.StagingService? _stagingService;
// Der Lizenz-Teil ist mit der Deploymentcenter-Umstellung aus dieser Fassung
// verschwunden: Startprüfung, laufende Nachprüfung und Notausschalter sitzen jetzt
// in ClawdDotNet.App (LicenseGate, LicenseWatch) und in der Avalonia-Oberfläche.
// Diese Datei ist nur noch Vorlage für die Portierung — siehe ClawdDotNet.slnx.
// ─── Services ───
private LiveLogViewerService? _logViewer;
@@ -51,6 +68,9 @@ public partial class frm_main : Form
private readonly Dictionary<string, DateTime> _jobLastRunTimes = new();
private bool _suppressAgentSelectionChanged;
/// <summary>Setzt die Beenden-Rückfrage außer Kraft (Lizenz-Sperre/-Deaktivierung).</summary>
private bool _forceClose;
public frm_main(
SettingsManager settingsManager,
InstanceConfig instanceConfig,
@@ -60,8 +80,8 @@ public partial class frm_main : Form
ToolRegistry toolRegistry,
InstanceDirectoryManager dirManager,
AgentEngine? agentEngine,
AgentScheduler? agentScheduler,
ToolJobScheduler? toolJobScheduler = null)
ClawdDotNet.Core.Tasks.TaskScanner? taskScanner = null,
ClawdDotNet.Core.Staging.StagingService? stagingService = null)
{
_settingsManager = settingsManager;
_instanceConfig = instanceConfig;
@@ -72,8 +92,8 @@ public partial class frm_main : Form
_toolRegistry = toolRegistry;
_dirManager = dirManager;
_agentEngine = agentEngine;
_agentScheduler = agentScheduler;
_toolJobScheduler = toolJobScheduler;
_taskScanner = taskScanner;
_stagingService = stagingService;
_instanceSettingsVm = new InstanceSettingsViewModel(_instanceConfig);
InitializeComponent();
@@ -580,8 +600,6 @@ public partial class frm_main : Form
foreach (var agent in _instanceConfig.Agents)
{
var lastResult = _agentScheduler?.GetLastResult(agent.AgentId);
_agentListEntries.Add(new AgentListDisplayEntry
{
AgentId = agent.AgentId,
@@ -590,7 +608,7 @@ public partial class frm_main : Form
ToolCount = agent.Tools.Count,
HasIdentity = !string.IsNullOrWhiteSpace(agent.Identity) ? "Ja" : "—",
HasSoul = !string.IsNullOrWhiteSpace(agent.Soul) ? "Ja" : "—",
Status = lastResult?.Status.ToString() ?? "Bereit"
Status = _agentEngine?.IsRunning(agent.AgentId) == true ? "Läuft" : "Bereit"
});
}
}
@@ -752,7 +770,7 @@ public partial class frm_main : Form
private async void OnRunAgentNowClick(object? sender, EventArgs e)
{
if (_agentScheduler is null || _agentEngine is null)
if (_agentEngine is null)
{
MessageBox.Show("Kein API-Key konfiguriert Agenten können nicht ausgeführt werden.",
"Hinweis", MessageBoxButtons.OK, MessageBoxIcon.Warning);
@@ -778,8 +796,8 @@ public partial class frm_main : Form
try
{
using var cts = new CancellationTokenSource(agentConfig.LoopGuard.Timeout);
var result = await _agentScheduler.RunNowAsync(
agentConfig, "Manual execution triggered by user.", cts.Token);
var result = await _agentEngine.RunAsync(
agentConfig, "Manual execution triggered by user.", _instanceConfig.InstanceId, cts.Token);
RefreshAgentList();
@@ -925,52 +943,14 @@ public partial class frm_main : Form
dgv_jobhistory.DataSource = _jobHistoryEntries;
RefreshJobHistoryGrid();
// ─── ToolJobScheduler: OnJobTick → History + LastRun ───
if (_toolJobScheduler is not null)
{
_toolJobScheduler.OnJobTick += OnToolJobTick;
}
// Tool-Job-Ticks laufen jetzt über den Scanner (Tasks); ihre Historie ergibt sich
// aus dem Audit-Log/den Receipts (A3), nicht mehr aus einem Scheduler-Event.
EnsureBuiltInServices();
RefreshJobList();
RefreshServiceList();
}
private void 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();
@@ -1123,9 +1103,9 @@ public partial class frm_main : Form
if (entry.JobType == "Tool Job")
{
if (_toolJobScheduler is null)
if (_taskScanner is null)
{
MessageBox.Show("Tool-Job-Scheduler ist nicht aktiv.", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Warning);
MessageBox.Show("Scanner ist nicht aktiv.", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
@@ -1136,11 +1116,13 @@ public partial class frm_main : Form
btn_runJob.Text = "⏳ Läuft...";
try
{
var result = await _toolJobScheduler.TriggerJobAsync(agent, jobConfig, CancellationToken.None);
// Poll-Jobs sind jetzt Tasks (Id: tj-<agent>-<job>); der Scanner führt sie aus.
var ran = await _taskScanner.RunTaskNowAsync(
$"tj-{agent.AgentId}-{jobConfig.JobId}", CancellationToken.None);
_jobLastRunTimes[jobConfig.JobId] = DateTime.Now;
RefreshJobList();
var status = result.ShouldWakeAgent ? "Wake → Agent gestartet" : (result.LogSummary ?? "Keine Aktion");
var status = ran ? "Ausgeführt" : "Nicht bereit (läuft evtl. schon oder ist deaktiviert)";
_jobHistoryService?.Add(new JobHistoryEntry
{
@@ -1181,9 +1163,9 @@ public partial class frm_main : Form
else
{
// Agent Wakeup Job
if (_agentScheduler is null || _agentEngine is null)
if (_agentEngine is null)
{
MessageBox.Show("Agent-Scheduler ist nicht aktiv.", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Warning);
MessageBox.Show("Agent-Engine ist nicht aktiv.", "Fehler", MessageBoxButtons.OK, MessageBoxIcon.Warning);
return;
}
@@ -1193,7 +1175,7 @@ public partial class frm_main : Form
btn_runJob.Text = "⏳ Läuft...";
try
{
var result = await _agentScheduler.RunNowAsync(agent, taskMessage, CancellationToken.None);
var result = await _agentEngine.RunAsync(agent, taskMessage, _instanceConfig.InstanceId, CancellationToken.None);
_jobLastRunTimes[$"agent_{agent.AgentId}"] = DateTime.Now;
RefreshJobList();
@@ -1244,8 +1226,6 @@ public partial class frm_main : Form
// Agent Wakeup Jobs
if (agent.Scheduler is not null)
{
var lastResult = _agentScheduler?.GetLastResult(agent.AgentId);
string nextRun = "—";
if (!string.IsNullOrWhiteSpace(agent.Scheduler.Cron))
{
@@ -1263,7 +1243,7 @@ public partial class frm_main : Form
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
{
@@ -1275,8 +1255,8 @@ public partial class frm_main : Form
RunOnStart = agent.Scheduler.RunOnStart,
NextRun = nextRun,
LastRun = agentLastRun,
LastStatus = lastResult?.Status.ToString() ?? "—",
Status = _agentScheduler is not null ? "Aktiv" : "Inaktiv"
LastStatus = "—",
Status = _taskScanner is not null ? "Aktiv" : "Inaktiv"
});
}
@@ -1298,8 +1278,6 @@ public partial class frm_main : Form
}
}
var lastToolResult = _toolJobScheduler?.GetLastResult(toolJob.JobId);
var toolLastRun = _jobLastRunTimes.TryGetValue(toolJob.JobId, out var lastTime)
? lastTime.ToString("yyyy-MM-dd HH:mm:ss")
: "—";
@@ -1316,7 +1294,7 @@ public partial class frm_main : Form
RunOnStart = toolJob.RunOnStart,
NextRun = nextRun,
LastRun = toolLastRun,
LastStatus = lastToolResult?.LogSummary ?? "—",
LastStatus = "—",
Status = toolJob.Enabled ? "Aktiv" : "Deaktiviert"
});
}
@@ -1516,7 +1494,8 @@ public partial class frm_main : Form
private void OnFormClosing(object? sender, FormClosingEventArgs e)
{
if (e.CloseReason == CloseReason.UserClosing)
// Erzwungenes Schließen (Lizenz-Sperre/-Deaktivierung) überspringt die Rückfrage.
if (e.CloseReason == CloseReason.UserClosing && !_forceClose)
{
var result = MessageBox.Show(
"ClawdDotNet ist für den 24/7-Betrieb ausgelegt.\n\n" +
@@ -1568,17 +1547,20 @@ public partial class frm_main : Form
private void btn_openAgentFolder_Click(object sender, EventArgs e)
{
Process.Start("explorer.exe", _instancePath + "\\Agents");
SystemShell.OpenFolder(Path.Combine(_instancePath, "Agents"));
}
private void btn_showInstanceFolder_Click(object sender, EventArgs e)
{
Process.Start("explorer.exe", _instancePath);
SystemShell.OpenFolder(_instancePath);
}
private void btn_ShowLogFolder2_Click(object sender, EventArgs e)
{
Process.Start("explorer.exe", Environment.CurrentDirectory + "/Logs");
// _logDirectory statt CurrentDirectory + "/Logs": Das Arbeitsverzeichnis ist
// nicht zwingend das Programmverzeichnis, und die Instanz kann ein eigenes
// Log-Ziel haben — geöffnet werden soll der Ordner, in den auch geschrieben wird.
SystemShell.OpenFolder(Path.GetFullPath(_logDirectory));
}
private void dgv_Jobs_CellContentClick(object sender, DataGridViewCellEventArgs e)