Files
ClaudeDo/src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs
T
Mika Kuns 193752540b feat(ui): PowerUse-Toggle im Usage-Monitor, Opt-in pro Liste
Der Toggle sitzt unter den Gauges im Usage-Monitor, weil "das restliche Fenster
ausnutzen" eine Entscheidung ist, die man beim Blick auf das restliche Fenster
trifft. Er speichert sofort per Read-modify-write, gleiche Disziplin wie
SaveStagesAsync, damit er keine Felder ueberschreibt, die er nicht besitzt.

Zwei Stellen, die das Opt-in still ausgeschaltet haetten:
- WorkerHub.UpdateListConfig loescht die Config-Zeile, wenn alle Felder leer
  sind. PowerUseAllowed muss in diese Leer-Pruefung, sonst verschwindet das
  Opt-in sofort wieder, wenn es das einzige gesetzte Feld ist.
- SettingsModalViewModel baut das AppSettingsDto positional neu auf. Ohne
  Round-trip haette jedes Speichern der Settings PowerUseEnabled auf den
  Record-Default false zurueckgesetzt. Gleiches Muster wie _throttleStages.

Das Handoff-Budget ist als Spinner in Settings -> General editierbar, weil es
auch fuer normale (interaktive) Handler-Runs gilt und nicht an PowerUse haengt.
Das Addendum selbst ist ueber Settings -> Files anpassbar.
2026-09-02 15:44:13 +02:00

139 lines
5.2 KiB
C#

using System.Collections.ObjectModel;
using ClaudeDo.Data;
using ClaudeDo.Ui.Localization;
using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
namespace ClaudeDo.Ui.ViewModels.Modals.Settings;
public sealed partial class FilesSettingsTabViewModel : ViewModelBase
{
private readonly IWorkerClient _worker;
public OperationStatus RestoreOp { get; } = new();
public OperationStatus ClaudeBinOp { get; } = new();
// worker.config.json's claude_bin: a command name or a full path to the CLI. Saved on its own
// (not with the modal's Save button) because the worker owns the file, not app_settings.
[ObservableProperty] private string _claudeBin = "";
[ObservableProperty] private string _claudeBinResolvedHint = "";
[ObservableProperty] private string _statusMessage = "";
[ObservableProperty] private bool _hasCustomizedPrompts;
[ObservableProperty] private string? _viewedKindName;
[ObservableProperty] private string _viewedContent = "";
public string SystemPromptPath { get; } = PromptFiles.PathFor(PromptKind.System);
public string PlanningPromptPath { get; } = PromptFiles.PathFor(PromptKind.Planning);
public string PlanningInitialPromptPath { get; } = PromptFiles.PathFor(PromptKind.PlanningInitial);
public string RetryPromptPath { get; } = PromptFiles.PathFor(PromptKind.Retry);
public string DailyPrepPromptPath { get; } = PromptFiles.PathFor(PromptKind.DailyPrep);
public string WeeklyReportPromptPath { get; } = PromptFiles.PathFor(PromptKind.WeeklyReport);
public string PowerUsePromptPath { get; } = PromptFiles.PathFor(PromptKind.PowerUseAddendum);
public ObservableCollection<CustomizedPromptRowViewModel> CustomizedPrompts { get; } = new();
public FilesSettingsTabViewModel(IWorkerClient worker)
{
_worker = worker;
RefreshCustomizedPrompts();
RestoreOp.PropertyChanged += (_, e) =>
{
if (e.PropertyName == nameof(OperationStatus.IsRunning)) RestoreDefaultAgentsCommand.NotifyCanExecuteChanged();
};
}
public async Task LoadClaudeBinAsync()
{
var dto = await _worker.GetClaudeBinAsync();
if (dto is null) return;
ClaudeBin = dto.Value;
ApplyResolvedHint(dto);
}
[RelayCommand]
private async Task SaveClaudeBin()
{
StatusMessage = "";
using var op = ClaudeBinOp.Begin(Loc.T("ops.claudeBin.saving"));
var dto = await _worker.SetClaudeBinAsync(ClaudeBin);
if (dto is null)
{
StatusMessage = Loc.T("vm.filesTab.workerOffline");
return;
}
ClaudeBin = dto.Value;
ApplyResolvedHint(dto);
}
private void ApplyResolvedHint(ClaudeBinDto dto) =>
ClaudeBinResolvedHint = dto.ResolvedPath is null
? Loc.T("settings.files.claudeBinUnresolved")
: Loc.T("settings.files.claudeBinResolved", dto.ResolvedPath);
private bool CanRestoreDefaultAgents() => !RestoreOp.IsRunning;
[RelayCommand(CanExecute = nameof(CanRestoreDefaultAgents))]
private async Task RestoreDefaultAgents()
{
StatusMessage = "";
using var op = RestoreOp.Begin(Loc.T("ops.skills.restoringDefaults"));
try
{
var r = await _worker.RestoreDefaultAgentsAsync();
if (r is null) StatusMessage = Loc.T("vm.filesTab.workerOffline");
else if (r.Copied == 0 && r.Skipped == 0) StatusMessage = Loc.T("vm.filesTab.noneBundled");
else if (r.Copied == 0) StatusMessage = Loc.T("vm.filesTab.allPresent");
else StatusMessage = Loc.T("vm.filesTab.restored", r.Copied);
await _worker.RefreshAgentsAsync();
}
catch (Exception ex) { StatusMessage = Loc.T("vm.filesTab.restoreFailed", ex.Message); }
}
[RelayCommand]
private void ViewPrompt(string? kindName)
{
if (!Enum.TryParse<PromptKind>(kindName, ignoreCase: true, out var kind)) return;
ViewedKindName = kindName;
ViewedContent = PromptFiles.ReadOrDefault(kind);
}
private void RefreshCustomizedPrompts()
{
CustomizedPrompts.Clear();
foreach (var kind in Enum.GetValues<PromptKind>())
{
if (PromptFiles.Classify(kind) != PromptFileState.Edited) continue;
CustomizedPrompts.Add(new CustomizedPromptRowViewModel(kind, PromptFiles.DiffAgainstDefault(kind), OnResetPromptToDefault));
}
HasCustomizedPrompts = CustomizedPrompts.Count > 0;
}
private void OnResetPromptToDefault(PromptKind kind)
{
PromptFiles.ResetToDefault(kind);
RefreshCustomizedPrompts();
StatusMessage = Loc.T("vm.filesTab.resetToDefault");
}
}
public sealed partial class CustomizedPromptRowViewModel : ObservableObject
{
private readonly Action<PromptKind> _onReset;
public PromptKind Kind { get; }
public string KindName => Kind.ToString();
public string DiffPreview { get; }
public CustomizedPromptRowViewModel(PromptKind kind, string diffPreview, Action<PromptKind> onReset)
{
Kind = kind;
DiffPreview = diffPreview;
_onReset = onReset;
}
[RelayCommand]
private void Reset() => _onReset(Kind);
}