feat(ui): live task updates from worker events + planning polish
Wire TasksIslandViewModel to TaskUpdated/WorktreeUpdated/TaskMessage worker events so rows refresh without a full reload; add ForegroundHelper to permit wt.exe to take foreground on planning launch; misc UI polish on lists, task rows and settings modal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,8 +5,8 @@ namespace ClaudeDo.Ui.ViewModels.Islands;
|
||||
public sealed partial class ListNavItemViewModel : ViewModelBase
|
||||
{
|
||||
public required string Id { get; init; }
|
||||
public required string Name { get; init; }
|
||||
public required ListKind Kind { get; init; }
|
||||
[ObservableProperty] private string _name = "";
|
||||
[ObservableProperty] private int _count;
|
||||
[ObservableProperty] private bool _isActive;
|
||||
[ObservableProperty] private string? _workingDir;
|
||||
|
||||
@@ -40,8 +40,9 @@ public sealed partial class ListsIslandViewModel : ViewModelBase
|
||||
private async System.Threading.Tasks.Task OpenListSettingsAsync(ListNavItemViewModel? row)
|
||||
{
|
||||
if (row is null || ShowListSettingsModal is null || _services is null) return;
|
||||
var rawId = row.Id.StartsWith("user:", StringComparison.Ordinal) ? row.Id["user:".Length..] : row.Id;
|
||||
var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
|
||||
await vm.LoadAsync(row.Id, row.Name, row.WorkingDir, row.DefaultCommitType);
|
||||
await vm.LoadAsync(rawId, row.Name, row.WorkingDir, row.DefaultCommitType);
|
||||
await ShowListSettingsModal(vm);
|
||||
await RefreshRowAsync(row.Id);
|
||||
}
|
||||
@@ -169,6 +170,46 @@ public sealed partial class ListsIslandViewModel : ViewModelBase
|
||||
[RelayCommand]
|
||||
private void Select(ListNavItemViewModel item) => SelectedList = item;
|
||||
|
||||
[RelayCommand]
|
||||
private async Task CreateListAsync()
|
||||
{
|
||||
var entity = new ListEntity
|
||||
{
|
||||
Id = Guid.NewGuid().ToString("N"),
|
||||
Name = "New list",
|
||||
DefaultCommitType = "chore",
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
};
|
||||
|
||||
await using (var ctx = await _dbFactory.CreateDbContextAsync())
|
||||
{
|
||||
var lists = new ListRepository(ctx);
|
||||
await lists.AddAsync(entity);
|
||||
}
|
||||
|
||||
var item = new ListNavItemViewModel
|
||||
{
|
||||
Id = $"user:{entity.Id}",
|
||||
Name = entity.Name,
|
||||
Kind = ListKind.User,
|
||||
IconKey = "Folder",
|
||||
DotColorKey = "Moss",
|
||||
WorkingDir = entity.WorkingDir,
|
||||
DefaultCommitType = entity.DefaultCommitType,
|
||||
};
|
||||
Items.Add(item);
|
||||
UserLists.Add(item);
|
||||
SelectedList = item;
|
||||
|
||||
if (ShowListSettingsModal is not null && _services is not null)
|
||||
{
|
||||
var vm = _services.GetRequiredService<ListSettingsModalViewModel>();
|
||||
await vm.LoadAsync(entity.Id, entity.Name, entity.WorkingDir, entity.DefaultCommitType);
|
||||
await ShowListSettingsModal(vm);
|
||||
await RefreshRowAsync(item.Id);
|
||||
}
|
||||
}
|
||||
|
||||
partial void OnSelectedListChanged(ListNavItemViewModel? value)
|
||||
{
|
||||
foreach (var i in Items) i.IsActive = ReferenceEquals(i, value);
|
||||
@@ -188,6 +229,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase
|
||||
var entity = await lists.GetByIdAsync(rawId);
|
||||
if (entity is null) return;
|
||||
|
||||
row.Name = entity.Name;
|
||||
row.WorkingDir = entity.WorkingDir;
|
||||
row.DefaultCommitType = entity.DefaultCommitType;
|
||||
}
|
||||
|
||||
@@ -101,25 +101,27 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
partial void OnDiffDeletionsChanged(int value) { OnPropertyChanged(nameof(HasDiff)); OnPropertyChanged(nameof(DiffDeletionsText)); }
|
||||
|
||||
public static TaskRowViewModel FromEntity(TaskEntity t)
|
||||
{
|
||||
var row = new TaskRowViewModel { Id = t.Id, CreatedAt = t.CreatedAt };
|
||||
row.UpdateFromEntity(t);
|
||||
return row;
|
||||
}
|
||||
|
||||
public void UpdateFromEntity(TaskEntity t)
|
||||
{
|
||||
var (add, del) = ParseDiffStat(t.Worktree?.DiffStat);
|
||||
return new TaskRowViewModel
|
||||
{
|
||||
Id = t.Id,
|
||||
Title = t.Title,
|
||||
ListName = t.List?.Name ?? "",
|
||||
Done = t.Status == TaskStatus.Done,
|
||||
IsStarred = t.IsStarred,
|
||||
IsMyDay = t.IsMyDay,
|
||||
Status = t.Status,
|
||||
Branch = t.Worktree?.BranchName,
|
||||
DiffStat = t.Worktree?.DiffStat,
|
||||
ScheduledFor = t.ScheduledFor,
|
||||
DiffAdditions = add,
|
||||
DiffDeletions = del,
|
||||
CreatedAt = t.CreatedAt,
|
||||
ParentTaskId = t.ParentTaskId,
|
||||
};
|
||||
Title = t.Title;
|
||||
ListName = t.List?.Name ?? "";
|
||||
Done = t.Status == TaskStatus.Done;
|
||||
IsStarred = t.IsStarred;
|
||||
IsMyDay = t.IsMyDay;
|
||||
Status = t.Status;
|
||||
Branch = t.Worktree?.BranchName;
|
||||
DiffStat = t.Worktree?.DiffStat;
|
||||
ScheduledFor = t.ScheduledFor;
|
||||
DiffAdditions = add;
|
||||
DiffDeletions = del;
|
||||
ParentTaskId = t.ParentTaskId;
|
||||
}
|
||||
|
||||
// Best-effort parse of diff stat strings like "+12 -3" or "12 additions, 3 deletions".
|
||||
|
||||
@@ -49,6 +49,70 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
||||
{
|
||||
_dbFactory = dbFactory;
|
||||
_worker = worker;
|
||||
if (_worker is not null)
|
||||
{
|
||||
_worker.TaskUpdatedEvent += OnWorkerTaskUpdated;
|
||||
_worker.WorktreeUpdatedEvent += OnWorkerTaskUpdated;
|
||||
_worker.TaskMessageEvent += OnWorkerTaskMessage;
|
||||
}
|
||||
}
|
||||
|
||||
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 OnWorkerTaskUpdated(string taskId)
|
||||
{
|
||||
var list = _currentList;
|
||||
if (list is null) return;
|
||||
|
||||
try
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync();
|
||||
var entity = await db.Tasks
|
||||
.Include(t => t.List)
|
||||
.Include(t => t.Worktree)
|
||||
.FirstOrDefaultAsync(t => t.Id == taskId);
|
||||
|
||||
var existing = Items.FirstOrDefault(r => r.Id == taskId);
|
||||
|
||||
if (entity is null)
|
||||
{
|
||||
if (existing is not null) Items.Remove(existing);
|
||||
}
|
||||
else
|
||||
{
|
||||
var matches = TaskMatchesList(entity, list);
|
||||
if (existing is not null && matches) existing.UpdateFromEntity(entity);
|
||||
else if (existing is not null) Items.Remove(existing);
|
||||
else if (matches) { LoadForList(list); return; }
|
||||
else return;
|
||||
}
|
||||
|
||||
Regroup();
|
||||
UpdateSubtitle();
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
|
||||
private static bool TaskMatchesList(TaskEntity t, ListNavItemViewModel list) => list.Kind switch
|
||||
{
|
||||
ListKind.Smart when list.Id == "smart:my-day" => t.IsMyDay,
|
||||
ListKind.Smart when list.Id == "smart:important" => t.IsStarred,
|
||||
ListKind.Smart when list.Id == "smart:planned" => t.ScheduledFor != null,
|
||||
ListKind.Virtual when list.Id == "virtual:queued" => t.Status == TaskStatus.Queued,
|
||||
ListKind.Virtual when list.Id == "virtual:running" => t.Status == TaskStatus.Running,
|
||||
ListKind.Virtual when list.Id == "virtual:review" => t.Status == TaskStatus.Done && t.Worktree?.State == WorktreeState.Active,
|
||||
ListKind.User => $"user:{t.ListId}" == list.Id,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
private void OnCurrentListPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(ListNavItemViewModel.Name) && sender is ListNavItemViewModel vm)
|
||||
HeaderTitle = vm.Name;
|
||||
}
|
||||
|
||||
public void LoadForList(ListNavItemViewModel? list)
|
||||
@@ -58,7 +122,12 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
||||
_loadCts = new CancellationTokenSource();
|
||||
var ct = _loadCts.Token;
|
||||
|
||||
if (_currentList is not null)
|
||||
_currentList.PropertyChanged -= OnCurrentListPropertyChanged;
|
||||
_currentList = list;
|
||||
if (_currentList is not null)
|
||||
_currentList.PropertyChanged += OnCurrentListPropertyChanged;
|
||||
|
||||
Items.Clear();
|
||||
OverdueItems.Clear();
|
||||
OpenItems.Clear();
|
||||
@@ -385,6 +454,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
||||
private async Task OpenPlanningSessionAsync(TaskRowViewModel? row)
|
||||
{
|
||||
if (row is null || row.Status != TaskStatus.Manual) return;
|
||||
ForegroundHelper.AllowAny();
|
||||
try { await _worker!.StartPlanningSessionAsync(row.Id); }
|
||||
catch { }
|
||||
}
|
||||
@@ -412,6 +482,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
||||
switch (choice)
|
||||
{
|
||||
case UnfinishedPlanningModalResult.Resume:
|
||||
ForegroundHelper.AllowAny();
|
||||
await _worker.ResumePlanningSessionAsync(row.Id);
|
||||
break;
|
||||
case UnfinishedPlanningModalResult.FinalizeNow:
|
||||
|
||||
Reference in New Issue
Block a user