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; /// /// Eine Instanz in der Auswahl. /// /// Das Monogramm und der Punkt stammen aus dem Entwurf. Der Punkt zeigt bewusst /// Bereitschaft 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. /// public sealed class InstanceChoiceViewModel(InstanceInfo info, bool isCurrent) { public InstanceInfo Info { get; } = info; public string InstanceName => Info.InstanceName; public string FolderPath => Info.FolderPath; /// Erste zwei Buchstaben, gross — wie im Entwurf. 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); } } /// Ohne API-Key kann die Instanz keine Agenten starten. public bool IsReady => Info.ApiKeyStatus.Equals("Konfiguriert", StringComparison.OrdinalIgnoreCase) || Info.ApiKeyStatus.Equals("Configured", StringComparison.OrdinalIgnoreCase); /// Die gerade laufende Instanz — traegt im Entwurf das Etikett „Aktiv". public bool IsCurrent { get; } = isCurrent; } /// /// Auswahl der Instanz — beim Start und beim Wechseln aus der laufenden Anwendung. /// /// Die WinForms-Fassung mischte Datenzugriff, Spaltenformatierung und Meldungsfenster /// in einem Formular. Hier steht nur, was geschieht; das wie es aussieht /// liegt in der Ansicht, und Rückfragen laufen über statt über /// einen direkten Dialogaufruf — so bleibt das Ansichtsmodell ohne Fensterbezug prüfbar. /// public sealed partial class InstancePickerViewModel : ViewModelBase { private readonly InstanceDirectoryManager _directories; private readonly TaskCompletionSource _completion = new(); private readonly string? _currentInstancePath; [ObservableProperty] [NotifyCanExecuteChangedFor(nameof(StartCommand))] private InstanceChoiceViewModel? _selected; [ObservableProperty] private string _newInstanceName = ""; /// Fehlertext für die Ansicht; leer heißt: alles in Ordnung. [ObservableProperty] private string _errorText = ""; public ObservableCollection Instances { get; } = []; /// Der gewählte Pfad, oder null bei Abbruch. public Task Result => _completion.Task; public string InstancesDirectory => _directories.InstancesDirectory; /// /// Beim Start heisst die Schaltflaeche „Starten", aus der laufenden Anwendung heraus /// „Wechseln" — und dann steht der Hinweis dabei, dass dafuer neu gestartet wird. /// public bool IsSwitchMode => _currentInstancePath is not null; public string ConfirmLabel => IsSwitchMode ? LocalizationManager.Instance["desktop.instancePicker.confirmSwitch"] : LocalizationManager.Instance["desktop.instancePicker.confirmStart"]; /// Ueberschrift des Dialogs — nicht aus ConfirmLabel zusammengesetzt, /// sonst stuende dort „Instanz Starten". 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"]; /// /// Pfad der laufenden Instanz, oder null beim Start. Steuert Beschriftung und /// das Etikett „Aktiv" in der Liste. /// 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); /// Waehlt eine Zeile und bestaetigt in einem Zug — die Schaltflaeche je Zeile. [RelayCommand] private void Activate(InstanceChoiceViewModel? choice) { if (choice is null) return; Selected = choice; _completion.TrySetResult(choice.FolderPath); } /// Abbruch — der Aufrufer beendet die Anwendung oder schliesst den Dialog. [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); } } /// /// Wird gesetzt, wenn ein Fehler von außen kommt (etwa eine unlesbare /// Instanzkonfiguration), damit das Fenster offen bleiben und eine andere Wahl /// zulassen kann. /// public void ConfirmError(string message) => ErrorText = message; }