feat(ui): per-task agent settings in DetailsIsland

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mika Kuns
2026-04-22 13:29:57 +02:00
parent 5784dbee94
commit bba577888b
2 changed files with 142 additions and 0 deletions

View File

@@ -3,6 +3,7 @@ using System.Text;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Data; using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories; using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Helpers; using ClaudeDo.Ui.Helpers;
using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services;
@@ -53,9 +54,32 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
OnPropertyChanged(nameof(IsRunning)); OnPropertyChanged(nameof(IsRunning));
OnPropertyChanged(nameof(IsDone)); OnPropertyChanged(nameof(IsDone));
OnPropertyChanged(nameof(IsFailed)); OnPropertyChanged(nameof(IsFailed));
OnPropertyChanged(nameof(IsAgentSectionEnabled));
ShowFailedActions = value == "Failed"; ShowFailedActions = value == "Failed";
} }
[ObservableProperty] private string? _model; [ObservableProperty] private string? _model;
// Agent settings overrides
[ObservableProperty] private string _taskModelSelection = "(inherit)";
[ObservableProperty] private string _taskSystemPrompt = "";
[ObservableProperty] private AgentInfo? _taskSelectedAgent;
[ObservableProperty] private string _effectiveModelHint = "";
[ObservableProperty] private string _effectiveSystemPromptHint = "";
[ObservableProperty] private string _effectiveAgentHint = "";
public System.Collections.ObjectModel.ObservableCollection<string> TaskModelOptions { get; } = new()
{
"(inherit)", "sonnet", "opus", "haiku",
};
public System.Collections.ObjectModel.ObservableCollection<AgentInfo> TaskAgentOptions { get; } = new();
private bool _suppressAgentSave;
private CancellationTokenSource? _agentSaveCts;
public bool IsAgentSectionEnabled => !IsRunning;
[ObservableProperty] private string? _worktreePath; [ObservableProperty] private string? _worktreePath;
[ObservableProperty] private string? _worktreeBaseCommit; [ObservableProperty] private string? _worktreeBaseCommit;
[ObservableProperty] private string? _worktreeStateLabel; [ObservableProperty] private string? _worktreeStateLabel;
@@ -212,6 +236,67 @@ 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 OnTaskSelectedAgentChanged(AgentInfo? value) => QueueAgentSave();
private void QueueAgentSave()
{
if (_suppressAgentSave || Task is null) return;
_agentSaveCts?.Cancel();
_agentSaveCts = new CancellationTokenSource();
var ct = _agentSaveCts.Token;
_ = SaveAgentSettingsAsync(ct);
}
private async System.Threading.Tasks.Task SaveAgentSettingsAsync(CancellationToken ct)
{
try
{
await System.Threading.Tasks.Task.Delay(300, ct);
if (Task is null) return;
var model = TaskModelSelection == "(inherit)" ? null : TaskModelSelection;
var sp = string.IsNullOrWhiteSpace(TaskSystemPrompt) ? null : TaskSystemPrompt;
var ap = TaskSelectedAgent is null || string.IsNullOrWhiteSpace(TaskSelectedAgent.Path)
? null : TaskSelectedAgent.Path;
await _worker.UpdateTaskAgentSettingsAsync(
new ClaudeDo.Ui.Services.UpdateTaskAgentSettingsDto(Task.Id, model, sp, ap));
}
catch (OperationCanceledException) { }
catch { }
}
private async System.Threading.Tasks.Task LoadAgentSettingsAsync(
ClaudeDo.Data.Models.TaskEntity entity, CancellationToken ct)
{
_suppressAgentSave = true;
try
{
TaskAgentOptions.Clear();
TaskAgentOptions.Add(new AgentInfo("(inherit)", "", ""));
var agents = await _worker.GetAgentsAsync();
foreach (var a in agents) TaskAgentOptions.Add(a);
TaskModelSelection = string.IsNullOrWhiteSpace(entity.Model) ? "(inherit)" : entity.Model!;
TaskSystemPrompt = entity.SystemPrompt ?? "";
TaskSelectedAgent = string.IsNullOrWhiteSpace(entity.AgentPath)
? TaskAgentOptions[0]
: (TaskAgentOptions.FirstOrDefault(a => a.Path == entity.AgentPath) ?? TaskAgentOptions[0]);
var listCfg = await _worker.GetListConfigAsync(entity.ListId);
EffectiveModelHint = string.IsNullOrWhiteSpace(listCfg?.Model) ? "(global default)" : listCfg!.Model!;
EffectiveSystemPromptHint = string.IsNullOrWhiteSpace(listCfg?.SystemPrompt) ? "(none)" : listCfg!.SystemPrompt!;
EffectiveAgentHint = string.IsNullOrWhiteSpace(listCfg?.AgentPath)
? "(none)" : System.IO.Path.GetFileName(listCfg!.AgentPath!);
}
finally
{
_suppressAgentSave = false;
}
}
public void Bind(TaskRowViewModel? row) public void Bind(TaskRowViewModel? row)
{ {
_loadCts?.Cancel(); _loadCts?.Cancel();
@@ -237,6 +322,20 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
AgentStatusLabel = "Idle"; AgentStatusLabel = "Idle";
LatestRunSessionId = null; LatestRunSessionId = null;
ShowFailedActions = false; ShowFailedActions = false;
_suppressAgentSave = true;
try
{
TaskModelSelection = "(inherit)";
TaskSystemPrompt = "";
TaskSelectedAgent = null;
}
finally
{
_suppressAgentSave = false;
}
EffectiveModelHint = "";
EffectiveSystemPromptHint = "";
EffectiveAgentHint = "";
return; return;
} }
@@ -266,6 +365,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
WorktreeStateLabel = entity.Worktree?.State.ToString(); WorktreeStateLabel = entity.Worktree?.State.ToString();
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} \u2190 main" : null; BranchLine = entity.Worktree is { } w ? $"{w.BranchName} \u2190 main" : null;
AgentStatusLabel = entity.Status.ToString(); AgentStatusLabel = entity.Status.ToString();
await LoadAgentSettingsAsync(entity, ct);
ct.ThrowIfCancellationRequested();
var runRepo = new TaskRunRepository(ctx); var runRepo = new TaskRunRepository(ctx);
var latestRun = await runRepo.GetLatestByTaskIdAsync(row.Id, ct); var latestRun = await runRepo.GetLatestByTaskIdAsync(row.Id, ct);

View File

@@ -150,6 +150,47 @@
LostFocus="NotesLostFocus"/> LostFocus="NotesLostFocus"/>
</StackPanel> </StackPanel>
<!-- Agent settings overrides -->
<Expander Header="Agent settings (overrides)"
IsExpanded="False"
IsEnabled="{Binding IsAgentSectionEnabled}"
Margin="18,0,18,12">
<StackPanel Spacing="8" Margin="0,8,0,0">
<StackPanel Spacing="2">
<TextBlock Text="Model" />
<ComboBox ItemsSource="{Binding TaskModelOptions}"
SelectedItem="{Binding TaskModelSelection, Mode=TwoWay}"
MinWidth="160" HorizontalAlignment="Left" />
<TextBlock Text="{Binding EffectiveModelHint, StringFormat='Effective if inherited: {0}'}"
Opacity="0.6" FontSize="11" />
</StackPanel>
<StackPanel Spacing="2">
<TextBlock Text="System prompt (appended)" />
<TextBox Text="{Binding TaskSystemPrompt, Mode=TwoWay}"
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="60"
Watermark="{Binding EffectiveSystemPromptHint}" />
</StackPanel>
<StackPanel Spacing="2">
<TextBlock Text="Agent file" />
<ComboBox ItemsSource="{Binding TaskAgentOptions}"
SelectedItem="{Binding TaskSelectedAgent, Mode=TwoWay}"
MinWidth="240" HorizontalAlignment="Left">
<ComboBox.ItemTemplate>
<DataTemplate>
<TextBlock Text="{Binding Name}" />
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<TextBlock Text="{Binding EffectiveAgentHint, StringFormat='Effective if inherited: {0}'}"
Opacity="0.6" FontSize="11" />
</StackPanel>
</StackPanel>
</Expander>
</StackPanel> </StackPanel>
</ScrollViewer> </ScrollViewer>
</Grid> </Grid>