Umbau durch Gemini Flash, Durchsicht und Nacharbeit durch Claude. Umfang: - Desktop: LocalizationManager (JSON-Pakete, Rueckfall auf Englisch, Umschalten zur Laufzeit), Localize-Markup-Erweiterung, Sprachwahl in den Einstellungen (AppSettings.Language, Vorgabe "en"); 317 Schluessel je Sprache - WebUI: alle Ansichten auf t()/data-i18n, 656 Schluessel je Sprache - Agenten-Werkzeuge: Beschreibungen, Schemata und Meldungen auf Englisch - Tests an die englischen Meldungen angepasst, keine Pruefung abgeschwaecht Nacharbeit bei der Durchsicht: - ~25 Kommentarbloecke mit Begruendungen waren ersatzlos entfallen, darunter sicherheitsrelevante (NTFS-Datenstrom beim shared:-Praefix, Bremse gegen Agentenschleifen, doppelter POST beim Anlegen einer Instanz). Wieder eingesetzt: auf Englisch, wo die Datei umgestellt ist, sonst im Wortlaut. - Drei WebUI-Schluessel fehlten (logs.paused, instances.instanceOfflineError, instances.noInstanceActiveError). Weil t() bei fehlendem Schluessel den Schluessel selbst liefert, griff der "|| err.message"-Rueckfall nie — die Oberflaeche haette den Schluesselnamen angezeigt. Geprueft: Schluesselgleichheit en/de, Platzhalter je Schluessel, jeder im Code benutzte Schluessel vorhanden, node --check fuer alle Module, 1.220 Tests gruen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
191 lines
7.4 KiB
C#
191 lines
7.4 KiB
C#
using System.Collections.ObjectModel;
|
|
using ClawdDotNet.App.Models;
|
|
using ClawdDotNet.App.Services;
|
|
using CommunityToolkit.Mvvm.ComponentModel;
|
|
using CommunityToolkit.Mvvm.Input;
|
|
using ClawdDotNet.Core.Instances;
|
|
|
|
namespace ClawdDotNet.Desktop.ViewModels;
|
|
|
|
/// <summary>
|
|
/// Eine Instanz in der Auswahl.
|
|
///
|
|
/// <para>Das Monogramm und der Punkt stammen aus dem Entwurf. Der Punkt zeigt bewusst
|
|
/// <em>Bereitschaft</em> und nicht „läuft": Ob eine andere Instanz gerade läuft, weiß
|
|
/// diese Anwendung nicht — jede Instanz ist ein eigener Prozess, und einen
|
|
/// instanzübergreifenden Status gibt es noch nicht. Ein grüner Punkt für „läuft" wäre
|
|
/// hier schlicht geraten.</para>
|
|
/// </summary>
|
|
public sealed class InstanceChoiceViewModel(InstanceInfo info, bool isCurrent)
|
|
{
|
|
public InstanceInfo Info { get; } = info;
|
|
|
|
public string InstanceName => Info.InstanceName;
|
|
public string FolderPath => Info.FolderPath;
|
|
|
|
/// <summary>Erste zwei Buchstaben, gross — wie im Entwurf.</summary>
|
|
public string Monogram => Info.InstanceName.Length >= 2
|
|
? Info.InstanceName[..2].ToUpperInvariant()
|
|
: Info.InstanceName.ToUpperInvariant();
|
|
|
|
public string Summary
|
|
{
|
|
get
|
|
{
|
|
var keyStatus = IsReady
|
|
? LocalizationManager.Instance["desktop.instancePicker.apiKeyConfigured"]
|
|
: LocalizationManager.Instance["desktop.instancePicker.apiKeyMissing"];
|
|
return string.Format(
|
|
LocalizationManager.Instance["desktop.instancePicker.agentsCount"],
|
|
Info.AgentCount,
|
|
keyStatus);
|
|
}
|
|
}
|
|
|
|
/// <summary>Ohne API-Key kann die Instanz keine Agenten starten.</summary>
|
|
public bool IsReady => Info.ApiKeyStatus.Equals("Konfiguriert", StringComparison.OrdinalIgnoreCase)
|
|
|| Info.ApiKeyStatus.Equals("Configured", StringComparison.OrdinalIgnoreCase);
|
|
|
|
/// <summary>Die gerade laufende Instanz — traegt im Entwurf das Etikett „Aktiv".</summary>
|
|
public bool IsCurrent { get; } = isCurrent;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Auswahl der Instanz — beim Start und beim Wechseln aus der laufenden Anwendung.
|
|
///
|
|
/// <para>Die WinForms-Fassung mischte Datenzugriff, Spaltenformatierung und Meldungsfenster
|
|
/// in einem Formular. Hier steht nur, <em>was</em> geschieht; das <em>wie</em> es aussieht
|
|
/// liegt in der Ansicht, und Rückfragen laufen über <see cref="ConfirmError"/> statt über
|
|
/// einen direkten Dialogaufruf — so bleibt das Ansichtsmodell ohne Fensterbezug prüfbar.</para>
|
|
/// </summary>
|
|
public sealed partial class InstancePickerViewModel : ViewModelBase
|
|
{
|
|
private readonly InstanceDirectoryManager _directories;
|
|
private readonly TaskCompletionSource<string?> _completion = new();
|
|
private readonly string? _currentInstancePath;
|
|
|
|
[ObservableProperty]
|
|
[NotifyCanExecuteChangedFor(nameof(StartCommand))]
|
|
private InstanceChoiceViewModel? _selected;
|
|
|
|
[ObservableProperty]
|
|
private string _newInstanceName = "";
|
|
|
|
/// <summary>Fehlertext für die Ansicht; leer heißt: alles in Ordnung.</summary>
|
|
[ObservableProperty]
|
|
private string _errorText = "";
|
|
|
|
public ObservableCollection<InstanceChoiceViewModel> Instances { get; } = [];
|
|
|
|
/// <summary>Der gewählte Pfad, oder <c>null</c> bei Abbruch.</summary>
|
|
public Task<string?> Result => _completion.Task;
|
|
|
|
public string InstancesDirectory => _directories.InstancesDirectory;
|
|
|
|
/// <summary>
|
|
/// Beim Start heisst die Schaltflaeche „Starten", aus der laufenden Anwendung heraus
|
|
/// „Wechseln" — und dann steht der Hinweis dabei, dass dafuer neu gestartet wird.
|
|
/// </summary>
|
|
public bool IsSwitchMode => _currentInstancePath is not null;
|
|
|
|
public string ConfirmLabel => IsSwitchMode
|
|
? LocalizationManager.Instance["desktop.instancePicker.confirmSwitch"]
|
|
: LocalizationManager.Instance["desktop.instancePicker.confirmStart"];
|
|
|
|
/// <summary>Ueberschrift des Dialogs — nicht aus ConfirmLabel zusammengesetzt,
|
|
/// sonst stuende dort „Instanz Starten".</summary>
|
|
public string Title => IsSwitchMode
|
|
? LocalizationManager.Instance["desktop.instancePicker.titleSwitch"]
|
|
: LocalizationManager.Instance["desktop.instancePicker.titleSelect"];
|
|
|
|
public string Hint => IsSwitchMode
|
|
? LocalizationManager.Instance["desktop.instancePicker.hintSwitch"]
|
|
: LocalizationManager.Instance["desktop.instancePicker.hintSelect"];
|
|
|
|
/// <param name="currentInstancePath">
|
|
/// Pfad der laufenden Instanz, oder <c>null</c> beim Start. Steuert Beschriftung und
|
|
/// das Etikett „Aktiv" in der Liste.
|
|
/// </param>
|
|
public InstancePickerViewModel(InstanceDirectoryManager directories, string? currentInstancePath = null)
|
|
{
|
|
_directories = directories;
|
|
_currentInstancePath = currentInstancePath;
|
|
Refresh();
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void Refresh()
|
|
{
|
|
Instances.Clear();
|
|
|
|
foreach (var instance in _directories.ListInstances())
|
|
{
|
|
var isCurrent = _currentInstancePath is not null
|
|
&& string.Equals(instance.FolderPath, _currentInstancePath,
|
|
StringComparison.OrdinalIgnoreCase);
|
|
|
|
Instances.Add(new InstanceChoiceViewModel(instance, isCurrent));
|
|
}
|
|
|
|
// Eine einzelne Instanz gleich vorwählen — der häufigste Fall, und ein Klick
|
|
// weniger bei jedem Start.
|
|
Selected ??= Instances.FirstOrDefault(i => i.IsCurrent) ?? Instances.FirstOrDefault();
|
|
}
|
|
|
|
private bool CanStart => Selected is not null;
|
|
|
|
[RelayCommand(CanExecute = nameof(CanStart))]
|
|
private void Start() => _completion.TrySetResult(Selected!.FolderPath);
|
|
|
|
/// <summary>Waehlt eine Zeile und bestaetigt in einem Zug — die Schaltflaeche je Zeile.</summary>
|
|
[RelayCommand]
|
|
private void Activate(InstanceChoiceViewModel? choice)
|
|
{
|
|
if (choice is null) return;
|
|
Selected = choice;
|
|
_completion.TrySetResult(choice.FolderPath);
|
|
}
|
|
|
|
/// <summary>Abbruch — der Aufrufer beendet die Anwendung oder schliesst den Dialog.</summary>
|
|
[RelayCommand]
|
|
private void Cancel() => _completion.TrySetResult(null);
|
|
|
|
[RelayCommand]
|
|
private void Create()
|
|
{
|
|
var name = NewInstanceName.Trim();
|
|
|
|
if (string.IsNullOrWhiteSpace(name))
|
|
{
|
|
ErrorText = LocalizationManager.Instance["desktop.instancePicker.nameRequired"];
|
|
return;
|
|
}
|
|
|
|
try
|
|
{
|
|
_directories.CreateInstance(name);
|
|
NewInstanceName = "";
|
|
ErrorText = "";
|
|
Refresh();
|
|
|
|
// Die frisch angelegte Instanz vorwählen — wer sie erstellt, will sie starten.
|
|
Selected = Instances.FirstOrDefault(i =>
|
|
string.Equals(i.InstanceName, name, StringComparison.Ordinal));
|
|
}
|
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException
|
|
or ArgumentException)
|
|
{
|
|
ErrorText = string.Format(
|
|
LocalizationManager.Instance["desktop.instancePicker.createFailed"],
|
|
ex.Message);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Wird gesetzt, wenn ein Fehler von außen kommt (etwa eine unlesbare
|
|
/// Instanzkonfiguration), damit das Fenster offen bleiben und eine andere Wahl
|
|
/// zulassen kann.
|
|
/// </summary>
|
|
public void ConfirmError(string message) => ErrorText = message;
|
|
}
|