From ec4cbcbaf9331369138f864697cda0bbc46e0021 Mon Sep 17 00:00:00 2001 From: Mika Kuns Date: Wed, 12 Aug 2026 09:38:05 +0200 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFchore(claude-do):=20[A3]=20Settings=20?= =?UTF-8?q?+=20Update-Check:=20OperationStatus=20f=C3=BCr=20Skill-Inst?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vorgaben: `docs/superpowers/plans/2026-08-11-operation-feedback.md`, Gruppe A, Entwurf A3. P0-1 ist gemergt: `src/ClaudeDo.Ui/Services/OperationStatus.cs` + `src/ClaudeDo.Ui/Views/Controls/OperationIndicator.axaml`. SCOPE-ÄNDERUNG (Nutzer, 2026-08-12): OnlineInbox Sign-In/Sign-Out ist AUS DEM SCOPE GENOMMEN — vorerst unwichtig. `ViewModels/Modals/Settings/OnlineInboxSettingsViewModel.cs` und die ClaudeDo-Task: 2356806f-f16e-4652-b615-4562e95c3a65 --- .../Services/UpdateCheckService.cs | 68 +++++++++---------- .../ViewModels/IslandsShellViewModel.cs | 8 ++- .../Modals/RepoImportModalViewModel.cs | 3 + .../Settings/FilesSettingsTabViewModel.cs | 15 ++-- .../SessionSkillsSettingsTabViewModel.cs | 32 +++++++-- src/ClaudeDo.Ui/Views/MainWindow.axaml | 1 + .../Views/Modals/RepoImportModalView.axaml | 1 + .../Views/Modals/SettingsModalView.axaml | 7 +- .../Services/UpdateCheckServiceTests.cs | 19 ++++++ .../FilesSettingsTabViewModelTests.cs | 29 +++++++- .../RepoImportModalViewModelTests.cs | 12 ++++ .../SessionSkillsSettingsTabViewModelTests.cs | 60 +++++++++++++++- 12 files changed, 201 insertions(+), 54 deletions(-) diff --git a/src/ClaudeDo.Ui/Services/UpdateCheckService.cs b/src/ClaudeDo.Ui/Services/UpdateCheckService.cs index bd727596..eae549eb 100644 --- a/src/ClaudeDo.Ui/Services/UpdateCheckService.cs +++ b/src/ClaudeDo.Ui/Services/UpdateCheckService.cs @@ -1,4 +1,5 @@ using ClaudeDo.Releases; +using ClaudeDo.Ui.Localization; using CommunityToolkit.Mvvm.ComponentModel; namespace ClaudeDo.Ui.Services; @@ -15,10 +16,11 @@ public sealed partial class UpdateCheckService : ObservableObject { private readonly IReleaseClient _releases; + public OperationStatus Op { get; } = new(); + [ObservableProperty] private bool _isUpdateAvailable; [ObservableProperty] private string? _latestVersion; [ObservableProperty] private string _currentVersion; - [ObservableProperty] private bool _isChecking; [ObservableProperty] private UpdateCheckStatus _lastCheckStatus = UpdateCheckStatus.NeverChecked; public UpdateCheckService(IReleaseClient releases, string currentVersion) @@ -29,45 +31,39 @@ public sealed partial class UpdateCheckService : ObservableObject public async Task CheckNowAsync(CancellationToken ct) { - IsChecking = true; + using var op = Op.Begin(Loc.T("ops.updateCheck.checking")); + + GiteaRelease? rel; try { - GiteaRelease? rel; - try - { - rel = await _releases.GetLatestReleaseAsync(ct); - } - catch - { - LastCheckStatus = UpdateCheckStatus.CheckFailed; - IsUpdateAvailable = false; - return; - } - - if (rel is null) - { - LastCheckStatus = UpdateCheckStatus.CheckFailed; - IsUpdateAvailable = false; - return; - } - - var latest = (rel.TagName ?? "").TrimStart('v', 'V'); - var cmp = VersionComparer.Compare(latest, CurrentVersion); - if (cmp.IsNewer) - { - LatestVersion = latest; - IsUpdateAvailable = true; - LastCheckStatus = UpdateCheckStatus.UpdateAvailable; - } - else - { - IsUpdateAvailable = false; - LastCheckStatus = UpdateCheckStatus.UpToDate; - } + rel = await _releases.GetLatestReleaseAsync(ct); } - finally + catch { - IsChecking = false; + LastCheckStatus = UpdateCheckStatus.CheckFailed; + IsUpdateAvailable = false; + return; + } + + if (rel is null) + { + LastCheckStatus = UpdateCheckStatus.CheckFailed; + IsUpdateAvailable = false; + return; + } + + var latest = (rel.TagName ?? "").TrimStart('v', 'V'); + var cmp = VersionComparer.Compare(latest, CurrentVersion); + if (cmp.IsNewer) + { + LatestVersion = latest; + IsUpdateAvailable = true; + LastCheckStatus = UpdateCheckStatus.UpdateAvailable; + } + else + { + IsUpdateAvailable = false; + LastCheckStatus = UpdateCheckStatus.UpToDate; } } } diff --git a/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs b/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs index b6c35395..4077eb27 100644 --- a/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs @@ -377,6 +377,10 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable RefreshBannerFromStatus(); } }; + _updateCheck.Op.PropertyChanged += (_, e) => + { + if (e.PropertyName == nameof(OperationStatus.IsRunning)) CheckForUpdatesCommand.NotifyCanExecuteChanged(); + }; // Fire-and-forget startup check — never block UI. _ = Task.Run(async () => { @@ -578,7 +582,9 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable finally { _usageMonitorOpen = false; } } - [RelayCommand] + private bool CanCheckForUpdates() => !_updateCheck.Op.IsRunning; + + [RelayCommand(CanExecute = nameof(CanCheckForUpdates))] private async Task CheckForUpdatesAsync() { await _updateCheck.CheckNowAsync(CancellationToken.None); diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/RepoImportModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/RepoImportModalViewModel.cs index 3c88fee1..c674c8b1 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/RepoImportModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/RepoImportModalViewModel.cs @@ -33,6 +33,8 @@ public sealed partial class RepoImportModalViewModel : ViewModelBase private readonly HashSet _existingDirs = new(StringComparer.OrdinalIgnoreCase); private readonly List _folders = new(); + public OperationStatus ScanOp { get; } = new(); + public ObservableCollection Repos { get; } = new(); public Action? CloseAction { get; set; } @@ -96,6 +98,7 @@ public sealed partial class RepoImportModalViewModel : ViewModelBase private async Task ScanAndAddAsync(IEnumerable folders) { + using var op = ScanOp.Begin(Loc.T("ops.repoImport.scanning")); var current = new HashSet( Repos.Select(r => r.FullPath), StringComparer.OrdinalIgnoreCase); diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs index b156bb87..4013bd66 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs @@ -11,8 +11,9 @@ public sealed partial class FilesSettingsTabViewModel : ViewModelBase { private readonly IWorkerClient _worker; + public OperationStatus RestoreOp { get; } = new(); + [ObservableProperty] private string _statusMessage = ""; - [ObservableProperty] private bool _isBusy; [ObservableProperty] private bool _hasCustomizedPrompts; [ObservableProperty] private string? _viewedKindName; [ObservableProperty] private string _viewedContent = ""; @@ -30,12 +31,19 @@ public sealed partial class FilesSettingsTabViewModel : ViewModelBase { _worker = worker; RefreshCustomizedPrompts(); + RestoreOp.PropertyChanged += (_, e) => + { + if (e.PropertyName == nameof(OperationStatus.IsRunning)) RestoreDefaultAgentsCommand.NotifyCanExecuteChanged(); + }; } - [RelayCommand] + private bool CanRestoreDefaultAgents() => !RestoreOp.IsRunning; + + [RelayCommand(CanExecute = nameof(CanRestoreDefaultAgents))] private async Task RestoreDefaultAgents() { - IsBusy = true; StatusMessage = ""; + StatusMessage = ""; + using var op = RestoreOp.Begin(Loc.T("ops.skills.restoringDefaults")); try { var r = await _worker.RestoreDefaultAgentsAsync(); @@ -46,7 +54,6 @@ public sealed partial class FilesSettingsTabViewModel : ViewModelBase await _worker.RefreshAgentsAsync(); } catch (Exception ex) { StatusMessage = Loc.T("vm.filesTab.restoreFailed", ex.Message); } - finally { IsBusy = false; } } [RelayCommand] diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/SessionSkillsSettingsTabViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/SessionSkillsSettingsTabViewModel.cs index 2382240d..ba48180b 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/SessionSkillsSettingsTabViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/SessionSkillsSettingsTabViewModel.cs @@ -10,6 +10,9 @@ public sealed partial class SessionSkillsSettingsTabViewModel : ViewModelBase { private readonly IWorkerClient _worker; + public OperationStatus InstallOp { get; } = new(); + public OperationStatus UpdateOp { get; } = new(); + [ObservableProperty] private string _installUrl = ""; [ObservableProperty] private string _statusMessage = ""; [ObservableProperty] private bool _isBusy; @@ -17,7 +20,18 @@ public sealed partial class SessionSkillsSettingsTabViewModel : ViewModelBase public ObservableCollection Skills { get; } = new(); - public SessionSkillsSettingsTabViewModel(IWorkerClient worker) => _worker = worker; + public SessionSkillsSettingsTabViewModel(IWorkerClient worker) + { + _worker = worker; + InstallOp.PropertyChanged += (_, e) => + { + if (e.PropertyName == nameof(OperationStatus.IsRunning)) InstallCommand.NotifyCanExecuteChanged(); + }; + UpdateOp.PropertyChanged += (_, e) => + { + if (e.PropertyName == nameof(OperationStatus.IsRunning)) UpdateCommand.NotifyCanExecuteChanged(); + }; + } public async Task LoadAsync() { @@ -32,11 +46,14 @@ public sealed partial class SessionSkillsSettingsTabViewModel : ViewModelBase finally { IsBusy = false; } } - [RelayCommand] + private bool CanInstall() => !InstallOp.IsRunning; + + [RelayCommand(CanExecute = nameof(CanInstall))] private async Task InstallAsync() { if (string.IsNullOrWhiteSpace(InstallUrl)) return; - IsBusy = true; StatusMessage = ""; + StatusMessage = ""; + using var op = InstallOp.Begin(Loc.T("ops.skills.installing")); try { var installed = await _worker.InstallSessionSkillAsync(InstallUrl.Trim()); @@ -45,14 +62,16 @@ public sealed partial class SessionSkillsSettingsTabViewModel : ViewModelBase await LoadAsync(); } catch (Exception ex) { StatusMessage = Loc.T("vm.sessionSkillsTab.installFailed", ex.Message); } - finally { IsBusy = false; } } - [RelayCommand] + private bool CanUpdate(string? sourceUrl) => !UpdateOp.IsRunning; + + [RelayCommand(CanExecute = nameof(CanUpdate))] private async Task UpdateAsync(string? sourceUrl) { if (string.IsNullOrWhiteSpace(sourceUrl)) return; - IsBusy = true; StatusMessage = ""; + StatusMessage = ""; + using var op = UpdateOp.Begin(Loc.T("ops.skills.updating")); try { await _worker.UpdateSessionSkillAsync(sourceUrl); @@ -60,7 +79,6 @@ public sealed partial class SessionSkillsSettingsTabViewModel : ViewModelBase await LoadAsync(); } catch (Exception ex) { StatusMessage = Loc.T("vm.sessionSkillsTab.updateFailed", ex.Message); } - finally { IsBusy = false; } } [RelayCommand] diff --git a/src/ClaudeDo.Ui/Views/MainWindow.axaml b/src/ClaudeDo.Ui/Views/MainWindow.axaml index a5dd9fee..6e844f2a 100644 --- a/src/ClaudeDo.Ui/Views/MainWindow.axaml +++ b/src/ClaudeDo.Ui/Views/MainWindow.axaml @@ -83,6 +83,7 @@ + diff --git a/src/ClaudeDo.Ui/Views/Modals/RepoImportModalView.axaml b/src/ClaudeDo.Ui/Views/Modals/RepoImportModalView.axaml index 4e686f93..e607f6e7 100644 --- a/src/ClaudeDo.Ui/Views/Modals/RepoImportModalView.axaml +++ b/src/ClaudeDo.Ui/Views/Modals/RepoImportModalView.axaml @@ -39,6 +39,7 @@ Command="{Binding ForgetFoldersCommand}" IsVisible="{Binding HasFolders}"/> + diff --git a/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml b/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml index 952907db..01d21c60 100644 --- a/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml +++ b/src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml @@ -280,8 +280,8 @@ TextWrapping="Wrap"/>