feat(ui): complete Batch 2 — LiveText display, start feedback, modal theming, ListEditor config

- Replace LiveLines with formatted LiveText + StreamLineFormatter
- Add log reload from disk for completed tasks
- Add start feedback (starting.../running) on detail and list views
- Apply dark theme (WindowBgBrush, AccentBrush) to editor modals
- Add model/system-prompt/agent-path config to ListEditorView
- Wire config loading/saving in MainWindowViewModel
- Fix duplicate AgentInfo DTO (use canonical Data.Models version)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mika Kuns
2026-04-14 16:36:40 +02:00
parent 0764bb30ab
commit 699fe8a148
5 changed files with 113 additions and 21 deletions

View File

@@ -1,6 +1,8 @@
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using AgentInfo = ClaudeDo.Data.Models.AgentInfo;
namespace ClaudeDo.Ui.ViewModels;
@@ -11,6 +13,11 @@ public partial class ListEditorViewModel : ViewModelBase
[ObservableProperty] private string _defaultCommitType = "chore";
[ObservableProperty] private string _windowTitle = "New List";
// Config fields
[ObservableProperty] private string _model = "Sonnet";
[ObservableProperty] private string? _systemPrompt;
[ObservableProperty] private AgentInfo? _selectedAgent;
private string? _editId;
private DateTime _createdAt;
private TaskCompletionSource<ListEntity?> _tcs = new();
@@ -20,6 +27,31 @@ public partial class ListEditorViewModel : ViewModelBase
public static string[] CommitTypes { get; } =
["chore", "feat", "fix", "refactor", "docs", "test", "perf", "style", "build", "ci"];
public static string[] ModelDisplayNames { get; } = ["Sonnet", "Opus", "Haiku"];
private static readonly Dictionary<string, string> ModelToId = new()
{
["Sonnet"] = "claude-sonnet-4-6",
["Opus"] = "claude-opus-4-6",
["Haiku"] = "claude-haiku-4-5",
};
private static readonly Dictionary<string, string> IdToModel =
ModelToId.ToDictionary(kv => kv.Value, kv => kv.Key);
public static string ModelIdToDisplay(string? modelId) =>
modelId is not null && IdToModel.TryGetValue(modelId, out var display) ? display : "Sonnet";
public static string? ModelDisplayToId(string display) =>
ModelToId.TryGetValue(display, out var id) ? id : null;
public List<AgentInfo> AvailableAgents { get; set; } = [];
public async Task LoadAgentsAsync(WorkerClient worker)
{
AvailableAgents = await worker.GetAgentsAsync();
}
public void InitForCreate()
{
_editId = null;
@@ -27,7 +59,7 @@ public partial class ListEditorViewModel : ViewModelBase
WindowTitle = "New List";
}
public void InitForEdit(ListEntity entity)
public void InitForEdit(ListEntity entity, ListConfigEntity? config)
{
_editId = entity.Id;
_createdAt = entity.CreatedAt;
@@ -35,6 +67,28 @@ public partial class ListEditorViewModel : ViewModelBase
WorkingDir = entity.WorkingDir;
DefaultCommitType = entity.DefaultCommitType;
WindowTitle = $"Edit List: {entity.Name}";
if (config is not null)
{
Model = ModelIdToDisplay(config.Model);
SystemPrompt = config.SystemPrompt;
SelectedAgent = AvailableAgents.FirstOrDefault(a => a.Path == config.AgentPath);
}
}
public ListConfigEntity? BuildConfig(string listId)
{
var modelId = ModelDisplayToId(Model);
if (modelId is null && SystemPrompt is null && SelectedAgent is null)
return null;
return new ListConfigEntity
{
ListId = listId,
Model = modelId,
SystemPrompt = string.IsNullOrWhiteSpace(SystemPrompt) ? null : SystemPrompt.Trim(),
AgentPath = SelectedAgent?.Path,
};
}
[RelayCommand]
@@ -60,10 +114,6 @@ public partial class ListEditorViewModel : ViewModelBase
RequestClose?.Invoke();
}
/// <summary>
/// Called by the view to await the editor result.
/// Returns the entity to persist or null if cancelled.
/// </summary>
public Task<ListEntity?> ShowAndWaitAsync()
{
_tcs = new TaskCompletionSource<ListEntity?>();

View File

@@ -78,6 +78,7 @@ public partial class MainWindowViewModel : ViewModelBase
private async Task AddList()
{
var editor = _listEditorFactory();
await editor.LoadAgentsAsync(_worker);
editor.InitForCreate();
var window = new ListEditorView { DataContext = editor };
@@ -90,6 +91,9 @@ public partial class MainWindowViewModel : ViewModelBase
try
{
await _listRepo.AddAsync(entity);
var configEntity = editor.BuildConfig(entity.Id);
if (configEntity is not null)
await _listRepo.SetConfigAsync(configEntity);
Lists.Add(new ListItemViewModel(entity));
}
catch (Exception ex)
@@ -105,8 +109,10 @@ public partial class MainWindowViewModel : ViewModelBase
var existing = await _listRepo.GetByIdAsync(SelectedList.Id);
if (existing is null) return;
var config = await _listRepo.GetConfigAsync(existing.Id);
var editor = _listEditorFactory();
editor.InitForEdit(existing);
await editor.LoadAgentsAsync(_worker);
editor.InitForEdit(existing, config);
var window = new ListEditorView { DataContext = editor };
editor.RequestClose += () => window.Close();
@@ -118,6 +124,9 @@ public partial class MainWindowViewModel : ViewModelBase
try
{
await _listRepo.UpdateAsync(entity);
var configEntity = editor.BuildConfig(entity.Id);
if (configEntity is not null)
await _listRepo.SetConfigAsync(configEntity);
SelectedList.Name = entity.Name;
SelectedList.WorkingDir = entity.WorkingDir;
SelectedList.DefaultCommitType = entity.DefaultCommitType;