feat(ui): TasksIslandViewModel with smart/virtual/user filtering

Uses IDbContextFactory directly (singleton-safe). Adds ToggleDone,
ToggleStar, Select commands. Fixes pre-existing SessionTerminalView
compiled-binding error on Classes property via x:CompileBindings="False".

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-04-20 10:22:11 +02:00
parent f167120c90
commit 0034accb4f
3 changed files with 157 additions and 4 deletions

View File

@@ -103,8 +103,12 @@ sealed class Program
// Islands shell VMs // Islands shell VMs
sc.AddSingleton<ListsIslandViewModel>(); sc.AddSingleton<ListsIslandViewModel>();
sc.AddSingleton<TasksIslandViewModel>(); sc.AddSingleton<TasksIslandViewModel>(sp =>
sc.AddSingleton<DetailsIslandViewModel>(); new TasksIslandViewModel(sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>()));
sc.AddSingleton<DetailsIslandViewModel>(sp =>
new DetailsIslandViewModel(
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
sp.GetRequiredService<WorkerClient>()));
sc.AddSingleton<IslandsShellViewModel>(); sc.AddSingleton<IslandsShellViewModel>();
return sc.BuildServiceProvider(); return sc.BuildServiceProvider();

View File

@@ -1,12 +1,127 @@
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel; using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using Microsoft.EntityFrameworkCore;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Ui.ViewModels.Islands; namespace ClaudeDo.Ui.ViewModels.Islands;
public sealed partial class TasksIslandViewModel : ViewModelBase public sealed partial class TasksIslandViewModel : ViewModelBase
{ {
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
private ListNavItemViewModel? _currentList;
public event EventHandler? SelectionChanged; public event EventHandler? SelectionChanged;
public ObservableCollection<TaskRowViewModel> Items { get; } = new();
[ObservableProperty] private string _newTaskTitle = "";
[ObservableProperty] private TaskRowViewModel? _selectedTask; [ObservableProperty] private TaskRowViewModel? _selectedTask;
public void LoadForList(ListNavItemViewModel? list) { /* Phase 5 */ } [ObservableProperty] private string _headerTitle = "";
partial void OnSelectedTaskChanged(TaskRowViewModel? value) => [ObservableProperty] private string _headerEyebrow = "";
[ObservableProperty] private string _subtitle = "";
public TasksIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory)
{
_dbFactory = dbFactory;
}
public async void LoadForList(ListNavItemViewModel? list)
{
_currentList = list;
Items.Clear();
if (list is null) return;
HeaderTitle = list.Name;
HeaderEyebrow = DateTime.Now.ToString("dddd · MMM dd").ToUpperInvariant();
await using var db = await _dbFactory.CreateDbContextAsync();
var all = await db.Tasks
.Include(t => t.List)
.Include(t => t.Worktree)
.ToListAsync();
IEnumerable<TaskEntity> filtered = list.Kind switch
{
ListKind.Smart when list.Id == "smart:my-day" => all.Where(t => t.IsMyDay),
ListKind.Smart when list.Id == "smart:important" => all.Where(t => t.IsStarred),
ListKind.Smart when list.Id == "smart:planned" => all.Where(t => t.ScheduledFor != null),
ListKind.Virtual when list.Id == "virtual:running" => all.Where(t => t.Status == TaskStatus.Running),
ListKind.Virtual when list.Id == "virtual:review" => all.Where(t => t.Status == TaskStatus.Done && t.Worktree?.State == WorktreeState.Active),
ListKind.User => all.Where(t => $"user:{t.ListId}" == list.Id),
_ => Enumerable.Empty<TaskEntity>(),
};
foreach (var t in filtered)
Items.Add(TaskRowViewModel.FromEntity(t));
UpdateSubtitle();
}
private void UpdateSubtitle()
{
var open = Items.Count(i => !i.Done);
var running = Items.Count(i => i.Status == TaskStatus.Running);
var review = Items.Count(i => i.Status == TaskStatus.Done && i.Branch != null);
Subtitle = $"{open} open · {running} running · {review} in review";
}
[RelayCommand]
private async Task AddAsync()
{
if (string.IsNullOrWhiteSpace(NewTaskTitle) || _currentList?.Kind != ListKind.User) return;
var listId = _currentList.Id["user:".Length..];
var entity = new TaskEntity
{
Id = Guid.NewGuid().ToString("N"),
ListId = listId,
Title = NewTaskTitle.Trim(),
CreatedAt = DateTime.UtcNow,
};
await using var db = await _dbFactory.CreateDbContextAsync();
db.Tasks.Add(entity);
await db.SaveChangesAsync();
Items.Insert(0, TaskRowViewModel.FromEntity(entity));
NewTaskTitle = "";
UpdateSubtitle();
}
[RelayCommand]
private async Task ToggleDoneAsync(TaskRowViewModel row)
{
row.Done = !row.Done;
await using var db = await _dbFactory.CreateDbContextAsync();
var entity = await db.Tasks.FirstOrDefaultAsync(t => t.Id == row.Id);
if (entity != null)
{
entity.Status = row.Done ? TaskStatus.Done : TaskStatus.Manual;
row.Status = entity.Status;
await db.SaveChangesAsync();
}
UpdateSubtitle();
}
[RelayCommand]
private async Task ToggleStarAsync(TaskRowViewModel row)
{
row.IsStarred = !row.IsStarred;
await using var db = await _dbFactory.CreateDbContextAsync();
var entity = await db.Tasks.FirstOrDefaultAsync(t => t.Id == row.Id);
if (entity != null)
{
entity.IsStarred = row.IsStarred;
await db.SaveChangesAsync();
}
}
[RelayCommand]
private void Select(TaskRowViewModel row) => SelectedTask = row;
partial void OnSelectedTaskChanged(TaskRowViewModel? value)
{
foreach (var i in Items) i.IsSelected = ReferenceEquals(i, value);
SelectionChanged?.Invoke(this, EventArgs.Empty); SelectionChanged?.Invoke(this, EventArgs.Empty);
} }
}

View File

@@ -0,0 +1,34 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
x:Class="ClaudeDo.Ui.Views.Islands.SessionTerminalView"
x:DataType="vm:DetailsIslandViewModel">
<Border Classes="terminal" Padding="10">
<DockPanel LastChildFill="True">
<!-- Prompt input bar (docked to bottom) -->
<Grid DockPanel.Dock="Bottom" ColumnDefinitions="Auto,*" Margin="0,8,0,0">
<TextBlock Grid.Column="0" Text="[you]" FontFamily="{DynamicResource MonoFamily}"
FontSize="11" Foreground="{DynamicResource MossBrush}"
VerticalAlignment="Center" Margin="0,0,8,0"/>
<TextBox Grid.Column="1" Text="{Binding PromptInput, Mode=TwoWay}"
FontFamily="{DynamicResource MonoFamily}" FontSize="11">
<TextBox.KeyBindings>
<KeyBinding Gesture="Enter" Command="{Binding SendPromptCommand}"/>
</TextBox.KeyBindings>
</TextBox>
</Grid>
<!-- Log output -->
<ScrollViewer Name="LogScroll" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding Log}">
<ItemsControl.ItemTemplate>
<DataTemplate x:CompileBindings="False" DataType="vm:LogLineViewModel">
<TextBlock Text="{Binding Text}" Classes="{Binding ClassName}"
FontFamily="{DynamicResource MonoFamily}" FontSize="11"
TextWrapping="Wrap"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
</Border>
</UserControl>