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:
@@ -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);
|
|
||||||
|
|||||||
@@ -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?>();
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user