diff --git a/src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs b/src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs index c6959f6..33cdfcd 100644 --- a/src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs @@ -1,8 +1,11 @@ using System.Collections.ObjectModel; +using System.ComponentModel; using System.Diagnostics; +using System.IO; using ClaudeDo.Data.Git; using ClaudeDo.Data.Models; using ClaudeDo.Data.Repositories; +using ClaudeDo.Ui.Helpers; using ClaudeDo.Ui.Services; using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.Input; @@ -37,14 +40,14 @@ public partial class TaskDetailViewModel : ViewModelBase [ObservableProperty] private string _worktreeState = ""; // Live stream - public ObservableCollection LiveLines { get; } = new(); + [ObservableProperty] private string _liveText = ""; + private StreamLineFormatter _formatter = new(); public ObservableCollection Tags { get; } = new(); [ObservableProperty] private string _newTagInput = ""; private string? _taskId; private string? _listId; private bool _isLoading; - private const int MaxLiveLines = 500; public event Action? TaskChanged; @@ -61,12 +64,15 @@ public partial class TaskDetailViewModel : ViewModelBase worker.TaskMessageEvent += OnTaskMessage; worker.WorktreeUpdatedEvent += OnWorktreeUpdated; worker.TaskUpdatedEvent += OnTaskUpdated; + worker.RunNowRequestedEvent += OnRunNowRequested; + worker.TaskStartedEvent += OnTaskStarted; } public async Task LoadAsync(string taskId) { _taskId = taskId; - LiveLines.Clear(); + LiveText = ""; + _formatter = new StreamLineFormatter(); var task = await _taskRepo.GetByIdAsync(taskId); if (task is null) return; @@ -79,6 +85,13 @@ public partial class TaskDetailViewModel : ViewModelBase Description = task.Description; Result = task.Result; 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(); StatusChoice = task.Status.ToString(); CommitType = task.CommitType; @@ -152,7 +165,8 @@ public partial class TaskDetailViewModel : ViewModelBase LogPath = null; StatusText = ""; HasWorktree = false; - LiveLines.Clear(); + LiveText = ""; + _formatter = new StreamLineFormatter(); Tags.Clear(); NewTagInput = ""; StatusChoice = "Manual"; @@ -259,9 +273,27 @@ public partial class TaskDetailViewModel : ViewModelBase private void OnTaskMessage(string taskId, string line) { if (taskId != _taskId) return; - if (LiveLines.Count >= MaxLiveLines) - LiveLines.RemoveAt(0); - LiveLines.Add(line); + var formatted = _formatter.FormatLine(line); + if (formatted is not null) + { + 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) diff --git a/src/ClaudeDo.Ui/Views/TaskDetailView.axaml b/src/ClaudeDo.Ui/Views/TaskDetailView.axaml index 2f605a9..7887d63 100644 --- a/src/ClaudeDo.Ui/Views/TaskDetailView.axaml +++ b/src/ClaudeDo.Ui/Views/TaskDetailView.axaml @@ -107,17 +107,19 @@ - - - - - - - - + CornerRadius="6" Padding="6" MaxHeight="300"> + + diff --git a/src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs b/src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs index 70546b3..b8e3b79 100644 --- a/src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs +++ b/src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs @@ -1,3 +1,4 @@ +using System.ComponentModel; using Avalonia.Controls; using Avalonia.Input; using Avalonia.Interactivity; @@ -31,4 +32,22 @@ public partial class TaskDetailView : UserControl { this.FindControl("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("LiveOutputScroll"); + scroll?.ScrollToEnd(); + } + } }