EnsureExists blindly seeded ~/.todo-app/prompts/*.md with the bundled default and never revisited it, so any file created by opening the Files settings tab shadowed every later default change permanently (SuggestImprovement/AskUser sections never reached real runs since 2026-06-04). PromptFiles now hashes what a file was seeded/saved with in prompts/.defaults.json: Classify() tells missing/current-default/ known-past-default/edited apart, ReconcileStaleDefaults() drops files that only ever matched a superseded default, and real edits are left alone and surfaced in the Files tab with a diff + reset-to-default action. QuarantineOrphans() moves stale-named leftovers (agent.md, planning.md) into prompts/_orphans instead of silently deleting them. Wired as a Worker startup sweep (PromptFileRecovery) alongside the existing OrphanRecovery/AttachmentOrphanRecovery services.
105 lines
4.0 KiB
C#
105 lines
4.0 KiB
C#
using System.Collections.ObjectModel;
|
|
using System.Diagnostics;
|
|
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;
|
|
|
|
[ObservableProperty] private string _statusMessage = "";
|
|
[ObservableProperty] private bool _isBusy;
|
|
[ObservableProperty] private bool _hasCustomizedPrompts;
|
|
|
|
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 ObservableCollection<CustomizedPromptRowViewModel> CustomizedPrompts { get; } = new();
|
|
|
|
public FilesSettingsTabViewModel(IWorkerClient worker)
|
|
{
|
|
_worker = worker;
|
|
RefreshCustomizedPrompts();
|
|
}
|
|
|
|
[RelayCommand]
|
|
private async Task RestoreDefaultAgents()
|
|
{
|
|
IsBusy = true; StatusMessage = "";
|
|
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); }
|
|
finally { IsBusy = false; }
|
|
}
|
|
|
|
[RelayCommand]
|
|
private void OpenPrompt(string? kindName)
|
|
{
|
|
if (!Enum.TryParse<PromptKind>(kindName, ignoreCase: true, out var kind)) return;
|
|
try
|
|
{
|
|
// No override file yet: seed it with today's bundled default (hash-tracked, so a later
|
|
// default change reconciles this file away automatically unless the user actually edits it)
|
|
// rather than leaving a plain file that would freeze the prompt forever.
|
|
if (!File.Exists(PromptFiles.PathFor(kind)))
|
|
PromptFiles.Save(kind, PromptFiles.DefaultFor(kind));
|
|
Process.Start(new ProcessStartInfo(PromptFiles.PathFor(kind)) { UseShellExecute = true });
|
|
}
|
|
catch (Exception ex) { StatusMessage = Loc.T("vm.filesTab.openFailed", ex.Message); }
|
|
finally { RefreshCustomizedPrompts(); }
|
|
}
|
|
|
|
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);
|
|
}
|