feat(ui): replace LiveLines with formatted LiveText, add log reload and start feedback

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Mika Kuns
2026-04-14 16:28:12 +02:00
parent 503fd69cd1
commit 0764bb30ab
3 changed files with 71 additions and 18 deletions

View File

@@ -1,8 +1,11 @@
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using ClaudeDo.Data.Git; using ClaudeDo.Data.Git;
using ClaudeDo.Data.Models; using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories; using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Helpers;
using ClaudeDo.Ui.Services; using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input; using CommunityToolkit.Mvvm.Input;
@@ -37,14 +40,14 @@ public partial class TaskDetailViewModel : ViewModelBase
[ObservableProperty] private string _worktreeState = ""; [ObservableProperty] private string _worktreeState = "";
// Live stream // Live stream
public ObservableCollection<string> LiveLines { get; } = new(); [ObservableProperty] private string _liveText = "";
private StreamLineFormatter _formatter = new();
public ObservableCollection<TagEntity> Tags { get; } = new(); public ObservableCollection<TagEntity> Tags { get; } = new();
[ObservableProperty] private string _newTagInput = ""; [ObservableProperty] private string _newTagInput = "";
private string? _taskId; private string? _taskId;
private string? _listId; private string? _listId;
private bool _isLoading; private bool _isLoading;
private const int MaxLiveLines = 500;
public event Action<string>? TaskChanged; public event Action<string>? TaskChanged;
@@ -61,12 +64,15 @@ public partial class TaskDetailViewModel : ViewModelBase
worker.TaskMessageEvent += OnTaskMessage; worker.TaskMessageEvent += OnTaskMessage;
worker.WorktreeUpdatedEvent += OnWorktreeUpdated; worker.WorktreeUpdatedEvent += OnWorktreeUpdated;
worker.TaskUpdatedEvent += OnTaskUpdated; worker.TaskUpdatedEvent += OnTaskUpdated;
worker.RunNowRequestedEvent += OnRunNowRequested;
worker.TaskStartedEvent += OnTaskStarted;
} }
public async Task LoadAsync(string taskId) public async Task LoadAsync(string taskId)
{ {
_taskId = taskId; _taskId = taskId;
LiveLines.Clear(); LiveText = "";
_formatter = new StreamLineFormatter();
var task = await _taskRepo.GetByIdAsync(taskId); var task = await _taskRepo.GetByIdAsync(taskId);
if (task is null) return; if (task is null) return;
@@ -79,6 +85,13 @@ public partial class TaskDetailViewModel : ViewModelBase
Description = task.Description; Description = task.Description;
Result = task.Result; Result = task.Result;
LogPath = task.LogPath; LogPath = task.LogPath;
if (task.LogPath is not null
&& task.Status is Data.Models.TaskStatus.Done or Data.Models.TaskStatus.Failed
&& File.Exists(task.LogPath))
{
_formatter = new StreamLineFormatter();
LiveText = _formatter.FormatFile(task.LogPath);
}
StatusText = task.Status.ToString().ToLowerInvariant(); StatusText = task.Status.ToString().ToLowerInvariant();
StatusChoice = task.Status.ToString(); StatusChoice = task.Status.ToString();
CommitType = task.CommitType; CommitType = task.CommitType;
@@ -152,7 +165,8 @@ public partial class TaskDetailViewModel : ViewModelBase
LogPath = null; LogPath = null;
StatusText = ""; StatusText = "";
HasWorktree = false; HasWorktree = false;
LiveLines.Clear(); LiveText = "";
_formatter = new StreamLineFormatter();
Tags.Clear(); Tags.Clear();
NewTagInput = ""; NewTagInput = "";
StatusChoice = "Manual"; StatusChoice = "Manual";
@@ -259,9 +273,27 @@ public partial class TaskDetailViewModel : ViewModelBase
private void OnTaskMessage(string taskId, string line) private void OnTaskMessage(string taskId, string line)
{ {
if (taskId != _taskId) return; if (taskId != _taskId) return;
if (LiveLines.Count >= MaxLiveLines) var formatted = _formatter.FormatLine(line);
LiveLines.RemoveAt(0); if (formatted is not null)
LiveLines.Add(line); {
LiveText += formatted;
if (LiveText.Length > 50_000)
LiveText = StreamLineFormatter.Trim(LiveText);
}
}
private void OnRunNowRequested(string taskId)
{
if (taskId != _taskId) return;
StatusText = "starting...";
LiveText = "";
_formatter = new StreamLineFormatter();
}
private void OnTaskStarted(string slot, string taskId, DateTime startedAt)
{
if (taskId != _taskId) return;
StatusText = "running";
} }
private async void OnWorktreeUpdated(string taskId) private async void OnWorktreeUpdated(string taskId)

View File

@@ -107,17 +107,19 @@
<TextBlock Text="Live Output" FontWeight="SemiBold" FontSize="12" <TextBlock Text="Live Output" FontWeight="SemiBold" FontSize="12"
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,8,0,2"/> Foreground="{StaticResource TextSecondaryBrush}" Margin="0,8,0,2"/>
<Border BorderBrush="{StaticResource BorderSubtleBrush}" BorderThickness="1" <Border BorderBrush="{StaticResource BorderSubtleBrush}" BorderThickness="1"
CornerRadius="6" Padding="6" MaxHeight="200"> CornerRadius="6" Padding="6" MaxHeight="300">
<ScrollViewer> <ScrollViewer x:Name="LiveOutputScroll">
<ItemsControl ItemsSource="{Binding LiveLines}"> <TextBox x:Name="LiveOutputBox"
<ItemsControl.ItemTemplate> Text="{Binding LiveText, Mode=OneWay}"
<DataTemplate> IsReadOnly="True"
<TextBlock Text="{Binding}" FontFamily="Consolas,Courier New,monospace" AcceptsReturn="True"
FontSize="11" TextWrapping="NoWrap" TextWrapping="NoWrap"
Foreground="{StaticResource TextPrimaryBrush}"/> FontFamily="Consolas,Courier New,monospace"
</DataTemplate> FontSize="11"
</ItemsControl.ItemTemplate> Foreground="{StaticResource TextPrimaryBrush}"
</ItemsControl> Background="Transparent"
BorderThickness="0"
Padding="0"/>
</ScrollViewer> </ScrollViewer>
</Border> </Border>

View File

@@ -1,3 +1,4 @@
using System.ComponentModel;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
@@ -31,4 +32,22 @@ public partial class TaskDetailView : UserControl
{ {
this.FindControl<TextBox>("TitleBox")?.Focus(); this.FindControl<TextBox>("TitleBox")?.Focus();
} }
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
if (DataContext is TaskDetailViewModel vm)
{
vm.PropertyChanged += OnViewModelPropertyChanged;
}
}
private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(TaskDetailViewModel.LiveText))
{
var scroll = this.FindControl<ScrollViewer>("LiveOutputScroll");
scroll?.ScrollToEnd();
}
}
} }