feat(ui): add settings modal and wire to worker hub
This commit is contained in:
@@ -1,6 +1,7 @@
|
|||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Markup.Xaml;
|
using Avalonia.Markup.Xaml;
|
||||||
|
using ClaudeDo.Ui.Services;
|
||||||
using ClaudeDo.Ui.ViewModels;
|
using ClaudeDo.Ui.ViewModels;
|
||||||
using ClaudeDo.Ui.Views;
|
using ClaudeDo.Ui.Views;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
@@ -24,6 +25,10 @@ public partial class App : Application
|
|||||||
{
|
{
|
||||||
DataContext = Services.GetRequiredService<IslandsShellViewModel>(),
|
DataContext = Services.GetRequiredService<IslandsShellViewModel>(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Kick off the SignalR retry loop — reconnects indefinitely if the worker
|
||||||
|
// is not up yet, or goes down and comes back.
|
||||||
|
_ = Services.GetRequiredService<WorkerClient>().StartAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
base.OnFrameworkInitializationCompleted();
|
base.OnFrameworkInitializationCompleted();
|
||||||
|
|||||||
@@ -77,9 +77,13 @@ sealed class Program
|
|||||||
|
|
||||||
// ViewModels
|
// ViewModels
|
||||||
sc.AddTransient<WorktreeModalViewModel>();
|
sc.AddTransient<WorktreeModalViewModel>();
|
||||||
|
sc.AddTransient<SettingsModalViewModel>();
|
||||||
|
|
||||||
// Islands shell VMs
|
// Islands shell VMs
|
||||||
sc.AddSingleton<ListsIslandViewModel>();
|
sc.AddSingleton<ListsIslandViewModel>(sp =>
|
||||||
|
new ListsIslandViewModel(
|
||||||
|
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
|
||||||
|
sp));
|
||||||
sc.AddSingleton<TasksIslandViewModel>(sp =>
|
sc.AddSingleton<TasksIslandViewModel>(sp =>
|
||||||
new TasksIslandViewModel(sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>()));
|
new TasksIslandViewModel(sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>()));
|
||||||
sc.AddSingleton<DetailsIslandViewModel>(sp =>
|
sc.AddSingleton<DetailsIslandViewModel>(sp =>
|
||||||
|
|||||||
@@ -226,6 +226,47 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable
|
|||||||
await _hub.DisposeAsync();
|
await _hub.DisposeAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<AppSettingsDto?> GetAppSettingsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _hub.InvokeAsync<AppSettingsDto>("GetAppSettings");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAppSettingsAsync(AppSettingsDto dto)
|
||||||
|
{
|
||||||
|
await _hub.InvokeAsync("UpdateAppSettings", dto);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _hub.InvokeAsync<WorktreeCleanupDto>("CleanupFinishedWorktrees");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WorktreeResetDto?> ResetAllWorktreesAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _hub.InvokeAsync<WorktreeResetDto>("ResetAllWorktrees");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// DTOs for deserializing hub responses
|
// DTOs for deserializing hub responses
|
||||||
private sealed class ActiveTaskDto
|
private sealed class ActiveTaskDto
|
||||||
{
|
{
|
||||||
@@ -234,3 +275,16 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable
|
|||||||
public DateTime StartedAt { get; set; }
|
public DateTime StartedAt { get; set; }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public sealed record AppSettingsDto(
|
||||||
|
string DefaultClaudeInstructions,
|
||||||
|
string DefaultModel,
|
||||||
|
int DefaultMaxTurns,
|
||||||
|
string DefaultPermissionMode,
|
||||||
|
string WorktreeStrategy,
|
||||||
|
string? CentralWorktreeRoot,
|
||||||
|
bool WorktreeAutoCleanupEnabled,
|
||||||
|
int WorktreeAutoCleanupDays);
|
||||||
|
|
||||||
|
public sealed record WorktreeCleanupDto(int Removed);
|
||||||
|
public sealed record WorktreeResetDto(int Removed, int TasksAffected, bool Blocked, int RunningTasks);
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using ClaudeDo.Ui.Services;
|
||||||
using ClaudeDo.Ui.ViewModels.Islands;
|
using ClaudeDo.Ui.ViewModels.Islands;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.ViewModels;
|
namespace ClaudeDo.Ui.ViewModels;
|
||||||
@@ -9,6 +10,14 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
|
|||||||
public ListsIslandViewModel Lists { get; }
|
public ListsIslandViewModel Lists { get; }
|
||||||
public TasksIslandViewModel Tasks { get; }
|
public TasksIslandViewModel Tasks { get; }
|
||||||
public DetailsIslandViewModel Details { get; }
|
public DetailsIslandViewModel Details { get; }
|
||||||
|
public WorkerClient Worker { get; }
|
||||||
|
|
||||||
|
public string ConnectionText =>
|
||||||
|
Worker.IsConnected ? "Online"
|
||||||
|
: Worker.IsReconnecting ? "Connecting…"
|
||||||
|
: "Offline";
|
||||||
|
|
||||||
|
public bool IsOffline => !Worker.IsConnected && !Worker.IsReconnecting;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private double _windowWidth = 1280;
|
private double _windowWidth = 1280;
|
||||||
@@ -37,9 +46,10 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
|
|||||||
public IslandsShellViewModel(
|
public IslandsShellViewModel(
|
||||||
ListsIslandViewModel lists,
|
ListsIslandViewModel lists,
|
||||||
TasksIslandViewModel tasks,
|
TasksIslandViewModel tasks,
|
||||||
DetailsIslandViewModel details)
|
DetailsIslandViewModel details,
|
||||||
|
WorkerClient worker)
|
||||||
{
|
{
|
||||||
Lists = lists; Tasks = tasks; Details = details;
|
Lists = lists; Tasks = tasks; Details = details; Worker = worker;
|
||||||
Lists.SelectionChanged += (_, _) => Tasks.LoadForList(Lists.SelectedList);
|
Lists.SelectionChanged += (_, _) => Tasks.LoadForList(Lists.SelectedList);
|
||||||
Tasks.SelectionChanged += (_, _) => Details.Bind(Tasks.SelectedTask);
|
Tasks.SelectionChanged += (_, _) => Details.Bind(Tasks.SelectedTask);
|
||||||
Details.CloseDetail = () => Tasks.SelectedTask = null;
|
Details.CloseDetail = () => Tasks.SelectedTask = null;
|
||||||
@@ -48,6 +58,14 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
|
|||||||
Tasks.LoadForList(Lists.SelectedList);
|
Tasks.LoadForList(Lists.SelectedList);
|
||||||
return System.Threading.Tasks.Task.CompletedTask;
|
return System.Threading.Tasks.Task.CompletedTask;
|
||||||
};
|
};
|
||||||
|
Worker.PropertyChanged += (_, e) =>
|
||||||
|
{
|
||||||
|
if (e.PropertyName is nameof(WorkerClient.IsConnected) or nameof(WorkerClient.IsReconnecting))
|
||||||
|
{
|
||||||
|
OnPropertyChanged(nameof(ConnectionText));
|
||||||
|
OnPropertyChanged(nameof(IsOffline));
|
||||||
|
}
|
||||||
|
};
|
||||||
_ = Lists.LoadAsync();
|
_ = Lists.LoadAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
176
src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs
Normal file
176
src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs
Normal file
@@ -0,0 +1,176 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
|
using ClaudeDo.Data;
|
||||||
|
using ClaudeDo.Ui.Services;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.ViewModels.Modals;
|
||||||
|
|
||||||
|
public sealed partial class SettingsModalViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly WorkerClient _worker;
|
||||||
|
|
||||||
|
[ObservableProperty] private string _defaultClaudeInstructions = "";
|
||||||
|
[ObservableProperty] private string _defaultModel = "sonnet";
|
||||||
|
[ObservableProperty] private int _defaultMaxTurns = 30;
|
||||||
|
[ObservableProperty] private string _defaultPermissionMode = "bypassPermissions";
|
||||||
|
[ObservableProperty] private string _worktreeStrategy = "sibling";
|
||||||
|
[ObservableProperty] private string? _centralWorktreeRoot;
|
||||||
|
[ObservableProperty] private bool _worktreeAutoCleanupEnabled;
|
||||||
|
[ObservableProperty] private int _worktreeAutoCleanupDays = 7;
|
||||||
|
|
||||||
|
[ObservableProperty] private string _statusMessage = "";
|
||||||
|
[ObservableProperty] private bool _isBusy;
|
||||||
|
[ObservableProperty] private bool _showResetConfirm;
|
||||||
|
[ObservableProperty] private string _validationError = "";
|
||||||
|
|
||||||
|
public IReadOnlyList<string> Models { get; } = new[] { "opus", "sonnet", "haiku" };
|
||||||
|
public IReadOnlyList<string> PermissionModes { get; } = new[]
|
||||||
|
{ "bypassPermissions", "acceptEdits", "plan", "default" };
|
||||||
|
public IReadOnlyList<string> WorktreeStrategies { get; } = new[] { "sibling", "central" };
|
||||||
|
|
||||||
|
public string AppVersion { get; } =
|
||||||
|
Assembly.GetExecutingAssembly().GetName().Version?.ToString(3) ?? "0.0.0";
|
||||||
|
|
||||||
|
public string DataFolderPath { get; } = Paths.AppDataRoot();
|
||||||
|
public string LogsFolderPath { get; } = Path.Combine(Paths.AppDataRoot(), "logs");
|
||||||
|
public string WorkerConfigPath { get; } = Path.Combine(Paths.AppDataRoot(), "worker.config.json");
|
||||||
|
|
||||||
|
public Action? CloseAction { get; set; }
|
||||||
|
|
||||||
|
public SettingsModalViewModel(WorkerClient worker)
|
||||||
|
{
|
||||||
|
_worker = worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadAsync()
|
||||||
|
{
|
||||||
|
IsBusy = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dto = await _worker.GetAppSettingsAsync();
|
||||||
|
if (dto is not null)
|
||||||
|
{
|
||||||
|
DefaultClaudeInstructions = dto.DefaultClaudeInstructions ?? "";
|
||||||
|
DefaultModel = dto.DefaultModel ?? "sonnet";
|
||||||
|
DefaultMaxTurns = dto.DefaultMaxTurns;
|
||||||
|
DefaultPermissionMode = dto.DefaultPermissionMode ?? "bypassPermissions";
|
||||||
|
WorktreeStrategy = dto.WorktreeStrategy ?? "sibling";
|
||||||
|
CentralWorktreeRoot = dto.CentralWorktreeRoot;
|
||||||
|
WorktreeAutoCleanupEnabled = dto.WorktreeAutoCleanupEnabled;
|
||||||
|
WorktreeAutoCleanupDays = dto.WorktreeAutoCleanupDays;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
StatusMessage = "Worker offline — settings read-only.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally { IsBusy = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool Validate()
|
||||||
|
{
|
||||||
|
if (DefaultMaxTurns < 1 || DefaultMaxTurns > 200)
|
||||||
|
{ ValidationError = "Max turns must be between 1 and 200."; return false; }
|
||||||
|
|
||||||
|
if (WorktreeAutoCleanupEnabled &&
|
||||||
|
(WorktreeAutoCleanupDays < 1 || WorktreeAutoCleanupDays > 365))
|
||||||
|
{ ValidationError = "Cleanup days must be between 1 and 365."; return false; }
|
||||||
|
|
||||||
|
if (WorktreeStrategy == "central")
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(CentralWorktreeRoot))
|
||||||
|
{ ValidationError = "Central worktree root is required for Central strategy."; return false; }
|
||||||
|
if (!Directory.Exists(CentralWorktreeRoot))
|
||||||
|
{ ValidationError = $"Directory not found: {CentralWorktreeRoot}"; return false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
ValidationError = "";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task Save()
|
||||||
|
{
|
||||||
|
if (!Validate()) return;
|
||||||
|
|
||||||
|
IsBusy = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dto = new AppSettingsDto(
|
||||||
|
DefaultClaudeInstructions ?? "",
|
||||||
|
DefaultModel ?? "sonnet",
|
||||||
|
DefaultMaxTurns,
|
||||||
|
DefaultPermissionMode ?? "bypassPermissions",
|
||||||
|
WorktreeStrategy ?? "sibling",
|
||||||
|
string.IsNullOrWhiteSpace(CentralWorktreeRoot) ? null : CentralWorktreeRoot,
|
||||||
|
WorktreeAutoCleanupEnabled,
|
||||||
|
WorktreeAutoCleanupDays);
|
||||||
|
await _worker.UpdateAppSettingsAsync(dto);
|
||||||
|
CloseAction?.Invoke();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
StatusMessage = $"Save failed: {ex.Message}";
|
||||||
|
}
|
||||||
|
finally { IsBusy = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Cancel() => CloseAction?.Invoke();
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task CleanupWorktrees()
|
||||||
|
{
|
||||||
|
IsBusy = true;
|
||||||
|
StatusMessage = "";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _worker.CleanupFinishedWorktreesAsync();
|
||||||
|
StatusMessage = result is null
|
||||||
|
? "Worker offline."
|
||||||
|
: $"Removed {result.Removed} worktree(s).";
|
||||||
|
}
|
||||||
|
finally { IsBusy = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void RequestResetConfirm() => ShowResetConfirm = true;
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void CancelResetConfirm() => ShowResetConfirm = false;
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private async Task ConfirmResetAll()
|
||||||
|
{
|
||||||
|
ShowResetConfirm = false;
|
||||||
|
IsBusy = true;
|
||||||
|
StatusMessage = "";
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var result = await _worker.ResetAllWorktreesAsync();
|
||||||
|
if (result is null)
|
||||||
|
StatusMessage = "Worker offline.";
|
||||||
|
else if (result.Blocked)
|
||||||
|
StatusMessage = $"Cannot force-remove: {result.RunningTasks} task(s) still running. Cancel them first.";
|
||||||
|
else
|
||||||
|
StatusMessage = $"Removed {result.Removed} worktree(s) from {result.TasksAffected} task(s).";
|
||||||
|
}
|
||||||
|
finally { IsBusy = false; }
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void OpenPath(string? path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path)) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var target = File.Exists(path) ? path : (Directory.Exists(path) ? path : null);
|
||||||
|
if (target is null) return;
|
||||||
|
Process.Start(new ProcessStartInfo("explorer.exe", $"\"{target}\"") { UseShellExecute = true });
|
||||||
|
}
|
||||||
|
catch { /* ignore */ }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
<KeyBinding Gesture="Shift+OemQuestion" Command="{Binding FocusSearchCommand}"/>
|
<KeyBinding Gesture="Shift+OemQuestion" Command="{Binding FocusSearchCommand}"/>
|
||||||
<KeyBinding Gesture="Ctrl+N" Command="{Binding FocusAddTaskCommand}"/>
|
<KeyBinding Gesture="Ctrl+N" Command="{Binding FocusAddTaskCommand}"/>
|
||||||
</Window.KeyBindings>
|
</Window.KeyBindings>
|
||||||
<Grid RowDefinitions="36,*">
|
<Grid RowDefinitions="36,*,22">
|
||||||
<!-- Custom title bar -->
|
<!-- Custom title bar -->
|
||||||
<Border Grid.Row="0"
|
<Border Grid.Row="0"
|
||||||
Background="{DynamicResource DeepBrush}"
|
Background="{DynamicResource DeepBrush}"
|
||||||
@@ -98,5 +98,38 @@
|
|||||||
<islands:DetailsIslandView DataContext="{Binding Details}"/>
|
<islands:DetailsIslandView DataContext="{Binding Details}"/>
|
||||||
</Border>
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
<!-- Footer: connection status -->
|
||||||
|
<Border Grid.Row="2"
|
||||||
|
Background="{DynamicResource DeepBrush}"
|
||||||
|
BorderBrush="{DynamicResource LineBrush}"
|
||||||
|
BorderThickness="0,1,0,0">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="7"
|
||||||
|
VerticalAlignment="Center" Margin="14,0">
|
||||||
|
<Ellipse Width="7" Height="7" Fill="#4CAF50"
|
||||||
|
IsVisible="{Binding Worker.IsConnected}"/>
|
||||||
|
<Ellipse Width="7" Height="7" Fill="#FFA726"
|
||||||
|
IsVisible="{Binding Worker.IsReconnecting}"/>
|
||||||
|
<Ellipse Width="7" Height="7" Fill="#EF5350"
|
||||||
|
IsVisible="{Binding IsOffline}"/>
|
||||||
|
<TextBlock Text="{Binding ConnectionText, Converter={StaticResource UpperCase}}"
|
||||||
|
FontFamily="{DynamicResource MonoFont}"
|
||||||
|
FontSize="10"
|
||||||
|
LetterSpacing="1.4"
|
||||||
|
Foreground="{DynamicResource TextDimBrush}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="·"
|
||||||
|
FontFamily="{DynamicResource MonoFont}"
|
||||||
|
FontSize="10"
|
||||||
|
Foreground="{DynamicResource TextFaintBrush}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Text="WORKER"
|
||||||
|
FontFamily="{DynamicResource MonoFont}"
|
||||||
|
FontSize="10"
|
||||||
|
LetterSpacing="1.4"
|
||||||
|
Foreground="{DynamicResource TextFaintBrush}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
</Grid>
|
</Grid>
|
||||||
</Window>
|
</Window>
|
||||||
|
|||||||
253
src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml
Normal file
253
src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml
Normal file
@@ -0,0 +1,253 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
|
||||||
|
x:Class="ClaudeDo.Ui.Views.Modals.SettingsModalView"
|
||||||
|
x:DataType="vm:SettingsModalViewModel"
|
||||||
|
Title="Settings"
|
||||||
|
Width="580" Height="760"
|
||||||
|
SystemDecorations="None"
|
||||||
|
ExtendClientAreaToDecorationsHint="True"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
Background="{DynamicResource SurfaceBrush}">
|
||||||
|
|
||||||
|
<Window.KeyBindings>
|
||||||
|
<KeyBinding Gesture="Escape" Command="{Binding CancelCommand}"/>
|
||||||
|
</Window.KeyBindings>
|
||||||
|
|
||||||
|
<Window.Styles>
|
||||||
|
<Style Selector="TextBlock.section-label">
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource MonoFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="10"/>
|
||||||
|
<Setter Property="LetterSpacing" Value="1.4"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextFaintBrush}"/>
|
||||||
|
<Setter Property="Margin" Value="4,0,0,6"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.field-label">
|
||||||
|
<Setter Property="FontSize" Value="11"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextDimBrush}"/>
|
||||||
|
<Setter Property="Margin" Value="0,0,0,4"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="TextBlock.path-mono">
|
||||||
|
<Setter Property="FontFamily" Value="{DynamicResource MonoFont}"/>
|
||||||
|
<Setter Property="FontSize" Value="11"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource TextDimBrush}"/>
|
||||||
|
<Setter Property="TextTrimming" Value="CharacterEllipsis"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.section">
|
||||||
|
<Setter Property="BorderBrush" Value="{DynamicResource LineBrush}"/>
|
||||||
|
<Setter Property="BorderThickness" Value="1"/>
|
||||||
|
<Setter Property="CornerRadius" Value="6"/>
|
||||||
|
<Setter Property="Padding" Value="14"/>
|
||||||
|
<Setter Property="Background" Value="{DynamicResource DeepBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.danger">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource BloodBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="White"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Button.primary">
|
||||||
|
<Setter Property="Background" Value="{DynamicResource AccentBrush}"/>
|
||||||
|
<Setter Property="Foreground" Value="{DynamicResource DeepBrush}"/>
|
||||||
|
<Setter Property="FontWeight" Value="SemiBold"/>
|
||||||
|
</Style>
|
||||||
|
</Window.Styles>
|
||||||
|
|
||||||
|
<Border Background="{DynamicResource SurfaceBrush}"
|
||||||
|
BorderBrush="{DynamicResource LineBrush}"
|
||||||
|
BorderThickness="1">
|
||||||
|
<Grid RowDefinitions="36,*,52">
|
||||||
|
|
||||||
|
<!-- Title bar -->
|
||||||
|
<Border Grid.Row="0"
|
||||||
|
x:Name="TitleBar"
|
||||||
|
Background="{DynamicResource DeepBrush}"
|
||||||
|
BorderBrush="{DynamicResource LineBrush}"
|
||||||
|
BorderThickness="0,0,0,1"
|
||||||
|
PointerPressed="TitleBar_PointerPressed">
|
||||||
|
<Grid ColumnDefinitions="*,Auto" Margin="14,0">
|
||||||
|
<TextBlock Text="SETTINGS"
|
||||||
|
FontFamily="{DynamicResource MonoFont}"
|
||||||
|
FontSize="11"
|
||||||
|
LetterSpacing="1.4"
|
||||||
|
Foreground="{DynamicResource TextBrush}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Classes="icon-btn"
|
||||||
|
Content="✕"
|
||||||
|
FontSize="12"
|
||||||
|
Command="{Binding CancelCommand}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Scrollable body -->
|
||||||
|
<ScrollViewer Grid.Row="1" Padding="20,16">
|
||||||
|
<StackPanel Spacing="18">
|
||||||
|
|
||||||
|
<!-- CLAUDE DEFAULTS -->
|
||||||
|
<StackPanel Spacing="0">
|
||||||
|
<TextBlock Classes="section-label" Text="CLAUDE DEFAULTS"/>
|
||||||
|
<Border Classes="section">
|
||||||
|
<StackPanel Spacing="12">
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Classes="field-label" Text="Default instructions"/>
|
||||||
|
<TextBox AcceptsReturn="True"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
Height="110"
|
||||||
|
Watermark="Baseline instructions applied to every task (e.g. 'speak German', 'never touch .env')"
|
||||||
|
Text="{Binding DefaultClaudeInstructions, Mode=TwoWay}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Grid ColumnDefinitions="*,12,*,12,*">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
|
<TextBlock Classes="field-label" Text="Model"/>
|
||||||
|
<ComboBox ItemsSource="{Binding Models}"
|
||||||
|
SelectedItem="{Binding DefaultModel, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Stretch"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" Spacing="4">
|
||||||
|
<TextBlock Classes="field-label" Text="Max turns"/>
|
||||||
|
<NumericUpDown Value="{Binding DefaultMaxTurns, Mode=TwoWay}"
|
||||||
|
Minimum="1" Maximum="200" Increment="1" FormatString="0"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="4" Spacing="4">
|
||||||
|
<TextBlock Classes="field-label" Text="Permission"/>
|
||||||
|
<ComboBox ItemsSource="{Binding PermissionModes}"
|
||||||
|
SelectedItem="{Binding DefaultPermissionMode, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Stretch"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- WORKTREES -->
|
||||||
|
<StackPanel Spacing="0">
|
||||||
|
<TextBlock Classes="section-label" Text="WORKTREES"/>
|
||||||
|
<Border Classes="section">
|
||||||
|
<StackPanel Spacing="12">
|
||||||
|
<Grid ColumnDefinitions="*,12,2*">
|
||||||
|
<StackPanel Grid.Column="0" Spacing="4">
|
||||||
|
<TextBlock Classes="field-label" Text="Strategy"/>
|
||||||
|
<ComboBox ItemsSource="{Binding WorktreeStrategies}"
|
||||||
|
SelectedItem="{Binding WorktreeStrategy, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Stretch"/>
|
||||||
|
</StackPanel>
|
||||||
|
<StackPanel Grid.Column="2" Spacing="4">
|
||||||
|
<TextBlock Classes="field-label" Text="Central worktree root"/>
|
||||||
|
<TextBox Text="{Binding CentralWorktreeRoot, Mode=TwoWay}"
|
||||||
|
Watermark="e.g. C:\worktrees"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Grid>
|
||||||
|
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<CheckBox IsChecked="{Binding WorktreeAutoCleanupEnabled, Mode=TwoWay}"
|
||||||
|
Content="Auto-cleanup finished worktrees after"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<NumericUpDown Value="{Binding WorktreeAutoCleanupDays, Mode=TwoWay}"
|
||||||
|
Width="130" Minimum="1" Maximum="365" Increment="1" FormatString="0"
|
||||||
|
IsEnabled="{Binding WorktreeAutoCleanupEnabled}"/>
|
||||||
|
<TextBlock Text="days" VerticalAlignment="Center"
|
||||||
|
Foreground="{DynamicResource TextDimBrush}"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,1,0,0" Margin="0,4,0,0"/>
|
||||||
|
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<Button Content="Cleanup finished worktrees"
|
||||||
|
Command="{Binding CleanupWorktreesCommand}"
|
||||||
|
HorizontalAlignment="Left"/>
|
||||||
|
|
||||||
|
<!-- Force-remove: button vs. confirm bar -->
|
||||||
|
<StackPanel>
|
||||||
|
<Button Content="Force-remove all worktrees"
|
||||||
|
Classes="danger"
|
||||||
|
Command="{Binding RequestResetConfirmCommand}"
|
||||||
|
HorizontalAlignment="Left"
|
||||||
|
IsVisible="{Binding !ShowResetConfirm}"/>
|
||||||
|
|
||||||
|
<Border BorderBrush="{DynamicResource BloodBrush}" BorderThickness="1"
|
||||||
|
CornerRadius="6" Padding="12,10"
|
||||||
|
IsVisible="{Binding ShowResetConfirm}">
|
||||||
|
<StackPanel Spacing="8">
|
||||||
|
<TextBlock Text="Remove ALL worktrees? Uncommitted work will be lost."
|
||||||
|
Foreground="{DynamicResource TextBrush}" TextWrapping="Wrap"/>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||||
|
<Button Content="Cancel" Command="{Binding CancelResetConfirmCommand}"/>
|
||||||
|
<Button Content="Remove All" Classes="danger"
|
||||||
|
Command="{Binding ConfirmResetAllCommand}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- ABOUT -->
|
||||||
|
<StackPanel Spacing="0">
|
||||||
|
<TextBlock Classes="section-label" Text="ABOUT"/>
|
||||||
|
<Border Classes="section">
|
||||||
|
<Grid RowDefinitions="Auto,Auto,Auto,Auto" ColumnDefinitions="90,*,Auto" RowSpacing="8">
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="0" Classes="field-label" Text="Version"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Grid.Row="0" Grid.Column="1" Classes="path-mono"
|
||||||
|
Text="{Binding AppVersion}" VerticalAlignment="Center"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="0" Classes="field-label" Text="Data"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Grid.Row="1" Grid.Column="1" Classes="path-mono"
|
||||||
|
Text="{Binding DataFolderPath}" VerticalAlignment="Center"/>
|
||||||
|
<Button Grid.Row="1" Grid.Column="2" Content="Open"
|
||||||
|
Command="{Binding OpenPathCommand}"
|
||||||
|
CommandParameter="{Binding DataFolderPath}"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="0" Classes="field-label" Text="Logs"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Grid.Row="2" Grid.Column="1" Classes="path-mono"
|
||||||
|
Text="{Binding LogsFolderPath}" VerticalAlignment="Center"/>
|
||||||
|
<Button Grid.Row="2" Grid.Column="2" Content="Open"
|
||||||
|
Command="{Binding OpenPathCommand}"
|
||||||
|
CommandParameter="{Binding LogsFolderPath}"/>
|
||||||
|
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="0" Classes="field-label" Text="Config"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
<TextBlock Grid.Row="3" Grid.Column="1" Classes="path-mono"
|
||||||
|
Text="{Binding WorkerConfigPath}" VerticalAlignment="Center"/>
|
||||||
|
<Button Grid.Row="3" Grid.Column="2" Content="Open"
|
||||||
|
Command="{Binding OpenPathCommand}"
|
||||||
|
CommandParameter="{Binding WorkerConfigPath}"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Inline status / error -->
|
||||||
|
<TextBlock Text="{Binding ValidationError}"
|
||||||
|
Foreground="{DynamicResource BloodBrush}"
|
||||||
|
FontSize="11"
|
||||||
|
IsVisible="{Binding ValidationError, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
<TextBlock Text="{Binding StatusMessage}"
|
||||||
|
Foreground="{DynamicResource TextDimBrush}"
|
||||||
|
FontSize="11"
|
||||||
|
IsVisible="{Binding StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
|
||||||
|
<!-- Footer -->
|
||||||
|
<Border Grid.Row="2"
|
||||||
|
Background="{DynamicResource DeepBrush}"
|
||||||
|
BorderBrush="{DynamicResource LineBrush}"
|
||||||
|
BorderThickness="0,1,0,0">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8"
|
||||||
|
HorizontalAlignment="Right" VerticalAlignment="Center"
|
||||||
|
Margin="16,0">
|
||||||
|
<Button Content="Cancel" Command="{Binding CancelCommand}" MinWidth="90"/>
|
||||||
|
<Button Content="Save" Classes="primary"
|
||||||
|
Command="{Binding SaveCommand}"
|
||||||
|
IsEnabled="{Binding !IsBusy}" MinWidth="90"/>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Window>
|
||||||
26
src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml.cs
Normal file
26
src/ClaudeDo.Ui/Views/Modals/SettingsModalView.axaml.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Modals;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Views.Modals;
|
||||||
|
|
||||||
|
public partial class SettingsModalView : Window
|
||||||
|
{
|
||||||
|
public SettingsModalView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override void OnDataContextChanged(EventArgs e)
|
||||||
|
{
|
||||||
|
base.OnDataContextChanged(e);
|
||||||
|
if (DataContext is SettingsModalViewModel vm)
|
||||||
|
vm.CloseAction = Close;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||||
|
BeginMoveDrag(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,12 +1,28 @@
|
|||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using ClaudeDo.Data;
|
||||||
using ClaudeDo.Data.Models;
|
using ClaudeDo.Data.Models;
|
||||||
|
using ClaudeDo.Data.Repositories;
|
||||||
using ClaudeDo.Worker.Services;
|
using ClaudeDo.Worker.Services;
|
||||||
using Microsoft.AspNetCore.SignalR;
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
|
||||||
namespace ClaudeDo.Worker.Hub;
|
namespace ClaudeDo.Worker.Hub;
|
||||||
|
|
||||||
public record ActiveTaskDto(string Slot, string TaskId, DateTime StartedAt);
|
public record ActiveTaskDto(string Slot, string TaskId, DateTime StartedAt);
|
||||||
|
|
||||||
|
public record AppSettingsDto(
|
||||||
|
string DefaultClaudeInstructions,
|
||||||
|
string DefaultModel,
|
||||||
|
int DefaultMaxTurns,
|
||||||
|
string DefaultPermissionMode,
|
||||||
|
string WorktreeStrategy,
|
||||||
|
string? CentralWorktreeRoot,
|
||||||
|
bool WorktreeAutoCleanupEnabled,
|
||||||
|
int WorktreeAutoCleanupDays);
|
||||||
|
|
||||||
|
public record WorktreeCleanupDto(int Removed);
|
||||||
|
public record WorktreeResetDto(int Removed, int TasksAffected, bool Blocked, int RunningTasks);
|
||||||
|
|
||||||
public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||||
{
|
{
|
||||||
private static readonly string Version =
|
private static readonly string Version =
|
||||||
@@ -15,12 +31,21 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
private readonly QueueService _queue;
|
private readonly QueueService _queue;
|
||||||
private readonly AgentFileService _agentService;
|
private readonly AgentFileService _agentService;
|
||||||
private readonly HubBroadcaster _broadcaster;
|
private readonly HubBroadcaster _broadcaster;
|
||||||
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||||
|
private readonly WorktreeMaintenanceService _wtMaintenance;
|
||||||
|
|
||||||
public WorkerHub(QueueService queue, AgentFileService agentService, HubBroadcaster broadcaster)
|
public WorkerHub(
|
||||||
|
QueueService queue,
|
||||||
|
AgentFileService agentService,
|
||||||
|
HubBroadcaster broadcaster,
|
||||||
|
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||||
|
WorktreeMaintenanceService wtMaintenance)
|
||||||
{
|
{
|
||||||
_queue = queue;
|
_queue = queue;
|
||||||
_agentService = agentService;
|
_agentService = agentService;
|
||||||
_broadcaster = broadcaster;
|
_broadcaster = broadcaster;
|
||||||
|
_dbFactory = dbFactory;
|
||||||
|
_wtMaintenance = wtMaintenance;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string Ping() => $"pong v{Version}";
|
public string Ping() => $"pong v{Version}";
|
||||||
@@ -71,4 +96,49 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
public async Task<List<AgentInfo>> GetAgents() => await _agentService.ScanAsync();
|
public async Task<List<AgentInfo>> GetAgents() => await _agentService.ScanAsync();
|
||||||
|
|
||||||
public async Task RefreshAgents() => await _agentService.ScanAsync();
|
public async Task RefreshAgents() => await _agentService.ScanAsync();
|
||||||
|
|
||||||
|
public async Task<AppSettingsDto> GetAppSettings()
|
||||||
|
{
|
||||||
|
using var ctx = _dbFactory.CreateDbContext();
|
||||||
|
var row = await new AppSettingsRepository(ctx).GetAsync();
|
||||||
|
return new AppSettingsDto(
|
||||||
|
row.DefaultClaudeInstructions,
|
||||||
|
row.DefaultModel,
|
||||||
|
row.DefaultMaxTurns,
|
||||||
|
row.DefaultPermissionMode,
|
||||||
|
row.WorktreeStrategy,
|
||||||
|
row.CentralWorktreeRoot,
|
||||||
|
row.WorktreeAutoCleanupEnabled,
|
||||||
|
row.WorktreeAutoCleanupDays);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task UpdateAppSettings(AppSettingsDto dto)
|
||||||
|
{
|
||||||
|
using var ctx = _dbFactory.CreateDbContext();
|
||||||
|
var repo = new AppSettingsRepository(ctx);
|
||||||
|
await repo.UpdateAsync(new AppSettingsEntity
|
||||||
|
{
|
||||||
|
Id = AppSettingsEntity.SingletonId,
|
||||||
|
DefaultClaudeInstructions = dto.DefaultClaudeInstructions ?? "",
|
||||||
|
DefaultModel = dto.DefaultModel ?? "sonnet",
|
||||||
|
DefaultMaxTurns = dto.DefaultMaxTurns,
|
||||||
|
DefaultPermissionMode = dto.DefaultPermissionMode ?? "bypassPermissions",
|
||||||
|
WorktreeStrategy = dto.WorktreeStrategy ?? "sibling",
|
||||||
|
CentralWorktreeRoot = dto.CentralWorktreeRoot,
|
||||||
|
WorktreeAutoCleanupEnabled = dto.WorktreeAutoCleanupEnabled,
|
||||||
|
WorktreeAutoCleanupDays = dto.WorktreeAutoCleanupDays,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WorktreeCleanupDto> CleanupFinishedWorktrees()
|
||||||
|
{
|
||||||
|
var result = await _wtMaintenance.CleanupFinishedAsync();
|
||||||
|
return new WorktreeCleanupDto(result.Removed);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<WorktreeResetDto> ResetAllWorktrees()
|
||||||
|
{
|
||||||
|
var result = await _wtMaintenance.ResetAllAsync();
|
||||||
|
return new WorktreeResetDto(result.Removed, result.TasksAffected, result.Blocked, result.RunningTasks);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user