feat(ui): refactor Settings to TabControl + add Prime Claude tab

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mika Kuns
2026-04-28 09:22:16 +02:00
parent 8b02b63d3d
commit bca8c9e4cb
3 changed files with 179 additions and 160 deletions

View File

@@ -0,0 +1,23 @@
using System.Globalization;
using Avalonia.Data.Converters;
namespace ClaudeDo.Ui.Converters;
public sealed class DateOnlyToDateTimeConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is DateOnly d)
return d.ToDateTime(TimeOnly.MinValue);
return null;
}
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is DateTime dt)
return DateOnly.FromDateTime(dt);
if (value is DateTimeOffset dto)
return DateOnly.FromDateTime(dto.LocalDateTime);
return DateOnly.FromDateTime(DateTime.Today);
}
}

View File

@@ -0,0 +1,21 @@
using System.Globalization;
using Avalonia.Data.Converters;
namespace ClaudeDo.Ui.Converters;
public sealed class TimeSpanToHhmmConverter : IValueConverter
{
public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) =>
value is TimeSpan t ? $"{t.Hours:00}:{t.Minutes:00}" : "07:00";
public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture)
{
if (value is not string s) return new TimeSpan(7, 0, 0);
var parts = s.Split(':');
if (parts.Length == 2 &&
int.TryParse(parts[0], out var h) && h is >= 0 and <= 23 &&
int.TryParse(parts[1], out var m) && m is >= 0 and <= 59)
return new TimeSpan(h, m, 0);
return new TimeSpan(7, 0, 0);
}
}

View File

@@ -1,8 +1,10 @@
<Window xmlns="https://github.com/avaloniaui" <Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals" xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
xmlns:settings="using:ClaudeDo.Ui.ViewModels.Modals.Settings"
xmlns:conv="using:ClaudeDo.Ui.Converters"
x:Class="ClaudeDo.Ui.Views.Modals.SettingsModalView" x:Class="ClaudeDo.Ui.Views.Modals.SettingsModalView"
x:CompileBindings="False" x:DataType="vm:SettingsModalViewModel"
Title="Settings" Title="Settings"
Width="580" Height="760" Width="580" Height="760"
SystemDecorations="None" SystemDecorations="None"
@@ -14,6 +16,11 @@
<KeyBinding Gesture="Escape" Command="{Binding CancelCommand}"/> <KeyBinding Gesture="Escape" Command="{Binding CancelCommand}"/>
</Window.KeyBindings> </Window.KeyBindings>
<Window.Resources>
<conv:DateOnlyToDateTimeConverter x:Key="DateOnlyToDateTime"/>
<conv:TimeSpanToHhmmConverter x:Key="TimeSpanToHhmm"/>
</Window.Resources>
<Window.Styles> <Window.Styles>
<Style Selector="TextBlock.section-label"> <Style Selector="TextBlock.section-label">
<Setter Property="FontFamily" Value="{DynamicResource MonoFont}"/> <Setter Property="FontFamily" Value="{DynamicResource MonoFont}"/>
@@ -33,13 +40,6 @@
<Setter Property="Foreground" Value="{DynamicResource TextDimBrush}"/> <Setter Property="Foreground" Value="{DynamicResource TextDimBrush}"/>
<Setter Property="TextTrimming" Value="CharacterEllipsis"/> <Setter Property="TextTrimming" Value="CharacterEllipsis"/>
</Style> </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"> <Style Selector="Button.danger">
<Setter Property="Background" Value="{DynamicResource BloodBrush}"/> <Setter Property="Background" Value="{DynamicResource BloodBrush}"/>
<Setter Property="Foreground" Value="White"/> <Setter Property="Foreground" Value="White"/>
@@ -57,8 +57,7 @@
<Grid RowDefinitions="36,*,52"> <Grid RowDefinitions="36,*,52">
<!-- Title bar --> <!-- Title bar -->
<Border Grid.Row="0" <Border Grid.Row="0" x:Name="TitleBar"
x:Name="TitleBar"
Background="{DynamicResource DeepBrush}" Background="{DynamicResource DeepBrush}"
BorderBrush="{DynamicResource LineBrush}" BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,0,0,1" BorderThickness="0,0,0,1"
@@ -70,217 +69,193 @@
LetterSpacing="1.4" LetterSpacing="1.4"
Foreground="{DynamicResource TextBrush}" Foreground="{DynamicResource TextBrush}"
VerticalAlignment="Center"/> VerticalAlignment="Center"/>
<Button Grid.Column="1" <Button Grid.Column="1" Classes="icon-btn" Content="✕" FontSize="12"
Classes="icon-btn" Command="{Binding CancelCommand}" VerticalAlignment="Center"/>
Content="✕"
FontSize="12"
Command="{Binding CancelCommand}"
VerticalAlignment="Center"/>
</Grid> </Grid>
</Border> </Border>
<!-- Scrollable body --> <!-- Body: tabs + bottom validation/status strip -->
<ScrollViewer Grid.Row="1" Padding="20,16"> <DockPanel Grid.Row="1">
<StackPanel Spacing="18"> <StackPanel DockPanel.Dock="Bottom" Margin="20,0,20,8" Spacing="2">
<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>
<!-- CLAUDE DEFAULTS --> <TabControl Padding="20,12" TabStripPlacement="Top">
<StackPanel Spacing="0">
<TextBlock Classes="section-label" Text="CLAUDE DEFAULTS"/> <TabItem Header="General">
<Border Classes="section"> <ScrollViewer>
<StackPanel Spacing="12"> <StackPanel Spacing="12" Margin="0,8,0,0">
<StackPanel Spacing="4"> <StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="Default instructions"/> <TextBlock Classes="field-label" Text="Default instructions"/>
<TextBox AcceptsReturn="True" <TextBox AcceptsReturn="True" TextWrapping="Wrap" Height="110"
TextWrapping="Wrap" Watermark="Baseline instructions applied to every task"
Height="110" Text="{Binding General.DefaultClaudeInstructions, Mode=TwoWay}"/>
Watermark="Baseline instructions applied to every task (e.g. 'speak German', 'never touch .env')"
Text="{Binding DefaultClaudeInstructions, Mode=TwoWay}"/>
</StackPanel> </StackPanel>
<Grid ColumnDefinitions="*,12,*,12,*"> <Grid ColumnDefinitions="*,12,*,12,*">
<StackPanel Grid.Column="0" Spacing="4"> <StackPanel Grid.Column="0" Spacing="4">
<TextBlock Classes="field-label" Text="Model"/> <TextBlock Classes="field-label" Text="Model"/>
<ComboBox ItemsSource="{Binding Models}" <ComboBox ItemsSource="{Binding General.Models}"
SelectedItem="{Binding DefaultModel, Mode=TwoWay}" SelectedItem="{Binding General.DefaultModel, Mode=TwoWay}"
HorizontalAlignment="Stretch"/> HorizontalAlignment="Stretch"/>
</StackPanel> </StackPanel>
<StackPanel Grid.Column="2" Spacing="4"> <StackPanel Grid.Column="2" Spacing="4">
<TextBlock Classes="field-label" Text="Max turns"/> <TextBlock Classes="field-label" Text="Max turns"/>
<NumericUpDown Value="{Binding DefaultMaxTurns, Mode=TwoWay}" <NumericUpDown Value="{Binding General.DefaultMaxTurns, Mode=TwoWay}"
Minimum="1" Maximum="200" Increment="1" FormatString="0"/> Minimum="1" Maximum="200" Increment="1" FormatString="0"/>
</StackPanel> </StackPanel>
<StackPanel Grid.Column="4" Spacing="4"> <StackPanel Grid.Column="4" Spacing="4">
<TextBlock Classes="field-label" Text="Permission"/> <TextBlock Classes="field-label" Text="Permission"/>
<ComboBox ItemsSource="{Binding PermissionModes}" <ComboBox ItemsSource="{Binding General.PermissionModes}"
SelectedItem="{Binding DefaultPermissionMode, Mode=TwoWay}" SelectedItem="{Binding General.DefaultPermissionMode, Mode=TwoWay}"
HorizontalAlignment="Stretch"/> HorizontalAlignment="Stretch"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
</StackPanel> </StackPanel>
</Border> </ScrollViewer>
</StackPanel> </TabItem>
<!-- WORKTREES --> <TabItem Header="Worktrees">
<StackPanel Spacing="0"> <ScrollViewer>
<TextBlock Classes="section-label" Text="WORKTREES"/> <StackPanel Spacing="12" Margin="0,8,0,0">
<Border Classes="section">
<StackPanel Spacing="12">
<Grid ColumnDefinitions="*,12,2*"> <Grid ColumnDefinitions="*,12,2*">
<StackPanel Grid.Column="0" Spacing="4"> <StackPanel Grid.Column="0" Spacing="4">
<TextBlock Classes="field-label" Text="Strategy"/> <TextBlock Classes="field-label" Text="Strategy"/>
<ComboBox ItemsSource="{Binding WorktreeStrategies}" <ComboBox ItemsSource="{Binding Worktrees.WorktreeStrategies}"
SelectedItem="{Binding WorktreeStrategy, Mode=TwoWay}" SelectedItem="{Binding Worktrees.WorktreeStrategy, Mode=TwoWay}"
HorizontalAlignment="Stretch"/> HorizontalAlignment="Stretch"/>
</StackPanel> </StackPanel>
<StackPanel Grid.Column="2" Spacing="4"> <StackPanel Grid.Column="2" Spacing="4">
<TextBlock Classes="field-label" Text="Central worktree root"/> <TextBlock Classes="field-label" Text="Central worktree root"/>
<TextBox Text="{Binding CentralWorktreeRoot, Mode=TwoWay}" <TextBox Text="{Binding Worktrees.CentralWorktreeRoot, Mode=TwoWay}"
Watermark="e.g. C:\worktrees"/> Watermark="e.g. C:\worktrees"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
<StackPanel Orientation="Horizontal" Spacing="8"> <StackPanel Orientation="Horizontal" Spacing="8">
<CheckBox IsChecked="{Binding WorktreeAutoCleanupEnabled, Mode=TwoWay}" <CheckBox IsChecked="{Binding Worktrees.WorktreeAutoCleanupEnabled, Mode=TwoWay}"
Content="Auto-cleanup finished worktrees after" Content="Auto-cleanup finished worktrees after"
VerticalAlignment="Center"/> VerticalAlignment="Center"/>
<NumericUpDown Value="{Binding WorktreeAutoCleanupDays, Mode=TwoWay}" <NumericUpDown Value="{Binding Worktrees.WorktreeAutoCleanupDays, Mode=TwoWay}"
Width="130" Minimum="1" Maximum="365" Increment="1" FormatString="0" Width="130" Minimum="1" Maximum="365" Increment="1" FormatString="0"
IsEnabled="{Binding WorktreeAutoCleanupEnabled}"/> IsEnabled="{Binding Worktrees.WorktreeAutoCleanupEnabled}"/>
<TextBlock Text="days" VerticalAlignment="Center" <TextBlock Text="days" VerticalAlignment="Center" Foreground="{DynamicResource TextDimBrush}"/>
Foreground="{DynamicResource TextDimBrush}"/>
</StackPanel> </StackPanel>
<Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,1,0,0" Margin="0,4,0,0"/> <Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,1,0,0" Margin="0,4,0,0"/>
<StackPanel Spacing="8"> <StackPanel Spacing="8">
<Button Content="Cleanup finished worktrees" <Button Content="Cleanup finished worktrees"
Command="{Binding CleanupWorktreesCommand}" Command="{Binding Worktrees.CleanupWorktreesCommand}"
HorizontalAlignment="Left"/> HorizontalAlignment="Left"/>
<!-- Force-remove: button vs. confirm bar -->
<StackPanel> <StackPanel>
<Button Content="Force-remove all worktrees" <Button Content="Force-remove all worktrees" Classes="danger"
Classes="danger" Command="{Binding Worktrees.RequestResetConfirmCommand}"
Command="{Binding RequestResetConfirmCommand}"
HorizontalAlignment="Left" HorizontalAlignment="Left"
IsVisible="{Binding !ShowResetConfirm}"/> IsVisible="{Binding !Worktrees.ShowResetConfirm}"/>
<Border BorderBrush="{DynamicResource BloodBrush}" BorderThickness="1" <Border BorderBrush="{DynamicResource BloodBrush}" BorderThickness="1"
CornerRadius="6" Padding="12,10" CornerRadius="6" Padding="12,10"
IsVisible="{Binding ShowResetConfirm}"> IsVisible="{Binding Worktrees.ShowResetConfirm}">
<StackPanel Spacing="8"> <StackPanel Spacing="8">
<TextBlock Text="Remove ALL worktrees? Uncommitted work will be lost." <TextBlock Text="Remove ALL worktrees? Uncommitted work will be lost."
Foreground="{DynamicResource TextBrush}" TextWrapping="Wrap"/> Foreground="{DynamicResource TextBrush}" TextWrapping="Wrap"/>
<StackPanel Orientation="Horizontal" Spacing="8"> <StackPanel Orientation="Horizontal" Spacing="8">
<Button Content="Cancel" Command="{Binding CancelResetConfirmCommand}"/> <Button Content="Cancel" Command="{Binding Worktrees.CancelResetConfirmCommand}"/>
<Button Content="Remove All" Classes="danger" <Button Content="Remove All" Classes="danger"
Command="{Binding ConfirmResetAllCommand}"/> Command="{Binding Worktrees.ConfirmResetAllCommand}"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</Border> </Border>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
<TextBlock Text="{Binding Worktrees.StatusMessage}"
Foreground="{DynamicResource TextDimBrush}" FontSize="11"
IsVisible="{Binding Worktrees.StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel> </StackPanel>
</Border> </ScrollViewer>
</StackPanel> </TabItem>
<!-- AGENTS --> <TabItem Header="Files">
<StackPanel Spacing="0"> <ScrollViewer>
<TextBlock Classes="section-label" Text="AGENTS"/> <StackPanel Spacing="14" Margin="0,8,0,0">
<Border Classes="section"> <StackPanel Spacing="6">
<StackPanel Spacing="8"> <TextBlock Classes="section-label" Text="AGENTS"/>
<TextBlock Text="Restore bundled default agents (code-reviewer, test-writer, debugger, security-reviewer, explorer, researcher). Existing files are not overwritten." <TextBlock Text="Restore bundled default agents. Existing files are not overwritten."
FontSize="11" FontSize="11" TextWrapping="Wrap"
TextWrapping="Wrap" Foreground="{DynamicResource TextDimBrush}"/>
Foreground="{DynamicResource TextDimBrush}"/> <Button Content="Restore default agents"
<Button Content="Restore default agents" Command="{Binding Files.RestoreDefaultAgentsCommand}"
Command="{Binding RestoreDefaultAgentsCommand}" IsEnabled="{Binding !Files.IsBusy}"
IsEnabled="{Binding !IsBusy}" HorizontalAlignment="Left"/>
HorizontalAlignment="Left"/> </StackPanel>
<StackPanel Spacing="6">
<TextBlock Classes="section-label" Text="PROMPTS"/>
<Grid RowDefinitions="Auto,Auto,Auto" ColumnDefinitions="90,*,Auto" RowSpacing="8">
<TextBlock Grid.Row="0" Grid.Column="0" Classes="field-label" Text="System" VerticalAlignment="Center"/>
<TextBlock Grid.Row="0" Grid.Column="1" Classes="path-mono" Text="{Binding Files.SystemPromptPath}" VerticalAlignment="Center"/>
<Button Grid.Row="0" Grid.Column="2" Content="Open in editor"
Command="{Binding Files.OpenPromptCommand}" CommandParameter="System"/>
<TextBlock Grid.Row="1" Grid.Column="0" Classes="field-label" Text="Planning" VerticalAlignment="Center"/>
<TextBlock Grid.Row="1" Grid.Column="1" Classes="path-mono" Text="{Binding Files.PlanningPromptPath}" VerticalAlignment="Center"/>
<Button Grid.Row="1" Grid.Column="2" Content="Open in editor"
Command="{Binding Files.OpenPromptCommand}" CommandParameter="Planning"/>
<TextBlock Grid.Row="2" Grid.Column="0" Classes="field-label" Text="Agent" VerticalAlignment="Center"/>
<TextBlock Grid.Row="2" Grid.Column="1" Classes="path-mono" Text="{Binding Files.AgentPromptPath}" VerticalAlignment="Center"/>
<Button Grid.Row="2" Grid.Column="2" Content="Open in editor"
Command="{Binding Files.OpenPromptCommand}" CommandParameter="Agent"/>
</Grid>
</StackPanel>
<TextBlock Text="{Binding Files.StatusMessage}"
Foreground="{DynamicResource TextDimBrush}" FontSize="11"
IsVisible="{Binding Files.StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel> </StackPanel>
</Border> </ScrollViewer>
</StackPanel> </TabItem>
<!-- PROMPTS --> <TabItem Header="Prime Claude">
<StackPanel Spacing="0"> <ScrollViewer>
<TextBlock Classes="section-label" Text="PROMPTS"/> <StackPanel Spacing="12" Margin="0,8,0,0">
<Border Classes="section"> <TextBlock TextWrapping="Wrap" FontSize="11"
<Grid RowDefinitions="Auto,Auto,Auto" ColumnDefinitions="90,*,Auto" RowSpacing="8"> Foreground="{DynamicResource TextDimBrush}"
<TextBlock Grid.Row="0" Grid.Column="0" Classes="field-label" Text="System" Text="Prime your Claude usage window each morning by firing a single non-interactive ping at a chosen time. Only runs while ClaudeDo is open. If the app starts within 30 minutes of the target time, the ping fires immediately."/>
VerticalAlignment="Center"/> <ItemsControl ItemsSource="{Binding Prime.Rows}">
<TextBlock Grid.Row="0" Grid.Column="1" Classes="path-mono" <ItemsControl.ItemTemplate>
Text="{Binding SystemPromptPath}" VerticalAlignment="Center"/> <DataTemplate x:DataType="settings:PrimeScheduleRowViewModel">
<Button Grid.Row="0" Grid.Column="2" Content="Open in editor" <Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="1"
Command="{Binding OpenPromptCommand}" CornerRadius="6" Padding="10,8" Margin="0,0,0,8"
CommandParameter="System"/> Background="{DynamicResource DeepBrush}">
<Grid ColumnDefinitions="Auto,*,*,Auto,Auto,Auto,Auto" ColumnSpacing="8">
<CheckBox Grid.Column="0" IsChecked="{Binding Enabled, Mode=TwoWay}" VerticalAlignment="Center"/>
<CalendarDatePicker Grid.Column="1"
SelectedDate="{Binding StartDate, Mode=TwoWay, Converter={StaticResource DateOnlyToDateTime}}"
VerticalAlignment="Center"/>
<CalendarDatePicker Grid.Column="2"
SelectedDate="{Binding EndDate, Mode=TwoWay, Converter={StaticResource DateOnlyToDateTime}}"
VerticalAlignment="Center"/>
<TextBox Grid.Column="3" Width="64"
Text="{Binding TimeOfDay, Mode=TwoWay, Converter={StaticResource TimeSpanToHhmm}}"
VerticalAlignment="Center"/>
<CheckBox Grid.Column="4" Content="MonFri"
IsChecked="{Binding WorkdaysOnly, Mode=TwoWay}" VerticalAlignment="Center"/>
<TextBlock Grid.Column="5" Text="{Binding LastRunLabel}" VerticalAlignment="Center"
Foreground="{DynamicResource TextDimBrush}" FontSize="11"
MinWidth="80"/>
<Button Grid.Column="6" Content="✕"
Command="{Binding $parent[ItemsControl].((vm:SettingsModalViewModel)DataContext).Prime.RemoveScheduleCommand}"
CommandParameter="{Binding}"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<Button Content="+ Add schedule" Command="{Binding Prime.AddScheduleCommand}" HorizontalAlignment="Left"/>
</StackPanel>
</ScrollViewer>
</TabItem>
<TextBlock Grid.Row="1" Grid.Column="0" Classes="field-label" Text="Planning" </TabControl>
VerticalAlignment="Center"/> </DockPanel>
<TextBlock Grid.Row="1" Grid.Column="1" Classes="path-mono"
Text="{Binding PlanningPromptPath}" VerticalAlignment="Center"/>
<Button Grid.Row="1" Grid.Column="2" Content="Open in editor"
Command="{Binding OpenPromptCommand}"
CommandParameter="Planning"/>
<TextBlock Grid.Row="2" Grid.Column="0" Classes="field-label" Text="Agent"
VerticalAlignment="Center"/>
<TextBlock Grid.Row="2" Grid.Column="1" Classes="path-mono"
Text="{Binding AgentPromptPath}" VerticalAlignment="Center"/>
<Button Grid.Row="2" Grid.Column="2" Content="Open in editor"
Command="{Binding OpenPromptCommand}"
CommandParameter="Agent"/>
</Grid>
</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 --> <!-- Footer -->
<Border Grid.Row="2" <Border Grid.Row="2"