refactor(agent-config): single AgentConfigEditor for list + task scopes

This commit is contained in:
Mika Kuns
2026-06-23 08:52:49 +02:00
parent 60eb671e8f
commit eb0ddb56d3
14 changed files with 761 additions and 482 deletions

View File

@@ -0,0 +1,85 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Agent"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
xmlns:loc="using:ClaudeDo.Ui.Localization"
x:Class="ClaudeDo.Ui.Views.Controls.AgentConfigEditor"
x:DataType="vm:AgentConfigEditorViewModel"
x:Name="Root">
<StackPanel Spacing="12">
<!-- Model -->
<StackPanel Spacing="4">
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr settings.agentEditor.model}" VerticalAlignment="Center"/>
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding ModelBadge}"/>
<Button Grid.Column="3" Classes="icon-btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
Command="{Binding ResetModelCommand}"/>
</Grid>
<ComboBox ItemsSource="{Binding ModelOptions}"
SelectedItem="{Binding Model, Mode=TwoWay}"
PlaceholderText="{Binding ModelInheritedHint}"
HorizontalAlignment="Stretch"/>
</StackPanel>
<!-- Max turns -->
<StackPanel Spacing="4">
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr settings.agentEditor.maxTurns}" VerticalAlignment="Center"/>
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding TurnsBadge}"/>
<Button Grid.Column="3" Classes="icon-btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
Command="{Binding ResetTurnsCommand}"/>
</Grid>
<NumericUpDown Value="{Binding MaxTurns, Mode=TwoWay}"
PlaceholderText="{Binding TurnsInheritedHint}"
Minimum="1" Maximum="200" Increment="1" FormatString="0"
HorizontalAlignment="Stretch"/>
</StackPanel>
<!-- System prompt -->
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="{loc:Tr settings.agentEditor.systemPrompt}"/>
<TextBox Text="{Binding SystemPrompt, Mode=TwoWay}"
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="80"/>
<TextBlock Classes="meta" Opacity="0.6"
Text="{loc:Tr settings.agentEditor.promptPrepended}"
IsVisible="{Binding EffectiveSystemPromptHint, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<TextBlock Classes="meta" Opacity="0.6" TextWrapping="Wrap"
Text="{Binding EffectiveSystemPromptHint}"
IsVisible="{Binding EffectiveSystemPromptHint, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<!-- Agent file -->
<StackPanel Spacing="4">
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr settings.agentEditor.agentFile}" VerticalAlignment="Center"/>
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding AgentBadge}"/>
<Button Grid.Column="3" Classes="icon-btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
Command="{Binding ResetAgentCommand}"/>
</Grid>
<Grid ColumnDefinitions="*,Auto">
<ComboBox Grid.Column="0"
ItemsSource="{Binding Agents}"
SelectedItem="{Binding SelectedAgent, Mode=TwoWay}"
HorizontalAlignment="Stretch">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Classes="title" Text="{Binding Name}"/>
<TextBlock Classes="meta" Text="{Binding Description}"
IsVisible="{Binding Description, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button Classes="btn" Grid.Column="1" Content="{loc:Tr settings.agentEditor.browse}"
Margin="8,0,0,0" Click="BrowseAgentClicked"
IsVisible="{Binding #Root.ShowAgentBrowse}"/>
</Grid>
<TextBlock Classes="path-mono" Text="{Binding SelectedAgent.Path}"
TextTrimming="PrefixCharacterEllipsis"
IsVisible="{Binding SelectedAgent.Path, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,75 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.ViewModels.Agent;
namespace ClaudeDo.Ui.Views.Controls;
public partial class AgentConfigEditor : UserControl
{
// List scope shows a file picker for ad-hoc agent files; the task flyout only
// picks from discovered agents, so it leaves this off (default).
public static readonly StyledProperty<bool> ShowAgentBrowseProperty =
AvaloniaProperty.Register<AgentConfigEditor, bool>(nameof(ShowAgentBrowse));
public bool ShowAgentBrowse
{
get => GetValue(ShowAgentBrowseProperty);
set => SetValue(ShowAgentBrowseProperty, value);
}
public AgentConfigEditor() => InitializeComponent();
private async void BrowseAgentClicked(object? sender, RoutedEventArgs e)
{
if (DataContext is not AgentConfigEditorViewModel vm) return;
var top = TopLevel.GetTopLevel(this);
if (top is null) return;
var files = await top.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Choose agent file",
AllowMultiple = false,
FileTypeFilter = new[]
{
new FilePickerFileType("Agent files (*.md)") { Patterns = new[] { "*.md" } },
new FilePickerFileType("All files") { Patterns = new[] { "*" } },
},
});
if (files.Count == 0) return;
var path = files[0].Path.LocalPath;
var existing = vm.Agents.FirstOrDefault(a => string.Equals(a.Path, path, StringComparison.OrdinalIgnoreCase));
if (existing is not null)
{
vm.SelectedAgent = existing;
return;
}
var (name, description) = ReadFrontmatter(path);
var agent = new AgentInfo(name, description, path);
vm.Agents.Add(agent);
vm.SelectedAgent = agent;
}
private static (string name, string description) ReadFrontmatter(string filePath)
{
var fallback = System.IO.Path.GetFileNameWithoutExtension(filePath);
try
{
using var reader = new System.IO.StreamReader(filePath);
if (reader.ReadLine()?.Trim() != "---") return (fallback, "");
string name = fallback, description = "";
while (reader.ReadLine() is { } line)
{
if (line.Trim() == "---") break;
if (line.StartsWith("name:")) name = line["name:".Length..].Trim();
else if (line.StartsWith("description:")) description = line["description:".Length..].Trim();
}
return (name, description);
}
catch { return (fallback, ""); }
}
}

View File

@@ -52,7 +52,7 @@
<!-- Column 2: gear button with agent settings flyout -->
<Button Grid.Column="2" Classes="icon-btn"
ToolTip.Tip="{loc:Tr details.agentSettingsTip}"
IsEnabled="{Binding AgentSettings.IsAgentSectionEnabled}"
IsEnabled="{Binding AgentSettings.IsEnabled}"
VerticalAlignment="Top"
Margin="6,0,0,0">
<TextBlock Text="⚙" FontSize="{StaticResource FontSizeTaskTitle}"/>
@@ -60,62 +60,7 @@
<Flyout Placement="BottomEdgeAlignedRight" ShowMode="Standard">
<StackPanel Width="340" Spacing="10" Margin="4">
<TextBlock Text="{loc:Tr details.agentSettingsHeading}" FontWeight="SemiBold"/>
<StackPanel Spacing="2">
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr details.modelLabel}" VerticalAlignment="Center"/>
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding AgentSettings.ModelBadge}"/>
<Button Grid.Column="3" Classes="icon-btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
Command="{Binding AgentSettings.ResetTaskModelCommand}"/>
</Grid>
<ComboBox ItemsSource="{Binding AgentSettings.TaskModelOptions}"
SelectedItem="{Binding AgentSettings.TaskModelSelection, Mode=TwoWay}"
PlaceholderText="{Binding AgentSettings.ModelInheritedHint}"
HorizontalAlignment="Stretch"/>
</StackPanel>
<StackPanel Spacing="2">
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr details.maxTurnsLabel}" VerticalAlignment="Center"/>
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding AgentSettings.TurnsBadge}"/>
<Button Grid.Column="3" Classes="icon-btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
Command="{Binding AgentSettings.ResetTaskTurnsCommand}"/>
</Grid>
<NumericUpDown Value="{Binding AgentSettings.TaskMaxTurns, Mode=TwoWay}"
PlaceholderText="{Binding AgentSettings.TurnsInheritedHint}"
Minimum="1" Maximum="200" Increment="1" FormatString="0"
HorizontalAlignment="Stretch"/>
</StackPanel>
<StackPanel Spacing="2">
<TextBlock Classes="field-label" Text="{loc:Tr details.systemPromptLabel}"/>
<TextBox Text="{Binding AgentSettings.TaskSystemPrompt, Mode=TwoWay}"
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="70"/>
<TextBlock Classes="meta" Opacity="0.6"
Text="{loc:Tr details.systemPromptPrepended}"
IsVisible="{Binding AgentSettings.EffectiveSystemPromptHint, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
<TextBlock Classes="meta" Opacity="0.6" TextWrapping="Wrap"
Text="{Binding AgentSettings.EffectiveSystemPromptHint}"
IsVisible="{Binding AgentSettings.EffectiveSystemPromptHint, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
<StackPanel Spacing="2">
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr details.agentFileLabel}" VerticalAlignment="Center"/>
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding AgentSettings.AgentBadge}"/>
<Button Grid.Column="3" Classes="icon-btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
Command="{Binding AgentSettings.ResetTaskAgentCommand}"/>
</Grid>
<ComboBox ItemsSource="{Binding AgentSettings.TaskAgentOptions}"
SelectedItem="{Binding AgentSettings.TaskSelectedAgent, Mode=TwoWay}"
HorizontalAlignment="Stretch">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
</StackPanel>
<ctl:AgentConfigEditor DataContext="{Binding AgentSettings}"/>
</StackPanel>
</Flyout>
</Button.Flyout>

View File

@@ -69,72 +69,10 @@
<Grid ColumnDefinitions="*,Auto" Margin="4,0,0,6">
<TextBlock Classes="section-label" Text="{loc:Tr modals.listSettings.sectionAgent}" Margin="0"/>
<Button Classes="btn" Grid.Column="1" Content="{loc:Tr modals.listSettings.resetAgentSettings}"
Command="{Binding ResetAgentSettingsCommand}" />
Command="{Binding Agent.ResetAllCommand}" />
</Grid>
<Border Classes="section">
<StackPanel Spacing="12">
<StackPanel Spacing="4">
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr modals.listSettings.model}" VerticalAlignment="Center"/>
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding ModelBadge}"/>
<Button Grid.Column="3" Classes="btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
Command="{Binding ResetModelCommand}" Padding="6,1"/>
</Grid>
<ComboBox ItemsSource="{Binding ModelOptions}"
SelectedItem="{Binding SelectedModel, Mode=TwoWay}"
PlaceholderText="{Binding ModelInheritedHint}"
HorizontalAlignment="Left" MinWidth="160" />
</StackPanel>
<StackPanel Spacing="4">
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr modals.listSettings.maxTurns}" VerticalAlignment="Center"/>
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding TurnsBadge}"/>
<Button Grid.Column="3" Classes="btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
Command="{Binding ResetTurnsCommand}" Padding="6,1"/>
</Grid>
<NumericUpDown Value="{Binding MaxTurns, Mode=TwoWay}"
PlaceholderText="{Binding TurnsInheritedHint}"
Minimum="1" Maximum="200" Increment="1" FormatString="0"
HorizontalAlignment="Left" Width="160"/>
</StackPanel>
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="{loc:Tr modals.listSettings.systemPrompt}"/>
<TextBox Text="{Binding SystemPrompt, Mode=TwoWay}"
AcceptsReturn="True" TextWrapping="Wrap"
MinHeight="80" />
</StackPanel>
<StackPanel Spacing="4">
<Grid ColumnDefinitions="Auto,Auto,*,Auto" ColumnSpacing="6">
<TextBlock Grid.Column="0" Classes="field-label" Text="{loc:Tr modals.listSettings.agentFile}" VerticalAlignment="Center"/>
<ctl:InheritedBadge Grid.Column="1" BadgeText="{Binding AgentBadge}"/>
<Button Grid.Column="3" Classes="btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
Command="{Binding ResetAgentCommand}" Padding="6,1"/>
</Grid>
<Grid ColumnDefinitions="*,Auto">
<ComboBox Grid.Column="0"
ItemsSource="{Binding Agents}"
SelectedItem="{Binding SelectedAgent, Mode=TwoWay}"
HorizontalAlignment="Stretch">
<ComboBox.ItemTemplate>
<DataTemplate>
<StackPanel>
<TextBlock Classes="title" Text="{Binding Name}"/>
<TextBlock Classes="meta" Text="{Binding Description}"/>
</StackPanel>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<Button Classes="btn" Grid.Column="1" Content="{loc:Tr modals.listSettings.browse}"
Margin="8,0,0,0" Click="BrowseAgentClicked" />
</Grid>
<TextBlock Classes="path-mono" Text="{Binding SelectedAgent.Path}"
TextTrimming="PrefixCharacterEllipsis"
IsVisible="{Binding SelectedAgent.Path, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"/>
</StackPanel>
</StackPanel>
<ctl:AgentConfigEditor DataContext="{Binding Agent}" ShowAgentBrowse="True"/>
</Border>
</StackPanel>

View File

@@ -1,7 +1,6 @@
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.ViewModels.Modals;
namespace ClaudeDo.Ui.Views.Modals;
@@ -13,57 +12,6 @@ public partial class ListSettingsModalView : Window
InitializeComponent();
}
private async void BrowseAgentClicked(object? sender, RoutedEventArgs e)
{
if (DataContext is not ListSettingsModalViewModel vm) return;
var top = TopLevel.GetTopLevel(this);
if (top is null) return;
var files = await top.StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
{
Title = "Choose agent file",
AllowMultiple = false,
FileTypeFilter = new[]
{
new FilePickerFileType("Agent files (*.md)") { Patterns = new[] { "*.md" } },
new FilePickerFileType("All files") { Patterns = new[] { "*" } },
},
});
if (files.Count == 0) return;
var path = files[0].Path.LocalPath;
var existing = vm.Agents.FirstOrDefault(a => string.Equals(a.Path, path, StringComparison.OrdinalIgnoreCase));
if (existing is not null)
{
vm.SelectedAgent = existing;
return;
}
var (name, description) = ReadFrontmatter(path);
var agent = new AgentInfo(name, description, path);
vm.Agents.Add(agent);
vm.SelectedAgent = agent;
}
private static (string name, string description) ReadFrontmatter(string filePath)
{
var fallback = System.IO.Path.GetFileNameWithoutExtension(filePath);
try
{
using var reader = new System.IO.StreamReader(filePath);
if (reader.ReadLine()?.Trim() != "---") return (fallback, "");
string name = fallback, description = "";
while (reader.ReadLine() is { } line)
{
if (line.Trim() == "---") break;
if (line.StartsWith("name:")) name = line["name:".Length..].Trim();
else if (line.StartsWith("description:")) description = line["description:".Length..].Trim();
}
return (name, description);
}
catch { return (fallback, ""); }
}
private async void BrowseClicked(object? sender, RoutedEventArgs e)
{
if (DataContext is not ListSettingsModalViewModel vm) return;