feat(ui): add settings modal and wire to worker hub
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels;
|
||||
using ClaudeDo.Ui.Views;
|
||||
using Microsoft.Extensions.DependencyInjection;
|
||||
@@ -24,6 +25,10 @@ public partial class App : Application
|
||||
{
|
||||
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();
|
||||
|
||||
@@ -77,9 +77,13 @@ sealed class Program
|
||||
|
||||
// ViewModels
|
||||
sc.AddTransient<WorktreeModalViewModel>();
|
||||
sc.AddTransient<SettingsModalViewModel>();
|
||||
|
||||
// Islands shell VMs
|
||||
sc.AddSingleton<ListsIslandViewModel>();
|
||||
sc.AddSingleton<ListsIslandViewModel>(sp =>
|
||||
new ListsIslandViewModel(
|
||||
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
|
||||
sp));
|
||||
sc.AddSingleton<TasksIslandViewModel>(sp =>
|
||||
new TasksIslandViewModel(sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>()));
|
||||
sc.AddSingleton<DetailsIslandViewModel>(sp =>
|
||||
|
||||
@@ -226,6 +226,47 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable
|
||||
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
|
||||
private sealed class ActiveTaskDto
|
||||
{
|
||||
@@ -234,3 +275,16 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable
|
||||
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.Input;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using ClaudeDo.Ui.ViewModels.Islands;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels;
|
||||
@@ -9,6 +10,14 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
|
||||
public ListsIslandViewModel Lists { get; }
|
||||
public TasksIslandViewModel Tasks { 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]
|
||||
private double _windowWidth = 1280;
|
||||
@@ -37,9 +46,10 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
|
||||
public IslandsShellViewModel(
|
||||
ListsIslandViewModel lists,
|
||||
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);
|
||||
Tasks.SelectionChanged += (_, _) => Details.Bind(Tasks.SelectedTask);
|
||||
Details.CloseDetail = () => Tasks.SelectedTask = null;
|
||||
@@ -48,6 +58,14 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
|
||||
Tasks.LoadForList(Lists.SelectedList);
|
||||
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();
|
||||
}
|
||||
}
|
||||
|
||||
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="Ctrl+N" Command="{Binding FocusAddTaskCommand}"/>
|
||||
</Window.KeyBindings>
|
||||
<Grid RowDefinitions="36,*">
|
||||
<Grid RowDefinitions="36,*,22">
|
||||
<!-- Custom title bar -->
|
||||
<Border Grid.Row="0"
|
||||
Background="{DynamicResource DeepBrush}"
|
||||
@@ -98,5 +98,38 @@
|
||||
<islands:DetailsIslandView DataContext="{Binding Details}"/>
|
||||
</Border>
|
||||
</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>
|
||||
</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 ClaudeDo.Data;
|
||||
using ClaudeDo.Data.Models;
|
||||
using ClaudeDo.Data.Repositories;
|
||||
using ClaudeDo.Worker.Services;
|
||||
using Microsoft.AspNetCore.SignalR;
|
||||
using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ClaudeDo.Worker.Hub;
|
||||
|
||||
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
|
||||
{
|
||||
private static readonly string Version =
|
||||
@@ -15,12 +31,21 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
||||
private readonly QueueService _queue;
|
||||
private readonly AgentFileService _agentService;
|
||||
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;
|
||||
_agentService = agentService;
|
||||
_broadcaster = broadcaster;
|
||||
_dbFactory = dbFactory;
|
||||
_wtMaintenance = wtMaintenance;
|
||||
}
|
||||
|
||||
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 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