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,5 +1,6 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using Avalonia.Threading; using Avalonia.Threading;
using ClaudeDo.Data.Models;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using Microsoft.AspNetCore.SignalR.Client; using Microsoft.AspNetCore.SignalR.Client;
@@ -228,5 +229,3 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable
public DateTime StartedAt { get; set; } public DateTime StartedAt { get; set; }
} }
} }
public record AgentInfo(string Name, string Description, string Path);

View File

@@ -1,6 +1,8 @@
using ClaudeDo.Data.Models; using ClaudeDo.Data.Models;
using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
using AgentInfo = ClaudeDo.Data.Models.AgentInfo;
namespace ClaudeDo.Ui.ViewModels; namespace ClaudeDo.Ui.ViewModels;
@@ -11,6 +13,11 @@ public partial class ListEditorViewModel : ViewModelBase
[ObservableProperty] private string _defaultCommitType = "chore"; [ObservableProperty] private string _defaultCommitType = "chore";
[ObservableProperty] private string _windowTitle = "New List"; [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 string? _editId;
private DateTime _createdAt; private DateTime _createdAt;
private TaskCompletionSource<ListEntity?> _tcs = new(); private TaskCompletionSource<ListEntity?> _tcs = new();
@@ -20,6 +27,31 @@ public partial class ListEditorViewModel : ViewModelBase
public static string[] CommitTypes { get; } = public static string[] CommitTypes { get; } =
["chore", "feat", "fix", "refactor", "docs", "test", "perf", "style", "build", "ci"]; ["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() public void InitForCreate()
{ {
_editId = null; _editId = null;
@@ -27,7 +59,7 @@ public partial class ListEditorViewModel : ViewModelBase
WindowTitle = "New List"; WindowTitle = "New List";
} }
public void InitForEdit(ListEntity entity) public void InitForEdit(ListEntity entity, ListConfigEntity? config)
{ {
_editId = entity.Id; _editId = entity.Id;
_createdAt = entity.CreatedAt; _createdAt = entity.CreatedAt;
@@ -35,6 +67,28 @@ public partial class ListEditorViewModel : ViewModelBase
WorkingDir = entity.WorkingDir; WorkingDir = entity.WorkingDir;
DefaultCommitType = entity.DefaultCommitType; DefaultCommitType = entity.DefaultCommitType;
WindowTitle = $"Edit List: {entity.Name}"; 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] [RelayCommand]
@@ -60,10 +114,6 @@ public partial class ListEditorViewModel : ViewModelBase
RequestClose?.Invoke(); 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() public Task<ListEntity?> ShowAndWaitAsync()
{ {
_tcs = new TaskCompletionSource<ListEntity?>(); _tcs = new TaskCompletionSource<ListEntity?>();

View File

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

View File

@@ -1,29 +1,61 @@
<Window xmlns="https://github.com/avaloniaui" <Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels" xmlns:vm="using:ClaudeDo.Ui.ViewModels"
xmlns:svc="using:ClaudeDo.Data.Models"
x:Class="ClaudeDo.Ui.Views.ListEditorView" x:Class="ClaudeDo.Ui.Views.ListEditorView"
x:DataType="vm:ListEditorViewModel" x:DataType="vm:ListEditorViewModel"
Title="{Binding WindowTitle}" Title="{Binding WindowTitle}"
Width="450" Height="280" Width="450" Height="480"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
CanResize="False"> CanResize="False"
Background="{StaticResource WindowBgBrush}">
<StackPanel Margin="16" Spacing="10"> <StackPanel Margin="16" Spacing="10">
<TextBlock Text="Name" FontWeight="SemiBold"/> <TextBlock Text="Name" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}"/>
<TextBox Text="{Binding Name}" PlaceholderText="List name..."/> <TextBox Text="{Binding Name}" PlaceholderText="List name..."/>
<TextBlock Text="Working Directory" FontWeight="SemiBold"/> <TextBlock Text="Working Directory" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}"/>
<DockPanel> <DockPanel>
<Button DockPanel.Dock="Right" Content="Browse..." Click="OnBrowseFolder" Margin="8,0,0,0" VerticalAlignment="Center"/> <Button DockPanel.Dock="Right" Content="Browse..." Click="OnBrowseFolder" Margin="8,0,0,0" VerticalAlignment="Center"/>
<TextBox Text="{Binding WorkingDir}" PlaceholderText="(optional) Absolute path to git repo"/> <TextBox Text="{Binding WorkingDir}" PlaceholderText="(optional) Absolute path to git repo"/>
</DockPanel> </DockPanel>
<TextBlock Text="Default Commit Type" FontWeight="SemiBold"/> <TextBlock Text="Default Commit Type" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}"/>
<ComboBox ItemsSource="{Binding CommitTypes}" <ComboBox ItemsSource="{Binding CommitTypes}"
SelectedItem="{Binding DefaultCommitType}" SelectedItem="{Binding DefaultCommitType}"
MinWidth="150"/> MinWidth="150"/>
<Border Height="1" Background="{StaticResource BorderSubtleBrush}" Margin="0,6,0,2"/>
<TextBlock Text="Agent Config" FontWeight="Bold" FontSize="13"
Foreground="{StaticResource TextPrimaryBrush}"/>
<TextBlock Text="Model" FontWeight="SemiBold"
Foreground="{StaticResource TextSecondaryBrush}"/>
<ComboBox ItemsSource="{Binding ModelDisplayNames}"
SelectedItem="{Binding Model}"
MinWidth="150"/>
<TextBlock Text="System Prompt" FontWeight="SemiBold"
Foreground="{StaticResource TextSecondaryBrush}"/>
<TextBox Text="{Binding SystemPrompt}"
PlaceholderText="(optional) Additional system instructions..."
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="50"/>
<TextBlock Text="Agent File" FontWeight="SemiBold"
Foreground="{StaticResource TextSecondaryBrush}"/>
<ComboBox ItemsSource="{Binding AvailableAgents}"
SelectedItem="{Binding SelectedAgent}"
MinWidth="150">
<ComboBox.ItemTemplate>
<DataTemplate x:DataType="svc:AgentInfo">
<TextBlock Text="{Binding Name}"/>
</DataTemplate>
</ComboBox.ItemTemplate>
</ComboBox>
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right" Margin="0,10,0,0"> <StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right" Margin="0,10,0,0">
<Button Content="Save" Command="{Binding SaveCommand}" IsDefault="True" MinWidth="80"/> <Button Content="Save" Command="{Binding SaveCommand}" IsDefault="True" MinWidth="80"
Background="{StaticResource AccentBrush}" Foreground="{StaticResource TextPrimaryBrush}"/>
<Button Content="Cancel" Command="{Binding CancelCommand}" IsCancel="True" MinWidth="80"/> <Button Content="Cancel" Command="{Binding CancelCommand}" IsCancel="True" MinWidth="80"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>

View File

@@ -6,35 +6,37 @@
Title="{Binding WindowTitle}" Title="{Binding WindowTitle}"
Width="500" Height="420" Width="500" Height="420"
WindowStartupLocation="CenterOwner" WindowStartupLocation="CenterOwner"
CanResize="False"> CanResize="False"
Background="{StaticResource WindowBgBrush}">
<StackPanel Margin="16" Spacing="10"> <StackPanel Margin="16" Spacing="10">
<TextBlock Text="Title" FontWeight="SemiBold"/> <TextBlock Text="Title" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}"/>
<TextBox Text="{Binding Title}" PlaceholderText="Task title..."/> <TextBox Text="{Binding Title}" PlaceholderText="Task title..."/>
<TextBlock Text="Description" FontWeight="SemiBold"/> <TextBlock Text="Description" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}"/>
<TextBox Text="{Binding Description}" PlaceholderText="(optional)" AcceptsReturn="True" <TextBox Text="{Binding Description}" PlaceholderText="(optional)" AcceptsReturn="True"
TextWrapping="Wrap" MinHeight="80"/> TextWrapping="Wrap" MinHeight="80"/>
<Grid ColumnDefinitions="*,16,*"> <Grid ColumnDefinitions="*,16,*">
<StackPanel Grid.Column="0" Spacing="4"> <StackPanel Grid.Column="0" Spacing="4">
<TextBlock Text="Status" FontWeight="SemiBold"/> <TextBlock Text="Status" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}"/>
<ComboBox ItemsSource="{Binding StatusChoices}" <ComboBox ItemsSource="{Binding StatusChoices}"
SelectedItem="{Binding StatusChoice}" SelectedItem="{Binding StatusChoice}"
MinWidth="120"/> MinWidth="120"/>
</StackPanel> </StackPanel>
<StackPanel Grid.Column="2" Spacing="4"> <StackPanel Grid.Column="2" Spacing="4">
<TextBlock Text="Commit Type" FontWeight="SemiBold"/> <TextBlock Text="Commit Type" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}"/>
<ComboBox ItemsSource="{Binding CommitTypes}" <ComboBox ItemsSource="{Binding CommitTypes}"
SelectedItem="{Binding CommitType}" SelectedItem="{Binding CommitType}"
MinWidth="120"/> MinWidth="120"/>
</StackPanel> </StackPanel>
</Grid> </Grid>
<TextBlock Text="Tags (comma-separated)" FontWeight="SemiBold"/> <TextBlock Text="Tags (comma-separated)" FontWeight="SemiBold" Foreground="{StaticResource TextSecondaryBrush}"/>
<TextBox Text="{Binding TagsInput}" PlaceholderText="agent, manual, code, ..."/> <TextBox Text="{Binding TagsInput}" PlaceholderText="agent, manual, code, ..."/>
<StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right" Margin="0,10,0,0"> <StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right" Margin="0,10,0,0">
<Button Content="Save" Command="{Binding SaveCommand}" IsDefault="True" MinWidth="80"/> <Button Content="Save" Command="{Binding SaveCommand}" IsDefault="True" MinWidth="80"
Background="{StaticResource AccentBrush}" Foreground="{StaticResource TextPrimaryBrush}"/>
<Button Content="Cancel" Command="{Binding CancelCommand}" IsCancel="True" MinWidth="80"/> <Button Content="Cancel" Command="{Binding CancelCommand}" IsCancel="True" MinWidth="80"/>
</StackPanel> </StackPanel>
</StackPanel> </StackPanel>