feat(ui): replay run log in session terminal, drop per-row live tail
Set the task's log path when the run is created (not at completion) so the session terminal can replay live output when the user navigates away and back mid-run. Remove the now-redundant inline per-row live tail (LiveTail / HasLiveTail / TaskMessageEvent) and scroll the terminal to end after the next layout pass so wrapping lines aren't clipped.
This commit is contained in:
@@ -17,7 +17,6 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
[ObservableProperty] private PlanningPhase _planningPhase;
|
[ObservableProperty] private PlanningPhase _planningPhase;
|
||||||
[ObservableProperty] private string? _branch;
|
[ObservableProperty] private string? _branch;
|
||||||
[ObservableProperty] private string? _diffStat;
|
[ObservableProperty] private string? _diffStat;
|
||||||
[ObservableProperty] private string? _liveTail;
|
|
||||||
[ObservableProperty] private DateTime? _scheduledFor;
|
[ObservableProperty] private DateTime? _scheduledFor;
|
||||||
[ObservableProperty] private int _diffAdditions;
|
[ObservableProperty] private int _diffAdditions;
|
||||||
[ObservableProperty] private int _diffDeletions;
|
[ObservableProperty] private int _diffDeletions;
|
||||||
@@ -74,7 +73,6 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
&& PlanningPhase == PlanningPhase.Finalized
|
&& PlanningPhase == PlanningPhase.Finalized
|
||||||
&& !HasQueuedSubtasks;
|
&& !HasQueuedSubtasks;
|
||||||
public bool HasSchedule => ScheduledFor.HasValue;
|
public bool HasSchedule => ScheduledFor.HasValue;
|
||||||
public bool HasLiveTail => IsRunning && !string.IsNullOrEmpty(LiveTail);
|
|
||||||
|
|
||||||
public string DiffAdditionsText => $"+{DiffAdditions}";
|
public string DiffAdditionsText => $"+{DiffAdditions}";
|
||||||
public string DiffDeletionsText => $"−{DiffDeletions}";
|
public string DiffDeletionsText => $"−{DiffDeletions}";
|
||||||
@@ -96,7 +94,6 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
OnPropertyChanged(nameof(IsRunning));
|
OnPropertyChanged(nameof(IsRunning));
|
||||||
OnPropertyChanged(nameof(IsQueued));
|
OnPropertyChanged(nameof(IsQueued));
|
||||||
OnPropertyChanged(nameof(IsWaiting));
|
OnPropertyChanged(nameof(IsWaiting));
|
||||||
OnPropertyChanged(nameof(HasLiveTail));
|
|
||||||
OnPropertyChanged(nameof(IsDraft));
|
OnPropertyChanged(nameof(IsDraft));
|
||||||
OnPropertyChanged(nameof(IsPlanned));
|
OnPropertyChanged(nameof(IsPlanned));
|
||||||
OnPropertyChanged(nameof(CanOpenPlanningSession));
|
OnPropertyChanged(nameof(CanOpenPlanningSession));
|
||||||
@@ -152,7 +149,6 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
partial void OnBranchChanged(string? value) => OnPropertyChanged(nameof(HasBranch));
|
partial void OnBranchChanged(string? value) => OnPropertyChanged(nameof(HasBranch));
|
||||||
partial void OnLiveTailChanged(string? value) => OnPropertyChanged(nameof(HasLiveTail));
|
|
||||||
partial void OnDoneChanged(bool value) => OnPropertyChanged(nameof(IsOverdue));
|
partial void OnDoneChanged(bool value) => OnPropertyChanged(nameof(IsOverdue));
|
||||||
partial void OnScheduledForChanged(DateTime? value)
|
partial void OnScheduledForChanged(DateTime? value)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -56,18 +56,11 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
_worker.TaskUpdatedEvent += OnWorkerTaskUpdated;
|
_worker.TaskUpdatedEvent += OnWorkerTaskUpdated;
|
||||||
_worker.WorktreeUpdatedEvent += OnWorkerTaskUpdated;
|
_worker.WorktreeUpdatedEvent += OnWorkerTaskUpdated;
|
||||||
_worker.TaskMessageEvent += OnWorkerTaskMessage;
|
|
||||||
_worker.ListUpdatedEvent += OnWorkerListUpdated;
|
_worker.ListUpdatedEvent += OnWorkerListUpdated;
|
||||||
_worker.ConnectionRestoredEvent += () => LoadForList(_currentList);
|
_worker.ConnectionRestoredEvent += () => LoadForList(_currentList);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnWorkerTaskMessage(string taskId, string line)
|
|
||||||
{
|
|
||||||
var row = Items.FirstOrDefault(r => r.Id == taskId);
|
|
||||||
if (row is not null) row.LiveTail = line;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async void OnWorkerListUpdated(string listId)
|
private async void OnWorkerListUpdated(string listId)
|
||||||
{
|
{
|
||||||
// Mirror the renamed list onto every task row that references it,
|
// Mirror the renamed list onto every task row that references it,
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
using System.Collections.Specialized;
|
using System.Collections.Specialized;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Threading;
|
|
||||||
using ClaudeDo.Ui.ViewModels.Islands;
|
using ClaudeDo.Ui.ViewModels.Islands;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Views.Islands;
|
namespace ClaudeDo.Ui.Views.Islands;
|
||||||
@@ -9,16 +8,29 @@ public partial class SessionTerminalView : UserControl
|
|||||||
{
|
{
|
||||||
public SessionTerminalView() { InitializeComponent(); }
|
public SessionTerminalView() { InitializeComponent(); }
|
||||||
|
|
||||||
|
private DetailsIslandViewModel? _boundVm;
|
||||||
|
|
||||||
protected override void OnDataContextChanged(EventArgs e)
|
protected override void OnDataContextChanged(EventArgs e)
|
||||||
{
|
{
|
||||||
base.OnDataContextChanged(e);
|
base.OnDataContextChanged(e);
|
||||||
if (DataContext is DetailsIslandViewModel vm)
|
if (_boundVm is not null)
|
||||||
vm.Log.CollectionChanged += OnLogChanged;
|
_boundVm.Log.CollectionChanged -= OnLogChanged;
|
||||||
|
_boundVm = DataContext as DetailsIslandViewModel;
|
||||||
|
if (_boundVm is not null)
|
||||||
|
_boundVm.Log.CollectionChanged += OnLogChanged;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnLogChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
private void OnLogChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||||
{
|
{
|
||||||
if (e.Action != NotifyCollectionChangedAction.Add) return;
|
if (e.Action != NotifyCollectionChangedAction.Add) return;
|
||||||
Dispatcher.UIThread.Post(() => LogScroll.ScrollToEnd(), DispatcherPriority.Background);
|
// Scroll after the next layout pass so the freshly-added (wrapping) line
|
||||||
|
// is measured first — otherwise ScrollToEnd stops short and clips it.
|
||||||
|
EventHandler? handler = null;
|
||||||
|
handler = (_, _) =>
|
||||||
|
{
|
||||||
|
LogScroll.LayoutUpdated -= handler;
|
||||||
|
LogScroll.ScrollToEnd();
|
||||||
|
};
|
||||||
|
LogScroll.LayoutUpdated += handler;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -175,20 +175,6 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Live-tail row (visible when running + has tail) -->
|
|
||||||
<Border Classes="task-live-tail" IsVisible="{Binding HasLiveTail}">
|
|
||||||
<StackPanel Spacing="3">
|
|
||||||
<TextBlock Text="{Binding LiveTail}"
|
|
||||||
TextTrimming="CharacterEllipsis" MaxLines="1"/>
|
|
||||||
<Grid Height="3" HorizontalAlignment="Stretch">
|
|
||||||
<Rectangle Fill="{DynamicResource Surface3Brush}"
|
|
||||||
HorizontalAlignment="Stretch" RadiusX="1.5" RadiusY="1.5"/>
|
|
||||||
<Rectangle Fill="{DynamicResource MossBrush}"
|
|
||||||
HorizontalAlignment="Left" Width="60" RadiusX="1.5" RadiusY="1.5"/>
|
|
||||||
</Grid>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Star toggle -->
|
<!-- Star toggle -->
|
||||||
|
|||||||
@@ -238,6 +238,11 @@ public sealed class TaskRunner
|
|||||||
{
|
{
|
||||||
var runRepo = new TaskRunRepository(context);
|
var runRepo = new TaskRunRepository(context);
|
||||||
await runRepo.AddAsync(run, ct);
|
await runRepo.AddAsync(run, ct);
|
||||||
|
|
||||||
|
// Point the task at this run's log immediately so the UI can replay
|
||||||
|
// live output when the user navigates away and back mid-run.
|
||||||
|
var taskRepo = new TaskRepository(context);
|
||||||
|
await taskRepo.SetLogPathAsync(taskId, logPath, ct);
|
||||||
}
|
}
|
||||||
|
|
||||||
await _broadcaster.RunCreated(taskId, runNumber, isRetry);
|
await _broadcaster.RunCreated(taskId, runNumber, isRetry);
|
||||||
@@ -277,9 +282,6 @@ public sealed class TaskRunner
|
|||||||
{
|
{
|
||||||
var runRepo = new TaskRunRepository(context);
|
var runRepo = new TaskRunRepository(context);
|
||||||
await runRepo.UpdateAsync(run, CancellationToken.None);
|
await runRepo.UpdateAsync(run, CancellationToken.None);
|
||||||
|
|
||||||
var taskRepo = new TaskRepository(context);
|
|
||||||
await taskRepo.SetLogPathAsync(taskId, logPath, CancellationToken.None);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
@@ -296,9 +298,6 @@ public sealed class TaskRunner
|
|||||||
using var context = _dbFactory.CreateDbContext();
|
using var context = _dbFactory.CreateDbContext();
|
||||||
var runRepo = new TaskRunRepository(context);
|
var runRepo = new TaskRunRepository(context);
|
||||||
await runRepo.UpdateAsync(run, CancellationToken.None);
|
await runRepo.UpdateAsync(run, CancellationToken.None);
|
||||||
|
|
||||||
var taskRepo = new TaskRepository(context);
|
|
||||||
await taskRepo.SetLogPathAsync(taskId, logPath, CancellationToken.None);
|
|
||||||
}
|
}
|
||||||
catch (Exception updateEx)
|
catch (Exception updateEx)
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user