UI-Entwurf umgesetzt, Rocket.Chat-Tool, Deploymentcenter 2.4
Sicherungspunkt vor dem Aufraeumen. Buendelt die Arbeit, die seit dem Abschluss der Avalonia-Portierung im Arbeitsverzeichnis lag. Oberflaeche - Entwurf aus Mockup/ umgesetzt: Theme.axaml (Farben je Thema, Barlow als mitgelieferte Schrift), Icons.axaml (Symbolgeometrien), Shell.axaml (eigene ControlThemes statt Fluent umzufaerben). - Neue Steuerelemente StrokeIcon und BlueprintFrame, Seiten fuer Token-Verbrauch und Agenten-Chats, Werkzeug-Einstellungen als Seite statt eigenem Fenster, Texteditor-Fenster. - ThemeManager mit hellem und dunklem Thema; die beiden Pinsel-Konverter entfallen, weil ein fester Farbwert den Themenwechsel nicht ueberlebt. Rocket.Chat - Neues Tool-Projekt (Client, Konfiguration, Workspace-Dateien) nach der Bauform des Telegram-Tools: rocketchat_poll als Tool-Job, geweckt wird nur, wenn wirklich etwas anliegt. - send_file ist freigabepflichtig, send_message bewusst nicht: Der Raum ist Arbeitsraum, der Schutz sitzt an der Raum-Allowlist. - Konzept-Doc um die Messung gegen die echte Instanz 8.7 ergaenzt; drei Annahmen waren falsch und sind korrigiert. Deploymentcenter - DC6 (Update anwenden) und DC7 (Erstinstallation ueber setup.json) erledigt, DC3 fuer win-x64/dev; deploy/publish.py als Release-Strecke. - AppHost.DisposeAsync gegen doppeltes Herunterfahren gesperrt - sonst ueberschreibt eine zweite Abmeldung den Wartungszustand am Watchdog. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using ClawdDotNet.App;
|
||||
using ClawdDotNet.Core.Config;
|
||||
using ClawdDotNet.Core.Storage;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using Microsoft.Extensions.Logging;
|
||||
@@ -11,12 +12,20 @@ public sealed partial class ToolItemViewModel : ObservableObject
|
||||
{
|
||||
public string Name { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Kurzbeschreibung aus der Werkzeug-Registrierung. Leer, wenn das Werkzeug in dieser
|
||||
/// Instanz gar nicht geladen ist — die Liste der bekannten Namen ist länger als die
|
||||
/// der tatsächlich vorhandenen Werkzeuge.
|
||||
/// </summary>
|
||||
public string Description { get; }
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isEnabled;
|
||||
|
||||
public ToolItemViewModel(string name, bool isEnabled)
|
||||
public ToolItemViewModel(string name, bool isEnabled, string description = "")
|
||||
{
|
||||
Name = name;
|
||||
Description = description;
|
||||
_isEnabled = isEnabled;
|
||||
}
|
||||
}
|
||||
@@ -75,7 +84,7 @@ public sealed partial class AgentItemViewModel : ObservableObject
|
||||
"SocialMediaManager", "AgentSpawn", "AgentEditor", "Memory", "Taskboard"
|
||||
];
|
||||
|
||||
public AgentItemViewModel(AgentConfig config)
|
||||
public AgentItemViewModel(AgentConfig config, IReadOnlyDictionary<string, string>? descriptions = null)
|
||||
{
|
||||
Config = config;
|
||||
_displayName = config.DisplayName;
|
||||
@@ -95,7 +104,9 @@ public sealed partial class AgentItemViewModel : ObservableObject
|
||||
foreach (var tool in KnownTools)
|
||||
{
|
||||
var isEnabled = config.Tools.ContainsKey(tool);
|
||||
var item = new ToolItemViewModel(tool, isEnabled);
|
||||
var description = descriptions?.GetValueOrDefault(tool) ?? "";
|
||||
|
||||
var item = new ToolItemViewModel(tool, isEnabled, description);
|
||||
item.PropertyChanged += (_, e) =>
|
||||
{
|
||||
if (e.PropertyName == nameof(ToolItemViewModel.IsEnabled))
|
||||
@@ -128,8 +139,21 @@ public sealed partial class AgentItemViewModel : ObservableObject
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Welches Feld der Texteditor gerade bearbeitet.</summary>
|
||||
public enum AgentTextField
|
||||
{
|
||||
Identity,
|
||||
Soul
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Ansichtsmodell für die Agenten-Verwaltungsseite.
|
||||
///
|
||||
/// <para>Der Entwurf ordnet die Seite wieder wie die WinForms-Vorlage in vier Felder an:
|
||||
/// oben Agentenliste und Agenteneinstellungen, unten Werkzeugliste und
|
||||
/// Werkzeugeinstellungen. Für dieses Modell heißt das vor allem: Die Werkzeugeinstellungen
|
||||
/// sind kein eigenes Fenster mehr, sondern hängen als <see cref="ToolSettings"/> an der
|
||||
/// Auswahl in der Werkzeugliste.</para>
|
||||
/// </summary>
|
||||
public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
{
|
||||
@@ -141,9 +165,27 @@ public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
[ObservableProperty]
|
||||
private AgentItemViewModel? _selectedAgent;
|
||||
|
||||
/// <summary>
|
||||
/// Das ausgewählte Werkzeug des ausgewählten Agenten. Steuert, welche Felder unten
|
||||
/// rechts stehen.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private ToolItemViewModel? _selectedTool;
|
||||
|
||||
/// <summary>
|
||||
/// Die Felder zum ausgewählten Werkzeug. Wird bei jedem Wechsel neu gebaut — das
|
||||
/// Ansichtsmodell trägt die Werte von einem guten Dutzend Werkzeuge, und eines davon
|
||||
/// mit den Werten eines anderen weiterzuverwenden ginge irgendwann schief.
|
||||
/// </summary>
|
||||
[ObservableProperty]
|
||||
private ToolSettingsViewModel? _toolSettings;
|
||||
|
||||
[ObservableProperty]
|
||||
private string _statusText = "Bereit";
|
||||
|
||||
[ObservableProperty]
|
||||
private bool _isRunning;
|
||||
|
||||
public List<string> KnownModels { get; } =
|
||||
[
|
||||
"anthropic/claude-3-5-sonnet",
|
||||
@@ -158,7 +200,12 @@ public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
public List<string> CachingOptions { get; } = ["auto", "on", "off"];
|
||||
|
||||
public event Func<AddAgentViewModel, Task<AddAgentResultData?>>? RequestAddAgentDialog;
|
||||
public event Func<ToolSettingsViewModel, Task<Dictionary<string, object?>?>>? RequestToolSettingsDialog;
|
||||
|
||||
/// <summary>
|
||||
/// Öffnet den Editor für Identity bzw. Soul. Gibt der Aufrufer einen Text zurück,
|
||||
/// wurde gespeichert; <c>null</c> heißt Abbruch.
|
||||
/// </summary>
|
||||
public event Func<string, string, Task<string?>>? RequestTextEditorDialog;
|
||||
|
||||
public AgentsPageViewModel() : this(null) { }
|
||||
|
||||
@@ -177,20 +224,55 @@ public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Beschreibungen der tatsächlich geladenen Werkzeuge. Die Namensliste in
|
||||
/// <see cref="AgentItemViewModel.KnownTools"/> ist fest verdrahtet und kennt auch
|
||||
/// Werkzeuge, die in dieser Instanz nicht registriert sind — für die bleibt die
|
||||
/// Beschreibung eben leer.
|
||||
/// </summary>
|
||||
private IReadOnlyDictionary<string, string> ToolDescriptions()
|
||||
=> _host?.Tools.GetAll()
|
||||
.GroupBy(t => t.Name, StringComparer.OrdinalIgnoreCase)
|
||||
.ToDictionary(g => g.Key, g => g.First().Description, StringComparer.OrdinalIgnoreCase)
|
||||
?? new Dictionary<string, string>();
|
||||
|
||||
[RelayCommand]
|
||||
private void ReloadAgents()
|
||||
{
|
||||
Agents.Clear();
|
||||
if (_host is null) return;
|
||||
|
||||
var descriptions = ToolDescriptions();
|
||||
|
||||
foreach (var agent in _host.Instance.Agents)
|
||||
{
|
||||
Agents.Add(new AgentItemViewModel(agent));
|
||||
}
|
||||
Agents.Add(new AgentItemViewModel(agent, descriptions));
|
||||
|
||||
SelectedAgent = Agents.FirstOrDefault();
|
||||
}
|
||||
|
||||
partial void OnSelectedAgentChanged(AgentItemViewModel? value)
|
||||
{
|
||||
// Die Werkzeugliste unten gehört zum gewählten Agenten — bei einem Wechsel darf
|
||||
// dort nicht die Auswahl des vorigen stehen bleiben.
|
||||
SelectedTool = value?.AvailableTools.FirstOrDefault();
|
||||
}
|
||||
|
||||
partial void OnSelectedToolChanged(ToolItemViewModel? value)
|
||||
{
|
||||
if (SelectedAgent is null || value is null)
|
||||
{
|
||||
ToolSettings = null;
|
||||
return;
|
||||
}
|
||||
|
||||
// Auch für ein noch nicht zugelassenes Werkzeug sollen die Felder sichtbar sein —
|
||||
// sonst müsste man erst ankreuzen, um zu sehen, was einzustellen wäre.
|
||||
if (!SelectedAgent.Config.Tools.TryGetValue(value.Name, out var config) || config is null)
|
||||
config = new Dictionary<string, object?>();
|
||||
|
||||
ToolSettings = new ToolSettingsViewModel(value.Name, config);
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task AddAgentAsync()
|
||||
{
|
||||
@@ -217,7 +299,7 @@ public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
_host.Instance.Agents.Add(newConfig);
|
||||
SaveAgents();
|
||||
|
||||
var item = new AgentItemViewModel(newConfig);
|
||||
var item = new AgentItemViewModel(newConfig, ToolDescriptions());
|
||||
Agents.Add(item);
|
||||
SelectedAgent = item;
|
||||
|
||||
@@ -239,28 +321,118 @@ public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
StatusText = $"Agent '{agentId}' entfernt.";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Weckt den gewählten Agenten von Hand — im Entwurf die Schaltfläche „Jetzt ausf.".
|
||||
/// Nimmt dieselbe Nachricht wie der geplante Lauf, damit der Handlauf zeigt, was der
|
||||
/// geplante täte.
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private async Task ConfigureToolAsync(ToolItemViewModel? toolItem)
|
||||
private async Task RunAgentNowAsync()
|
||||
{
|
||||
if (SelectedAgent is null || toolItem is null || RequestToolSettingsDialog is null) return;
|
||||
if (_host is null || SelectedAgent is null) return;
|
||||
|
||||
var toolName = toolItem.Name;
|
||||
if (!SelectedAgent.Config.Tools.TryGetValue(toolName, out var config))
|
||||
if (_host.Engine is null)
|
||||
{
|
||||
config = new Dictionary<string, object?>();
|
||||
SelectedAgent.Config.Tools[toolName] = config;
|
||||
toolItem.IsEnabled = true;
|
||||
StatusText = "Agent-Engine ist nicht aktiv (kein API-Key).";
|
||||
return;
|
||||
}
|
||||
|
||||
var vm = new ToolSettingsViewModel(toolName, config);
|
||||
var newConfig = await RequestToolSettingsDialog(vm);
|
||||
var config = SelectedAgent.Config;
|
||||
var message = config.Scheduler?.TaskMessage ?? "Führe deine zugewiesenen Aufgaben aus.";
|
||||
|
||||
if (newConfig is not null)
|
||||
IsRunning = true;
|
||||
StatusText = $"Führe '{config.DisplayName}' aus…";
|
||||
|
||||
try
|
||||
{
|
||||
SelectedAgent.Config.Tools[toolName] = newConfig;
|
||||
SaveAgents();
|
||||
StatusText = $"Einstellungen für Werkzeug '{toolName}' gespeichert.";
|
||||
var result = await _host.Engine.RunAsync(
|
||||
config, message, _host.Instance.InstanceId, CancellationToken.None);
|
||||
|
||||
StatusText = $"'{config.DisplayName}' abgeschlossen: {result.Status} "
|
||||
+ $"({result.StepCount} Schritte, {result.TokensUsed:N0} Tokens).";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusText = $"Lauf fehlgeschlagen: {ex.Message}";
|
||||
_logger?.LogError(ex, "Handlauf des Agenten fehlgeschlagen");
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsRunning = false;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private Task EditIdentityAsync() => EditTextFieldAsync(AgentTextField.Identity);
|
||||
|
||||
[RelayCommand]
|
||||
private Task EditSoulAsync() => EditTextFieldAsync(AgentTextField.Soul);
|
||||
|
||||
/// <summary>
|
||||
/// Identity und Soul sind lange Texte. Der Entwurf gibt ihnen ein eigenes kleines
|
||||
/// Fenster statt eines Feldes in der Spalte — in einem 60 Pixel hohen Kasten lässt
|
||||
/// sich eine Rollenbeschreibung nicht schreiben.
|
||||
/// </summary>
|
||||
private async Task EditTextFieldAsync(AgentTextField field)
|
||||
{
|
||||
if (SelectedAgent is null || RequestTextEditorDialog is null) return;
|
||||
|
||||
var isIdentity = field == AgentTextField.Identity;
|
||||
var title = isIdentity
|
||||
? $"Identity bearbeiten — {SelectedAgent.DisplayName}"
|
||||
: $"Soul bearbeiten — {SelectedAgent.DisplayName}";
|
||||
|
||||
var current = isIdentity ? SelectedAgent.Identity : SelectedAgent.Soul;
|
||||
|
||||
var edited = await RequestTextEditorDialog(title, current);
|
||||
if (edited is null) return;
|
||||
|
||||
if (isIdentity) SelectedAgent.Identity = edited;
|
||||
else SelectedAgent.Soul = edited;
|
||||
|
||||
SaveAgents();
|
||||
}
|
||||
|
||||
/// <summary>Öffnet den Arbeitsordner des gewählten Agenten.</summary>
|
||||
[RelayCommand]
|
||||
private void OpenAgentFolder()
|
||||
{
|
||||
if (_host is null || SelectedAgent is null) return;
|
||||
|
||||
var path = _host.Directories.GetAgentPath(_host.InstancePath, SelectedAgent.DisplayName);
|
||||
|
||||
if (!Directory.Exists(path))
|
||||
{
|
||||
StatusText = $"Ordner nicht gefunden: {path}";
|
||||
return;
|
||||
}
|
||||
|
||||
SystemShell.OpenFolder(path);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Übernimmt die Felder des gewählten Werkzeugs in die Agentenkonfiguration.
|
||||
///
|
||||
/// <para>Das Ankreuzfeld in der Liste und diese Schaltfläche sind zwei verschiedene
|
||||
/// Dinge: Ersteres lässt das Werkzeug zu, letztere schreibt seine Einstellungen. Wer
|
||||
/// nur ankreuzt, bekommt ein Werkzeug mit Vorgabewerten — das ist gewollt.</para>
|
||||
/// </summary>
|
||||
[RelayCommand]
|
||||
private void SaveToolSettings()
|
||||
{
|
||||
if (SelectedAgent is null || SelectedTool is null || ToolSettings is null) return;
|
||||
|
||||
ToolSettings.ConfirmCommand.Execute(null);
|
||||
|
||||
if (ToolSettings.Result is not { } config) return;
|
||||
|
||||
SelectedAgent.Config.Tools[SelectedTool.Name] = config;
|
||||
|
||||
// Einstellungen zu schreiben heißt, das Werkzeug zu wollen.
|
||||
SelectedTool.IsEnabled = true;
|
||||
|
||||
SaveAgents();
|
||||
StatusText = $"Einstellungen für '{SelectedTool.Name}' gespeichert.";
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
@@ -269,12 +441,46 @@ public sealed partial class AgentsPageViewModel : PageViewModel
|
||||
if (_host is null) return;
|
||||
|
||||
foreach (var agentVm in Agents)
|
||||
{
|
||||
agentVm.ApplyChanges();
|
||||
}
|
||||
|
||||
_host.Directories.SaveInstanceConfig(_host.InstancePath, _host.Instance);
|
||||
_logger?.LogInformation("Agenten-Konfiguration gespeichert.");
|
||||
StatusText = "Agenten-Konfiguration gespeichert.";
|
||||
}
|
||||
|
||||
// ─── Teilerstellungen ───
|
||||
//
|
||||
// Die drei Teiler der Seite sollen den Neustart ueberleben. Gespeichert wird ein
|
||||
// Verhaeltnis, kein Pixelwert: Sonst sitzt der Teiler nach einem Wechsel auf einen
|
||||
// anderen Bildschirm an einer voellig anderen Stelle.
|
||||
|
||||
public double SplitVertical => Clamp(_host?.Settings.AppSettings.AgentsSplitVertical ?? 0.55);
|
||||
public double SplitTopList => Clamp(_host?.Settings.AppSettings.AgentsSplitTopList ?? 0.44);
|
||||
public double SplitBottomList => Clamp(_host?.Settings.AppSettings.AgentsSplitBottomList ?? 0.44);
|
||||
|
||||
/// <summary>
|
||||
/// Hält den Teiler in einem Bereich, in dem beide Seiten noch bedienbar sind. Ein
|
||||
/// gespeicherter Wert von 0.02 hätte eine Spalte auf einen Strich zusammengezogen und
|
||||
/// wäre ohne Zurücksetzen nicht mehr aufzuziehen gewesen.
|
||||
/// </summary>
|
||||
private static double Clamp(double ratio)
|
||||
=> double.IsFinite(ratio) ? Math.Clamp(ratio, 0.2, 0.8) : 0.5;
|
||||
|
||||
public void SaveSplits(double vertical, double topList, double bottomList)
|
||||
{
|
||||
if (_host is null) return;
|
||||
|
||||
try
|
||||
{
|
||||
var settings = _host.Settings.AppSettings;
|
||||
settings.AgentsSplitVertical = Clamp(vertical);
|
||||
settings.AgentsSplitTopList = Clamp(topList);
|
||||
settings.AgentsSplitBottomList = Clamp(bottomList);
|
||||
_host.Settings.Save();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Eine Teilerstellung ist es nicht wert, die Bedienung zu unterbrechen.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user