feat(ui): add worker log state and 30s timer to shell VM
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -11,6 +11,10 @@
|
|||||||
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="8.0.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<InternalsVisibleTo Include="ClaudeDo.Ui.Tests" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
|
using Avalonia.Threading;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using ClaudeDo.Data.Models;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
using ClaudeDo.Ui.ViewModels.Islands;
|
using ClaudeDo.Ui.ViewModels.Islands;
|
||||||
|
|
||||||
@@ -7,33 +9,44 @@ namespace ClaudeDo.Ui.ViewModels;
|
|||||||
|
|
||||||
public sealed partial class IslandsShellViewModel : ViewModelBase
|
public sealed partial class IslandsShellViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
public ListsIslandViewModel Lists { get; }
|
public ListsIslandViewModel? Lists { get; }
|
||||||
public TasksIslandViewModel Tasks { get; }
|
public TasksIslandViewModel? Tasks { get; }
|
||||||
public DetailsIslandViewModel Details { get; }
|
public DetailsIslandViewModel? Details { get; }
|
||||||
public WorkerClient Worker { get; }
|
public WorkerClient? Worker { get; }
|
||||||
|
|
||||||
public string ConnectionText =>
|
public string ConnectionText =>
|
||||||
Worker.IsConnected ? "Online"
|
Worker?.IsConnected == true ? "Online"
|
||||||
: Worker.IsReconnecting ? "Connecting…"
|
: Worker?.IsReconnecting == true ? "Connecting…"
|
||||||
: "Offline";
|
: "Offline";
|
||||||
|
|
||||||
public bool IsOffline => !Worker.IsConnected && !Worker.IsReconnecting;
|
public bool IsOffline => Worker?.IsConnected != true && Worker?.IsReconnecting != true;
|
||||||
|
|
||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private double _windowWidth = 1280;
|
private double _windowWidth = 1280;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private string? _workerLogText;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private WorkerLogLevel _workerLogLevel;
|
||||||
|
|
||||||
|
[ObservableProperty]
|
||||||
|
private bool _isWorkerLogVisible;
|
||||||
|
|
||||||
public bool ShowDetails => WindowWidth >= 1100;
|
public bool ShowDetails => WindowWidth >= 1100;
|
||||||
public bool ShowLists => WindowWidth >= 780;
|
public bool ShowLists => WindowWidth >= 780;
|
||||||
|
|
||||||
[RelayCommand]
|
private readonly System.Timers.Timer _clearTimer = new(30_000) { AutoReset = false };
|
||||||
private void FocusSearch() => Lists.RequestFocusSearch();
|
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void FocusAddTask() => Tasks.RequestFocusAddTask();
|
private void FocusSearch() => Lists?.RequestFocusSearch();
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void FocusAddTask() => Tasks?.RequestFocusAddTask();
|
||||||
|
|
||||||
public async Task ToggleSelectedDoneAsync()
|
public async Task ToggleSelectedDoneAsync()
|
||||||
{
|
{
|
||||||
if (Tasks.SelectedTask is { } row)
|
if (Tasks?.SelectedTask is { } row)
|
||||||
await Tasks.ToggleDoneCommand.ExecuteAsync(row);
|
await Tasks.ToggleDoneCommand.ExecuteAsync(row);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -43,6 +56,25 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
|
|||||||
OnPropertyChanged(nameof(ShowLists));
|
OnPropertyChanged(nameof(ShowLists));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void OnWorkerLogReceived(WorkerLogEntry entry)
|
||||||
|
{
|
||||||
|
var hhmm = entry.TimestampUtc.ToLocalTime().ToString("HH:mm");
|
||||||
|
WorkerLogText = $"{hhmm} · {entry.Message}";
|
||||||
|
WorkerLogLevel = entry.Level;
|
||||||
|
IsWorkerLogVisible = true;
|
||||||
|
_clearTimer.Stop();
|
||||||
|
_clearTimer.Start();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearWorkerLog()
|
||||||
|
{
|
||||||
|
IsWorkerLogVisible = false;
|
||||||
|
WorkerLogText = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// For tests only — does NOT wire up events.
|
||||||
|
internal IslandsShellViewModel() { }
|
||||||
|
|
||||||
public IslandsShellViewModel(
|
public IslandsShellViewModel(
|
||||||
ListsIslandViewModel lists,
|
ListsIslandViewModel lists,
|
||||||
TasksIslandViewModel tasks,
|
TasksIslandViewModel tasks,
|
||||||
@@ -68,6 +100,14 @@ public sealed partial class IslandsShellViewModel : ViewModelBase
|
|||||||
OnPropertyChanged(nameof(IsOffline));
|
OnPropertyChanged(nameof(IsOffline));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
Worker.WorkerLogReceivedEvent += OnWorkerLogReceived;
|
||||||
|
_clearTimer.Elapsed += (_, _) =>
|
||||||
|
{
|
||||||
|
if (Dispatcher.UIThread.CheckAccess())
|
||||||
|
ClearWorkerLog();
|
||||||
|
else
|
||||||
|
Dispatcher.UIThread.Post(ClearWorkerLog);
|
||||||
|
};
|
||||||
_ = Lists.LoadAsync();
|
_ = Lists.LoadAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
using System;
|
||||||
|
using ClaudeDo.Data.Models;
|
||||||
|
using ClaudeDo.Ui.Services;
|
||||||
|
using ClaudeDo.Ui.ViewModels;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Tests;
|
||||||
|
|
||||||
|
public class IslandsShellViewModelWorkerLogTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void Receiving_event_sets_text_level_and_visible()
|
||||||
|
{
|
||||||
|
var vm = new IslandsShellViewModel();
|
||||||
|
var at = new DateTime(2026, 4, 23, 14, 32, 0, DateTimeKind.Utc);
|
||||||
|
|
||||||
|
vm.OnWorkerLogReceived(new WorkerLogEntry("Created worktree for \"X\"", WorkerLogLevel.Info, at));
|
||||||
|
|
||||||
|
Assert.True(vm.IsWorkerLogVisible);
|
||||||
|
Assert.Equal(WorkerLogLevel.Info, vm.WorkerLogLevel);
|
||||||
|
Assert.Contains("Created worktree for \"X\"", vm.WorkerLogText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Second_event_replaces_first()
|
||||||
|
{
|
||||||
|
var vm = new IslandsShellViewModel();
|
||||||
|
vm.OnWorkerLogReceived(new WorkerLogEntry("first", WorkerLogLevel.Info, DateTime.UtcNow));
|
||||||
|
vm.OnWorkerLogReceived(new WorkerLogEntry("second", WorkerLogLevel.Success, DateTime.UtcNow));
|
||||||
|
|
||||||
|
Assert.Contains("second", vm.WorkerLogText);
|
||||||
|
Assert.Equal(WorkerLogLevel.Success, vm.WorkerLogLevel);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ClearWorkerLog_hides_line()
|
||||||
|
{
|
||||||
|
var vm = new IslandsShellViewModel();
|
||||||
|
vm.OnWorkerLogReceived(new WorkerLogEntry("msg", WorkerLogLevel.Info, DateTime.UtcNow));
|
||||||
|
|
||||||
|
vm.ClearWorkerLog();
|
||||||
|
|
||||||
|
Assert.False(vm.IsWorkerLogVisible);
|
||||||
|
Assert.Null(vm.WorkerLogText);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Text_is_formatted_as_HHmm_dot_message_local_time()
|
||||||
|
{
|
||||||
|
var vm = new IslandsShellViewModel();
|
||||||
|
var utc = new DateTime(2026, 4, 23, 12, 0, 0, DateTimeKind.Utc);
|
||||||
|
var expectedLocalHhmm = utc.ToLocalTime().ToString("HH:mm");
|
||||||
|
|
||||||
|
vm.OnWorkerLogReceived(new WorkerLogEntry("hello", WorkerLogLevel.Info, utc));
|
||||||
|
|
||||||
|
Assert.StartsWith(expectedLocalHhmm + " · ", vm.WorkerLogText);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user