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