feat(ui): show inherited markers and max-turns override in task flyout

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-04 12:37:45 +02:00
parent d0ab382973
commit cd683ba227
2 changed files with 106 additions and 37 deletions

View File

@@ -138,28 +138,60 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
OnPropertyChanged(nameof(ShowContinue)); OnPropertyChanged(nameof(ShowContinue));
OnPropertyChanged(nameof(ShowResetAndRetry)); OnPropertyChanged(nameof(ShowResetAndRetry));
OnPropertyChanged(nameof(IsAgentSectionEnabled)); OnPropertyChanged(nameof(IsAgentSectionEnabled));
OnPropertyChanged(nameof(EffectiveModelLabel));
OnPropertyChanged(nameof(EffectiveAgentLabel));
} }
[ObservableProperty] private string? _model; [ObservableProperty] private string? _model;
// Agent settings overrides // Agent settings overrides
[ObservableProperty] private string _taskModelSelection = ModelRegistry.TaskInheritSentinel; [ObservableProperty] private string? _taskModelSelection; // null = inherit
[ObservableProperty] private string _taskSystemPrompt = ""; [ObservableProperty] private string _taskSystemPrompt = "";
[ObservableProperty] private AgentInfo? _taskSelectedAgent; [ObservableProperty] private AgentInfo? _taskSelectedAgent;
[ObservableProperty] private decimal? _taskMaxTurns; // null = inherit
[ObservableProperty] private string _effectiveModelHint = ""; [ObservableProperty] private string _modelBadge = "";
[ObservableProperty] private string _modelInheritedHint = "";
[ObservableProperty] private string _turnsBadge = "";
[ObservableProperty] private string _turnsInheritedHint = "";
[ObservableProperty] private string _agentBadge = "";
[ObservableProperty] private string _effectiveSystemPromptHint = ""; [ObservableProperty] private string _effectiveSystemPromptHint = "";
[ObservableProperty] private string _effectiveAgentHint = "";
public string EffectiveModelLabel => Loc.T("vm.details.effectiveIfInherited", EffectiveModelHint); private string _globalModel = ModelRegistry.DefaultAlias;
public string EffectiveAgentLabel => Loc.T("vm.details.effectiveIfInherited", EffectiveAgentHint); private int _globalMaxTurns = 100;
private string? _listModel;
private int? _listMaxTurns;
private string? _listAgentName;
partial void OnEffectiveModelHintChanged(string value) => OnPropertyChanged(nameof(EffectiveModelLabel)); partial void OnTaskModelSelectionChanged(string? value) { RecomputeModelBadge(); QueueAgentSave(); }
partial void OnEffectiveAgentHintChanged(string value) => OnPropertyChanged(nameof(EffectiveAgentLabel)); partial void OnTaskMaxTurnsChanged(decimal? value) { RecomputeTurnsBadge(); QueueAgentSave(); }
public System.Collections.ObjectModel.ObservableCollection<string> TaskModelOptions { get; } = new( private void RecomputeModelBadge()
new[] { ModelRegistry.TaskInheritSentinel }.Concat(ModelRegistry.Aliases)); {
var (value, source) = InheritanceResolver.Resolve(TaskModelSelection, _listModel, _globalModel);
ModelInheritedHint = value;
ModelBadge = BadgeFor(source, !string.IsNullOrWhiteSpace(TaskModelSelection));
}
private void RecomputeTurnsBadge()
{
var (value, source) = InheritanceResolver.Resolve(
TaskMaxTurns?.ToString(), _listMaxTurns?.ToString(), _globalMaxTurns.ToString());
TurnsInheritedHint = value;
TurnsBadge = BadgeFor(source, TaskMaxTurns is not null);
}
private void RecomputeAgentBadge()
{
var taskSet = TaskSelectedAgent is not null && !string.IsNullOrWhiteSpace(TaskSelectedAgent.Path);
var (_, source) = InheritanceResolver.Resolve(
taskSet ? TaskSelectedAgent!.Path : null, _listAgentName, null);
AgentBadge = BadgeFor(source, taskSet);
}
private static string BadgeFor(InheritSource source, bool taskSet) => taskSet
? Loc.T("settings.inherit.overrideBadge")
: source == InheritSource.List
? Loc.T("settings.inherit.inheritedFromList")
: Loc.T("settings.inherit.inheritedFromGlobal");
public System.Collections.ObjectModel.ObservableCollection<string> TaskModelOptions { get; } = new(ModelRegistry.Aliases);
public System.Collections.ObjectModel.ObservableCollection<AgentInfo> TaskAgentOptions { get; } = new(); public System.Collections.ObjectModel.ObservableCollection<AgentInfo> TaskAgentOptions { get; } = new();
@@ -288,8 +320,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
Loc.LanguageChanged += (_, _) => Loc.LanguageChanged += (_, _) =>
{ {
OnPropertyChanged(nameof(AgentStatusLabel)); OnPropertyChanged(nameof(AgentStatusLabel));
OnPropertyChanged(nameof(EffectiveModelLabel)); RecomputeModelBadge();
OnPropertyChanged(nameof(EffectiveAgentLabel)); RecomputeTurnsBadge();
RecomputeAgentBadge();
}; };
// Subscribe once; filter by current task id inside the handler // Subscribe once; filter by current task id inside the handler
@@ -433,9 +466,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
Log.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece }); Log.Add(new LogLineViewModel { Kind = LogKind.Claude, Text = piece });
} }
partial void OnTaskModelSelectionChanged(string value) => QueueAgentSave();
partial void OnTaskSystemPromptChanged(string value) => QueueAgentSave(); partial void OnTaskSystemPromptChanged(string value) => QueueAgentSave();
partial void OnTaskSelectedAgentChanged(AgentInfo? value) => QueueAgentSave(); partial void OnTaskSelectedAgentChanged(AgentInfo? value) { RecomputeAgentBadge(); QueueAgentSave(); }
partial void OnEditableDescriptionChanged(string value) partial void OnEditableDescriptionChanged(string value)
{ {
@@ -478,13 +510,14 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
await System.Threading.Tasks.Task.Delay(300, ct); await System.Threading.Tasks.Task.Delay(300, ct);
if (Task is null) return; if (Task is null) return;
var model = TaskModelSelection == ModelRegistry.TaskInheritSentinel ? null : TaskModelSelection; var model = string.IsNullOrWhiteSpace(TaskModelSelection) ? null : TaskModelSelection;
var sp = string.IsNullOrWhiteSpace(TaskSystemPrompt) ? null : TaskSystemPrompt; var sp = string.IsNullOrWhiteSpace(TaskSystemPrompt) ? null : TaskSystemPrompt;
var ap = TaskSelectedAgent is null || string.IsNullOrWhiteSpace(TaskSelectedAgent.Path) var ap = TaskSelectedAgent is null || string.IsNullOrWhiteSpace(TaskSelectedAgent.Path)
? null : TaskSelectedAgent.Path; ? null : TaskSelectedAgent.Path;
var turns = TaskMaxTurns is decimal d ? (int?)d : null;
await _worker.UpdateTaskAgentSettingsAsync( await _worker.UpdateTaskAgentSettingsAsync(
new ClaudeDo.Ui.Services.UpdateTaskAgentSettingsDto(Task.Id, model, sp, ap)); new ClaudeDo.Ui.Services.UpdateTaskAgentSettingsDto(Task.Id, model, sp, ap, turns));
} }
catch (OperationCanceledException) { } catch (OperationCanceledException) { }
catch { } catch { }
@@ -497,21 +530,31 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
try try
{ {
TaskAgentOptions.Clear(); TaskAgentOptions.Clear();
TaskAgentOptions.Add(new AgentInfo(ModelRegistry.TaskInheritSentinel, "", "")); TaskAgentOptions.Add(new AgentInfo("(inherited)", "", ""));
var agents = await _worker.GetAgentsAsync(); var agents = await _worker.GetAgentsAsync();
foreach (var a in agents) TaskAgentOptions.Add(a); foreach (var a in agents) TaskAgentOptions.Add(a);
TaskModelSelection = string.IsNullOrWhiteSpace(entity.Model) ? ModelRegistry.TaskInheritSentinel : entity.Model!; TaskModelSelection = string.IsNullOrWhiteSpace(entity.Model) ? null : entity.Model!;
TaskMaxTurns = entity.MaxTurns is int tmt ? tmt : (decimal?)null;
TaskSystemPrompt = entity.SystemPrompt ?? ""; TaskSystemPrompt = entity.SystemPrompt ?? "";
TaskSelectedAgent = string.IsNullOrWhiteSpace(entity.AgentPath) TaskSelectedAgent = string.IsNullOrWhiteSpace(entity.AgentPath)
? TaskAgentOptions[0] ? TaskAgentOptions[0]
: (TaskAgentOptions.FirstOrDefault(a => a.Path == entity.AgentPath) ?? TaskAgentOptions[0]); : (TaskAgentOptions.FirstOrDefault(a => a.Path == entity.AgentPath) ?? TaskAgentOptions[0]);
var listCfg = await _worker.GetListConfigAsync(entity.ListId); var listCfg = await _worker.GetListConfigAsync(entity.ListId);
EffectiveModelHint = string.IsNullOrWhiteSpace(listCfg?.Model) ? "(global default)" : listCfg!.Model!; var app = await _worker.GetAppSettingsAsync();
EffectiveSystemPromptHint = string.IsNullOrWhiteSpace(listCfg?.SystemPrompt) ? "(none)" : listCfg!.SystemPrompt!; _globalModel = app?.DefaultModel ?? ModelRegistry.DefaultAlias;
EffectiveAgentHint = string.IsNullOrWhiteSpace(listCfg?.AgentPath) _globalMaxTurns = app?.DefaultMaxTurns ?? 100;
? "(none)" : System.IO.Path.GetFileName(listCfg!.AgentPath!); _listModel = listCfg?.Model;
_listMaxTurns = listCfg?.MaxTurns;
_listAgentName = string.IsNullOrWhiteSpace(listCfg?.AgentPath)
? null : System.IO.Path.GetFileName(listCfg!.AgentPath!);
EffectiveSystemPromptHint = string.IsNullOrWhiteSpace(listCfg?.SystemPrompt) ? "" : listCfg!.SystemPrompt!;
RecomputeModelBadge();
RecomputeTurnsBadge();
RecomputeAgentBadge();
} }
finally finally
{ {
@@ -583,7 +626,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
_suppressAgentSave = true; _suppressAgentSave = true;
try try
{ {
TaskModelSelection = ModelRegistry.TaskInheritSentinel; TaskModelSelection = null;
TaskMaxTurns = null;
TaskSystemPrompt = ""; TaskSystemPrompt = "";
TaskSelectedAgent = null; TaskSelectedAgent = null;
} }
@@ -591,9 +635,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
{ {
_suppressAgentSave = false; _suppressAgentSave = false;
} }
EffectiveModelHint = "";
EffectiveSystemPromptHint = ""; EffectiveSystemPromptHint = "";
EffectiveAgentHint = "";
return; return;
} }
@@ -1085,6 +1127,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
private bool CanResetAndRetry() => private bool CanResetAndRetry() =>
Task != null && _worker.IsConnected && ShowResetAndRetry; Task != null && _worker.IsConnected && ShowResetAndRetry;
[RelayCommand] private void ResetTaskModel() => TaskModelSelection = null;
[RelayCommand] private void ResetTaskTurns() => TaskMaxTurns = null;
[RelayCommand] private void ResetTaskAgent() => TaskSelectedAgent = TaskAgentOptions.Count > 0 ? TaskAgentOptions[0] : null;
} }
public sealed partial class SubtaskRowViewModel : ViewModelBase public sealed partial class SubtaskRowViewModel : ViewModelBase

View File

@@ -88,24 +88,50 @@
<TextBlock Text="{loc:Tr details.agentSettingsHeading}" FontWeight="SemiBold"/> <TextBlock Text="{loc:Tr details.agentSettingsHeading}" FontWeight="SemiBold"/>
<StackPanel Spacing="2"> <StackPanel Spacing="2">
<TextBlock Classes="field-label" Text="{loc:Tr details.modelLabel}"/> <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 ModelBadge}"/>
<Button Grid.Column="3" Classes="icon-btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
Command="{Binding ResetTaskModelCommand}"/>
</Grid>
<ComboBox ItemsSource="{Binding TaskModelOptions}" <ComboBox ItemsSource="{Binding TaskModelOptions}"
SelectedItem="{Binding TaskModelSelection, Mode=TwoWay}" SelectedItem="{Binding TaskModelSelection, Mode=TwoWay}"
PlaceholderText="{Binding 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 TurnsBadge}"/>
<Button Grid.Column="3" Classes="icon-btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
Command="{Binding ResetTaskTurnsCommand}"/>
</Grid>
<NumericUpDown Value="{Binding TaskMaxTurns, Mode=TwoWay}"
PlaceholderText="{Binding TurnsInheritedHint}"
Minimum="1" Maximum="200" Increment="1" FormatString="0"
HorizontalAlignment="Stretch"/> HorizontalAlignment="Stretch"/>
<TextBlock Classes="meta"
Text="{Binding EffectiveModelLabel}"
Opacity="0.6"/>
</StackPanel> </StackPanel>
<StackPanel Spacing="2"> <StackPanel Spacing="2">
<TextBlock Classes="field-label" Text="{loc:Tr details.systemPromptLabel}"/> <TextBlock Classes="field-label" Text="{loc:Tr details.systemPromptLabel}"/>
<TextBox Text="{Binding TaskSystemPrompt, Mode=TwoWay}" <TextBox Text="{Binding TaskSystemPrompt, Mode=TwoWay}"
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="70" AcceptsReturn="True" TextWrapping="Wrap" MinHeight="70"/>
PlaceholderText="{Binding EffectiveSystemPromptHint}"/> <TextBlock Classes="meta" Opacity="0.6"
Text="{loc:Tr details.systemPromptPrepended}"
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> </StackPanel>
<StackPanel Spacing="2"> <StackPanel Spacing="2">
<TextBlock Classes="field-label" Text="{loc:Tr details.agentFileLabel}"/> <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 AgentBadge}"/>
<Button Grid.Column="3" Classes="icon-btn" Content="↺" ToolTip.Tip="{loc:Tr settings.inherit.resetToInherited}"
Command="{Binding ResetTaskAgentCommand}"/>
</Grid>
<ComboBox ItemsSource="{Binding TaskAgentOptions}" <ComboBox ItemsSource="{Binding TaskAgentOptions}"
SelectedItem="{Binding TaskSelectedAgent, Mode=TwoWay}" SelectedItem="{Binding TaskSelectedAgent, Mode=TwoWay}"
HorizontalAlignment="Stretch"> HorizontalAlignment="Stretch">
@@ -115,9 +141,6 @@
</DataTemplate> </DataTemplate>
</ComboBox.ItemTemplate> </ComboBox.ItemTemplate>
</ComboBox> </ComboBox>
<TextBlock Classes="meta"
Text="{Binding EffectiveAgentLabel}"
Opacity="0.6"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>
</Flyout> </Flyout>