feat(ui): per-task agent settings in DetailsIsland
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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);
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user