refactor(agent-config): single AgentConfigEditor for list + task scopes
This commit is contained in:
85
src/ClaudeDo.Ui/Views/Controls/AgentConfigEditor.axaml
Normal file
85
src/ClaudeDo.Ui/Views/Controls/AgentConfigEditor.axaml
Normal 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>
|
||||
75
src/ClaudeDo.Ui/Views/Controls/AgentConfigEditor.axaml.cs
Normal file
75
src/ClaudeDo.Ui/Views/Controls/AgentConfigEditor.axaml.cs
Normal 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, ""); }
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user