64 KiB
UI Rewrite — Islands Layout Implementation Plan
For agentic workers: REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (
- [ ]) syntax for tracking.
Goal: Replace the current ClaudeDo.Ui with a high-fidelity three-island Avalonia interface per the design handoff at docs/UI Rewrite/design_handoff_claudedo/.
Architecture: Data-layer additions (IsStarred, IsMyDay, Notes columns; default-list seeding); a new Design/ resource folder (Tokens.axaml, IslandStyles.axaml); embedded Inter Tight + JetBrains Mono fonts; a chromeless MainWindow containing a three-column Grid of island Borders — Lists / Tasks / Details — backed by new IslandsShellViewModel, ListsIslandViewModel, TasksIslandViewModel, DetailsIslandViewModel. Existing WorkerClient, Repositories and SignalR plumbing are preserved.
Tech Stack: .NET 8.0, Avalonia 12.0.0 (Fluent theme), CommunityToolkit.Mvvm, Entity Framework Core (SQLite), xUnit.
Reference files:
docs/UI Rewrite/design_handoff_claudedo/README.md— full handoffdocs/UI Rewrite/design_handoff_claudedo/Tokens.axaml— design tokensdocs/UI Rewrite/design_handoff_claudedo/IslandStyles.axaml— control stylesdocs/UI Rewrite/design_handoff_claudedo/styles.css— measurement source of truthdocs/UI Rewrite/design_handoff_claudedo/ClaudeDo-standalone.html— interactive reference
Confirmed decisions (from brainstorm):
- Full rewrite of
ClaudeDo.Ui(Views + ViewModels).WorkerClient, repositories, SignalR untouched. - Schema: add
IsStarred,IsMyDay,NotestoTaskEntity. SeedMy Day,Important,PlannedLists on install. RunningandReviewlists are virtual filters (status-based), not seeded list rows.- Window: chromeless (
SystemDecorations="None"+ExtendClientAreaToDecorationsHint="True"). - Fonts: embed
Inter TightandJetBrains Mono. - Resource folder:
src/ClaudeDo.Ui/Design/.
File Structure
Data layer (new / modified):
- Modify:
src/ClaudeDo.Data/Models/TaskEntity.cs— add 3 properties - Modify:
src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs— column mappings - Create:
src/ClaudeDo.Data/Migrations/<timestamp>_AddTaskFlagsAndNotes.cs— EF migration - Modify:
src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs— generated update - Modify:
src/ClaudeDo.Installer/...(seed call site — discover during Phase 1)
UI design assets (new):
- Create:
src/ClaudeDo.Ui/Design/Tokens.axaml - Create:
src/ClaudeDo.Ui/Design/IslandStyles.axaml - Create:
src/ClaudeDo.Ui/Assets/Fonts/InterTight-*.ttf(Regular, Medium, SemiBold) - Create:
src/ClaudeDo.Ui/Assets/Fonts/JetBrainsMono-Regular.ttf - Modify:
src/ClaudeDo.Ui/App.axaml— merge Tokens + Styles - Modify:
src/ClaudeDo.Ui/ClaudeDo.Ui.csproj— embed font assets
Shell + Islands (new — replaces existing Views/ViewModels):
- Create:
src/ClaudeDo.Ui/Views/MainWindow.axaml(replace) — chromeless, 3-column Grid - Create:
src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs(replaceMainWindowViewModel) - Create:
src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml - Create:
src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs - Create:
src/ClaudeDo.Ui/ViewModels/Islands/ListNavItemViewModel.cs - Create:
src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml - Create:
src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs - Create:
src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml(UserControl) - Create:
src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs - Create:
src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml - Create:
src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs - Create:
src/ClaudeDo.Ui/Views/Islands/AgentStripView.axaml - Create:
src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml - Create:
src/ClaudeDo.Ui/ViewModels/Islands/LogLineViewModel.cs
Modals (new):
- Create:
src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml - Create:
src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs - Create:
src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml - Create:
src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs
To delete (after rewrite verified working):
src/ClaudeDo.Ui/Views/StatusBarView.axaml(.cs),TaskListView.axaml(.cs),TaskDetailView.axaml(.cs),TaskEditorView.axaml(.cs),ListEditorView.axaml(.cs)src/ClaudeDo.Ui/ViewModels/StatusBarViewModel.cs,TaskListViewModel.cs,TaskDetailViewModel.cs,TaskItemViewModel.cs,MainWindowViewModel.cs,TaskEditorViewModel.cs,ListEditorViewModel.cs,ListItemViewModel.cs,SubtaskItemViewModel.cs
Tests (new):
- Create:
tests/ClaudeDo.Worker.Tests/UiSchema/TaskEntityFlagsTests.cs - Create:
tests/ClaudeDo.Worker.Tests/UiSchema/DefaultListSeedTests.cs
Phase 1 — Schema and seed
Task 1: Add IsStarred, IsMyDay, Notes to TaskEntity
Files:
-
Modify:
src/ClaudeDo.Data/Models/TaskEntity.cs -
Modify:
src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs -
Test:
tests/ClaudeDo.Worker.Tests/UiSchema/TaskEntityFlagsTests.cs(new) -
Step 1: Write failing test —
tests/ClaudeDo.Worker.Tests/UiSchema/TaskEntityFlagsTests.cs
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using Microsoft.EntityFrameworkCore;
using Xunit;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Worker.Tests.UiSchema;
public class TaskEntityFlagsTests : IDisposable
{
private readonly string _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo-flags-{Guid.NewGuid():N}.db");
private ClaudeDoDbContext NewContext()
{
var opts = new DbContextOptionsBuilder<ClaudeDoDbContext>()
.UseSqlite($"Data Source={_dbPath}")
.Options;
var ctx = new ClaudeDoDbContext(opts);
ctx.Database.EnsureCreated();
return ctx;
}
[Fact]
public async Task Persists_IsStarred_IsMyDay_And_Notes()
{
await using var ctx = NewContext();
var list = new ListEntity { Id = "l1", Name = "L", CreatedAt = DateTime.UtcNow };
ctx.Lists.Add(list);
ctx.Tasks.Add(new TaskEntity
{
Id = "t1", ListId = "l1", Title = "T", CreatedAt = DateTime.UtcNow,
IsStarred = true, IsMyDay = true, Notes = "hello"
});
await ctx.SaveChangesAsync();
await using var ctx2 = NewContext();
var loaded = await ctx2.Tasks.SingleAsync();
Assert.True(loaded.IsStarred);
Assert.True(loaded.IsMyDay);
Assert.Equal("hello", loaded.Notes);
}
public void Dispose()
{
if (File.Exists(_dbPath)) File.Delete(_dbPath);
}
}
- Step 2: Run test — verify fail
Run: dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter FullyQualifiedName~TaskEntityFlagsTests
Expected: build error — TaskEntity has no IsStarred/IsMyDay/Notes.
- Step 3: Add properties to
TaskEntity— append before navigation block insrc/ClaudeDo.Data/Models/TaskEntity.cs:
public bool IsStarred { get; set; }
public bool IsMyDay { get; set; }
public string? Notes { get; set; }
- Step 4: Update
TaskEntityConfiguration— add column mappings insideConfigure(EntityTypeBuilder<TaskEntity> b):
b.Property(t => t.IsStarred).HasColumnName("is_starred").HasDefaultValue(false);
b.Property(t => t.IsMyDay).HasColumnName("is_my_day").HasDefaultValue(false);
b.Property(t => t.Notes).HasColumnName("notes");
(Match existing snake_case style — verify by reading the file first.)
- Step 5: Run test — verify pass
Run: dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter FullyQualifiedName~TaskEntityFlagsTests
Expected: PASS.
- Step 6: Commit
git add src/ClaudeDo.Data/Models/TaskEntity.cs src/ClaudeDo.Data/Configuration/TaskEntityConfiguration.cs tests/ClaudeDo.Worker.Tests/UiSchema/TaskEntityFlagsTests.cs
git commit -m "feat(data): add IsStarred, IsMyDay, Notes to TaskEntity"
Task 2: Generate EF Core migration for new columns
Files:
-
Create:
src/ClaudeDo.Data/Migrations/<timestamp>_AddTaskFlagsAndNotes.cs -
Modify:
src/ClaudeDo.Data/Migrations/ClaudeDoDbContextModelSnapshot.cs(generated) -
Step 1: Generate migration
Run from repo root:
dotnet ef migrations add AddTaskFlagsAndNotes --project src/ClaudeDo.Data/ClaudeDo.Data.csproj --startup-project src/ClaudeDo.Data/ClaudeDo.Data.csproj
If dotnet-ef is missing: dotnet tool install --global dotnet-ef --version 8.*.
-
Step 2: Inspect the generated
Up— confirm threeAddColumn<>calls foris_starred,is_my_day,notes. If column names mismatch, edit them by hand. -
Step 3: Apply migration in test (already covered by
EnsureCreatedin tests)
Run: dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter FullyQualifiedName~TaskEntityFlagsTests
Expected: still PASS.
- Step 4: Commit
git add src/ClaudeDo.Data/Migrations/
git commit -m "feat(data): migration for IsStarred/IsMyDay/Notes columns"
Task 3: Seed default Lists ("My Day", "Important", "Planned") on install
Files:
-
Discover seed call site — search
src/ClaudeDo.Installer/andsrc/ClaudeDo.App/forEnsureCreated,Migrate, or existing tag seeding ("agent" tag). -
Modify: the discovered seeder OR create
src/ClaudeDo.Data/Seeding/DefaultListsSeeder.csif no central seeder exists. -
Test:
tests/ClaudeDo.Worker.Tests/UiSchema/DefaultListSeedTests.cs(new) -
Step 1: Locate seed site
Run:
grep -rn "agent.*manual\|GetOrCreateAsync\|EnsureCreated\|Migrate(" src/ClaudeDo.Installer src/ClaudeDo.App
If a central seeder exists, add list-seeding there. Otherwise create DefaultListsSeeder.
- Step 2: Write failing test —
tests/ClaudeDo.Worker.Tests/UiSchema/DefaultListSeedTests.cs
using ClaudeDo.Data;
using ClaudeDo.Data.Seeding; // adjust namespace if seeded elsewhere
using Microsoft.EntityFrameworkCore;
using Xunit;
namespace ClaudeDo.Worker.Tests.UiSchema;
public class DefaultListSeedTests : IDisposable
{
private readonly string _dbPath = Path.Combine(Path.GetTempPath(), $"claudedo-seed-{Guid.NewGuid():N}.db");
private ClaudeDoDbContext NewContext()
{
var opts = new DbContextOptionsBuilder<ClaudeDoDbContext>()
.UseSqlite($"Data Source={_dbPath}").Options;
var ctx = new ClaudeDoDbContext(opts);
ctx.Database.EnsureCreated();
return ctx;
}
[Fact]
public async Task Seeds_MyDay_Important_Planned_Lists_Idempotently()
{
await using (var ctx = NewContext())
{
await DefaultListsSeeder.SeedAsync(ctx);
await DefaultListsSeeder.SeedAsync(ctx); // idempotent
}
await using var verify = NewContext();
var names = verify.Lists.Select(l => l.Name).OrderBy(n => n).ToList();
Assert.Equal(new[] { "Important", "My Day", "Planned" }, names);
}
public void Dispose() { if (File.Exists(_dbPath)) File.Delete(_dbPath); }
}
- Step 3: Run test — verify fail
Run: dotnet test ... --filter FullyQualifiedName~DefaultListSeedTests
Expected: build error.
- Step 4: Implement seeder — create
src/ClaudeDo.Data/Seeding/DefaultListsSeeder.cs:
using ClaudeDo.Data.Models;
using Microsoft.EntityFrameworkCore;
namespace ClaudeDo.Data.Seeding;
public static class DefaultListsSeeder
{
private static readonly string[] Defaults = { "My Day", "Important", "Planned" };
public static async Task SeedAsync(ClaudeDoDbContext ctx, CancellationToken ct = default)
{
var existing = await ctx.Lists.Select(l => l.Name).ToListAsync(ct);
var now = DateTime.UtcNow;
foreach (var name in Defaults.Where(n => !existing.Contains(n)))
{
ctx.Lists.Add(new ListEntity
{
Id = Guid.NewGuid().ToString("N"),
Name = name,
CreatedAt = now,
});
}
await ctx.SaveChangesAsync(ct);
}
}
-
Step 5: Wire seeder into install path — call
DefaultListsSeeder.SeedAsync(ctx)from the same code path that seeds the "agent"/"manual" tags. If none exists, call it once on app startup afterDatabase.Migrate()inClaudeDo.App. -
Step 6: Run test — verify pass
Run: dotnet test ... --filter FullyQualifiedName~DefaultListSeedTests
Expected: PASS.
- Step 7: Commit
git add src/ClaudeDo.Data/Seeding/ tests/ClaudeDo.Worker.Tests/UiSchema/DefaultListSeedTests.cs <wired-call-site>
git commit -m "feat(data): seed default Lists (My Day, Important, Planned)"
Phase 2 — Design tokens, fonts, and shell wiring
Task 4: Embed Inter Tight + JetBrains Mono fonts
Files:
-
Create:
src/ClaudeDo.Ui/Assets/Fonts/InterTight-Regular.ttf,InterTight-Medium.ttf,InterTight-SemiBold.ttf -
Create:
src/ClaudeDo.Ui/Assets/Fonts/JetBrainsMono-Regular.ttf -
Modify:
src/ClaudeDo.Ui/ClaudeDo.Ui.csproj -
Step 1: Download font files — fetch from Google Fonts (
https://fonts.google.com/specimen/Inter+Tight,https://fonts.google.com/specimen/JetBrains+Mono). Place TTFs insrc/ClaudeDo.Ui/Assets/Fonts/. SIL OFL license — includeOFL.txtnext to each family. -
Step 2: Mark as Avalonia resources — in
src/ClaudeDo.Ui/ClaudeDo.Ui.csprojadd (or extend the existing<ItemGroup>forAvaloniaResource):
<ItemGroup>
<AvaloniaResource Include="Assets/Fonts/*.ttf" />
<AvaloniaResource Include="Assets/Fonts/OFL.txt" />
</ItemGroup>
- Step 3: Build to confirm assets resolve
Run: dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
Expected: build succeeds.
- Step 4: Commit
git add src/ClaudeDo.Ui/Assets/Fonts/ src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
git commit -m "feat(ui): embed Inter Tight and JetBrains Mono fonts"
Task 5: Port Tokens.axaml into the Ui project
Files:
-
Create:
src/ClaudeDo.Ui/Design/Tokens.axaml -
Step 1: Copy and adapt — open
docs/UI Rewrite/design_handoff_claudedo/Tokens.axamland copy its<ResourceDictionary>content intosrc/ClaudeDo.Ui/Design/Tokens.axaml. -
Step 2: Replace placeholder font URIs — anywhere the file references
avares://YourApp/..., replace withavares://ClaudeDo.Ui/Assets/Fonts/#Inter Tightandavares://ClaudeDo.Ui/Assets/Fonts/#JetBrains Mono. -
Step 3: Verify required keys present — open the file and confirm these brushes/values exist (per README §"Design tokens"):
VoidBrush #0A0E0C,DeepBrush #0D1311,SurfaceBrush #161D1A,Surface2Brush #1C2422,Surface3Brush #222B28,LineBrush #2A3330TextBrush #E4EBE4,TextDimBrush #9AA8A0,TextMuteBrush #6B7973,TextFaintBrush #4A5550MossBrush #7C9166,SageBrush #8B9D7A,PeatBrush #D4A574,BloodBrush #C87060IslandRadius 14,ModalRadius 12,ChipRadius 10,RowRadius 8,ButtonRadius 6MotionFast 0:0:0.12,MotionBase 0:0:0.18,MotionSlow 0:0:0.30IslandShadow,ModalShadowBoxShadow valuesSansFamily,MonoFamilyFontFamilyvalues
If any are missing, add them inline using the values from the README §"Design tokens (reference)".
-
Step 4: Commit
git add src/ClaudeDo.Ui/Design/Tokens.axaml
git commit -m "feat(ui): add design Tokens resource dictionary"
Task 6: Port IslandStyles.axaml into the Ui project
Files:
-
Create:
src/ClaudeDo.Ui/Design/IslandStyles.axaml -
Step 1: Copy the full
<Styles>content fromdocs/UI Rewrite/design_handoff_claudedo/IslandStyles.axamlintosrc/ClaudeDo.Ui/Design/IslandStyles.axaml. -
Step 2: Confirm classed selectors present — file must define styles for at least:
Border.island,Border.island-headerBorder.list-item,Border.list-item.activeTextBox.searchBorder.task-row,Border.task-row.selectedEllipse.task-check,Ellipse.task-check.doneBorder.chipand status variantschip.running,chip.review,chip.error,chip.queued,chip.idleBorder.agent-stripand status variantsBorder.terminal,TextBlock.log-sys,log-tool,log-claude,log-stdout,log-stderr,log-done,log-msgButton.icon-btn
If any classed selector is missing, add a minimal one referencing the relevant token brush.
-
Step 3: Commit
git add src/ClaudeDo.Ui/Design/IslandStyles.axaml
git commit -m "feat(ui): add island control styles"
Task 7: Wire tokens and styles into App.axaml
Files:
-
Modify:
src/ClaudeDo.Ui/App.axaml -
Step 1: Read current App.axaml to preserve any DI/region wiring:
Run: open src/ClaudeDo.Ui/App.axaml and note its existing structure.
- Step 2: Edit — ensure the
<Application>root contains:
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceInclude Source="avares://ClaudeDo.Ui/Design/Tokens.axaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
<Application.Styles>
<FluentTheme />
<StyleInclude Source="avares://ClaudeDo.Ui/Design/IslandStyles.axaml" />
</Application.Styles>
(Preserve any existing RequestedThemeVariant, converter resources, etc. — only add the includes if not already present.)
- Step 3: Build
Run: dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
Expected: success.
- Step 4: Commit
git add src/ClaudeDo.Ui/App.axaml
git commit -m "feat(ui): merge Tokens and IslandStyles into App"
Phase 3 — Chromeless shell + three-island layout
Task 8: Replace MainWindowViewModel with IslandsShellViewModel
Files:
-
Create:
src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs -
Step 1: Write the VM with three child VMs and a width-driven boolean for collapsing the Details column:
using CommunityToolkit.Mvvm.ComponentModel;
using ClaudeDo.Ui.ViewModels.Islands;
namespace ClaudeDo.Ui.ViewModels;
public sealed partial class IslandsShellViewModel : ViewModelBase
{
public ListsIslandViewModel Lists { get; }
public TasksIslandViewModel Tasks { get; }
public DetailsIslandViewModel Details { get; }
[ObservableProperty]
private double _windowWidth = 1280;
public bool ShowDetails => WindowWidth >= 1100;
public bool ShowLists => WindowWidth >= 780;
partial void OnWindowWidthChanged(double value)
{
OnPropertyChanged(nameof(ShowDetails));
OnPropertyChanged(nameof(ShowLists));
}
public IslandsShellViewModel(
ListsIslandViewModel lists,
TasksIslandViewModel tasks,
DetailsIslandViewModel details)
{
Lists = lists; Tasks = tasks; Details = details;
Lists.SelectionChanged += (_, _) => Tasks.LoadForList(Lists.SelectedList);
Tasks.SelectionChanged += (_, _) => Details.Bind(Tasks.SelectedTask);
}
}
- Step 2: Register in DI — modify
src/ClaudeDo.App/Program.cs(or whereverMainWindowViewModelis registered) to register the new VMs:
services.AddSingleton<IslandsShellViewModel>();
services.AddSingleton<ListsIslandViewModel>();
services.AddSingleton<TasksIslandViewModel>();
services.AddSingleton<DetailsIslandViewModel>();
(Adjust to scoped/transient if existing patterns demand.)
-
Step 3: Build —
dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj— fails until child VM stubs exist; that's fine, next task creates them. -
Step 4: Defer commit until child VMs compile.
Task 9: Stub child island VMs (compile-only skeletons)
Files (all new):
-
src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs -
src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs -
src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs -
Step 1: Write minimal stubs so
IslandsShellViewModelcompiles:
// ListsIslandViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
namespace ClaudeDo.Ui.ViewModels.Islands;
public sealed partial class ListsIslandViewModel : ViewModelBase
{
public event EventHandler? SelectionChanged;
[ObservableProperty] private ListNavItemViewModel? _selectedList;
partial void OnSelectedListChanged(ListNavItemViewModel? value) =>
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
// TasksIslandViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
namespace ClaudeDo.Ui.ViewModels.Islands;
public sealed partial class TasksIslandViewModel : ViewModelBase
{
public event EventHandler? SelectionChanged;
[ObservableProperty] private TaskRowViewModel? _selectedTask;
public void LoadForList(ListNavItemViewModel? list) { /* Phase 5 */ }
partial void OnSelectedTaskChanged(TaskRowViewModel? value) =>
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
// DetailsIslandViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
namespace ClaudeDo.Ui.ViewModels.Islands;
public sealed partial class DetailsIslandViewModel : ViewModelBase
{
[ObservableProperty] private TaskRowViewModel? _task;
public void Bind(TaskRowViewModel? task) => Task = task;
}
- Step 2: Add minimal
ListNavItemViewModelandTaskRowViewModelso references resolve:
// ListNavItemViewModel.cs
using CommunityToolkit.Mvvm.ComponentModel;
namespace ClaudeDo.Ui.ViewModels.Islands;
public sealed partial class ListNavItemViewModel : ViewModelBase
{
public required string Id { get; init; }
public required string Name { get; init; }
[ObservableProperty] private int _count;
[ObservableProperty] private bool _isActive;
public string? IconKey { get; init; }
}
// TaskRowViewModel.cs (placeholder — fleshed out in Phase 5)
using CommunityToolkit.Mvvm.ComponentModel;
namespace ClaudeDo.Ui.ViewModels.Islands;
public sealed partial class TaskRowViewModel : ViewModelBase
{
public required string Id { get; init; }
[ObservableProperty] private string _title = "";
[ObservableProperty] private bool _done;
}
-
Step 3: Build —
dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csprojshould succeed. -
Step 4: Commit
git add src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs src/ClaudeDo.Ui/ViewModels/Islands/ src/ClaudeDo.App
git commit -m "feat(ui): scaffold islands shell and child VMs"
Task 10: Replace MainWindow.axaml with chromeless three-column shell
Files:
-
Modify:
src/ClaudeDo.Ui/Views/MainWindow.axaml -
Modify:
src/ClaudeDo.Ui/Views/MainWindow.axaml.cs -
Step 1: Rewrite
MainWindow.axaml:
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels"
xmlns:islands="using:ClaudeDo.Ui.Views.Islands"
x:Class="ClaudeDo.Ui.Views.MainWindow"
x:DataType="vm:IslandsShellViewModel"
Title="ClaudeDo"
Width="1280" Height="820" MinWidth="780" MinHeight="600"
Background="{DynamicResource VoidBrush}"
SystemDecorations="None"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaTitleBarHeightHint="-1">
<Grid RowDefinitions="36,*">
<!-- Custom title bar -->
<Border Grid.Row="0" Background="{DynamicResource DeepBrush}" PointerPressed="OnTitleBarPressed">
<Grid ColumnDefinitions="*,Auto">
<TextBlock Grid.Column="0" Margin="14,0" VerticalAlignment="Center"
FontFamily="{DynamicResource SansFamily}" FontSize="12"
Foreground="{DynamicResource TextDimBrush}" Text="ClaudeDo"/>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="0">
<Button Classes="title-btn" Click="OnMinimize" Content="—"/>
<Button Classes="title-btn" Click="OnToggleMax" Content="▢"/>
<Button Classes="title-btn close" Click="OnClose" Content="✕"/>
</StackPanel>
</Grid>
</Border>
<!-- Three islands -->
<Grid Grid.Row="1" Margin="7" ColumnDefinitions="260,*,320">
<Border Grid.Column="0" Classes="island" Margin="7">
<islands:ListsIslandView DataContext="{Binding Lists}"/>
</Border>
<Border Grid.Column="1" Classes="island" Margin="7">
<islands:TasksIslandView DataContext="{Binding Tasks}"/>
</Border>
<Border Grid.Column="2" Classes="island" Margin="7"
IsVisible="{Binding ShowDetails}">
<islands:DetailsIslandView DataContext="{Binding Details}"/>
</Border>
</Grid>
</Grid>
</Window>
- Step 2: Code-behind handlers —
MainWindow.axaml.cs:
using Avalonia.Controls;
using Avalonia.Input;
using ClaudeDo.Ui.ViewModels;
namespace ClaudeDo.Ui.Views;
public partial class MainWindow : Window
{
public MainWindow() { InitializeComponent(); }
private void OnTitleBarPressed(object? sender, PointerPressedEventArgs e)
{
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
BeginMoveDrag(e);
}
private void OnMinimize(object? s, Avalonia.Interactivity.RoutedEventArgs e) =>
WindowState = WindowState.Minimized;
private void OnToggleMax(object? s, Avalonia.Interactivity.RoutedEventArgs e) =>
WindowState = WindowState == WindowState.Maximized ? WindowState.Normal : WindowState.Maximized;
private void OnClose(object? s, Avalonia.Interactivity.RoutedEventArgs e) => Close();
protected override void OnSizeChanged(SizeChangedEventArgs e)
{
base.OnSizeChanged(e);
if (DataContext is IslandsShellViewModel vm) vm.WindowWidth = Bounds.Width;
}
}
- Step 3: Stub island views so the AXAML compiles — create three minimal UserControls:
<!-- src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml -->
<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.ListsIslandView"
x:DataType="vm:ListsIslandViewModel">
<TextBlock Margin="14" Text="Lists (placeholder)"
Foreground="{DynamicResource TextDimBrush}"/>
</UserControl>
(Identical pattern for TasksIslandView.axaml and DetailsIslandView.axaml. Each needs an empty .axaml.cs with InitializeComponent().)
- Step 4: Run the app
Run: dotnet run --project src/ClaudeDo.App/ClaudeDo.App.csproj
Expected: chromeless dark window, three islands with correct widths and 14px gaps; resize below 1100px collapses Details.
- Step 5: Commit
git add src/ClaudeDo.Ui/Views/MainWindow.axaml src/ClaudeDo.Ui/Views/MainWindow.axaml.cs src/ClaudeDo.Ui/Views/Islands/
git commit -m "feat(ui): chromeless three-island shell"
Phase 4 — Lists Island
Task 11: Build ListsIslandViewModel (real)
Files:
- Modify:
src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs
The Lists island shows: a search box, then nav items in this fixed order — My Day, Important, Planned, Running (virtual filter — all tasks with status Running), Review (virtual: status Done with worktree state Active), then one entry per real list (excluding the three seeded "smart" lists already shown above). Counts live-update from the Tasks repository.
- Step 1: Define
ListKindenum and replace stub:
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using ClaudeDo.Data.Repositories;
namespace ClaudeDo.Ui.ViewModels.Islands;
public enum ListKind { Smart, Virtual, User }
public sealed partial class ListsIslandViewModel : ViewModelBase
{
private readonly TaskRepository _tasks;
private readonly ListRepository _lists;
public event EventHandler? SelectionChanged;
public ObservableCollection<ListNavItemViewModel> Items { get; } = new();
[ObservableProperty] private string _searchText = "";
[ObservableProperty] private ListNavItemViewModel? _selectedList;
public ListsIslandViewModel(TaskRepository tasks, ListRepository lists)
{
_tasks = tasks; _lists = lists;
}
public async Task LoadAsync(CancellationToken ct = default)
{
Items.Clear();
Items.Add(new ListNavItemViewModel { Id = "smart:my-day", Name = "My Day", Kind = ListKind.Smart, IconKey = "Sun" });
Items.Add(new ListNavItemViewModel { Id = "smart:important", Name = "Important", Kind = ListKind.Smart, IconKey = "Star" });
Items.Add(new ListNavItemViewModel { Id = "smart:planned", Name = "Planned", Kind = ListKind.Smart, IconKey = "Calendar" });
Items.Add(new ListNavItemViewModel { Id = "virtual:running", Name = "Running", Kind = ListKind.Virtual, IconKey = "Pulse" });
Items.Add(new ListNavItemViewModel { Id = "virtual:review", Name = "Review", Kind = ListKind.Virtual, IconKey = "Eye" });
var seedNames = new HashSet<string>(new[] { "My Day", "Important", "Planned" });
foreach (var l in await _lists.GetAllAsync(ct))
if (!seedNames.Contains(l.Name))
Items.Add(new ListNavItemViewModel { Id = $"user:{l.Id}", Name = l.Name, Kind = ListKind.User, IconKey = "Folder" });
await RefreshCountsAsync(ct);
SelectedList = Items.FirstOrDefault();
}
public async Task RefreshCountsAsync(CancellationToken ct = default)
{
// Implementation note: extend TaskRepository with count-by-kind helpers if missing.
// Leave counts at 0 for now; populate as Phase 5 wires task loads.
foreach (var i in Items) i.Count = 0;
await Task.CompletedTask;
}
partial void OnSelectedListChanged(ListNavItemViewModel? value)
{
foreach (var i in Items) i.IsActive = ReferenceEquals(i, value);
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
}
- Step 2: Extend
ListNavItemViewModel— addKindproperty:
public required ListKind Kind { get; init; }
- Step 3: Trigger
LoadAsync— inIslandsShellViewModelconstructor, after wiring events:
_ = Lists.LoadAsync();
- Step 4: Build
Run: dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
Expected: success (assuming ListRepository.GetAllAsync exists — if not, use the existing query method or extend the repository).
- Step 5: Commit
git add src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs src/ClaudeDo.Ui/ViewModels/Islands/ListNavItemViewModel.cs src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs
git commit -m "feat(ui): ListsIslandViewModel with smart/virtual/user lists"
Task 12: Build ListsIslandView
Files:
-
Modify:
src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml -
Step 1: Replace placeholder with header + search + nav
ItemsControl:
<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.ListsIslandView"
x:DataType="vm:ListsIslandViewModel">
<DockPanel LastChildFill="True">
<Border DockPanel.Dock="Top" Classes="island-header">
<StackPanel Spacing="6" Margin="14,12">
<TextBlock Classes="eyebrow" Text="WORKSPACE"/>
<TextBlock FontFamily="{DynamicResource SansFamily}" FontSize="18"
FontWeight="SemiBold" Foreground="{DynamicResource TextBrush}"
Text="Lists"/>
<TextBox Classes="search" Margin="0,8,0,0" Watermark="Search…"
Text="{Binding SearchText, Mode=TwoWay}"/>
</StackPanel>
</Border>
<ScrollViewer>
<ItemsControl ItemsSource="{Binding Items}" Margin="6">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:ListNavItemViewModel">
<Border Classes="list-item" Classes.active="{Binding IsActive}">
<Grid ColumnDefinitions="20,*,Auto" Margin="10,8">
<PathIcon Grid.Column="0" Width="14" Height="14"/>
<TextBlock Grid.Column="1" Text="{Binding Name}"
VerticalAlignment="Center" Margin="8,0"
Foreground="{DynamicResource TextBrush}" FontSize="13"/>
<TextBlock Grid.Column="2" Text="{Binding Count}"
FontFamily="{DynamicResource MonoFamily}" FontSize="10"
Foreground="{DynamicResource TextFaintBrush}"/>
</Grid>
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
</UserControl>
-
Step 2: Selection behavior — wrap each row in a
Buttonor attach aTappedhandler that sets((ListsIslandViewModel)DataContext).SelectedList = item. Quick path: replaceBorderwith aButton Classes="list-item-btn"styled flat,Command="{Binding $parent[ItemsControl].DataContext.SelectCommand}"with[RelayCommand] private void Select(ListNavItemViewModel item) => SelectedList = item;added to the VM. -
Step 3: Run the app — verify list items render, search box shows, click selection toggles
activeclass. -
Step 4: Commit
git add src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs
git commit -m "feat(ui): Lists island view with search and nav items"
Phase 5 — Tasks Island
Task 13: TaskRowViewModel — full
Files:
-
Modify:
src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs -
Test:
tests/ClaudeDo.Worker.Tests/UiVm/TaskRowViewModelTests.cs(new) -
Step 1: Write the VM based on README §"Task model (MVVM)":
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using ClaudeDo.Data.Models;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Ui.ViewModels.Islands;
public sealed partial class TaskRowViewModel : ViewModelBase
{
public required string Id { get; init; }
[ObservableProperty] private string _title = "";
[ObservableProperty] private string _listName = "";
[ObservableProperty] private bool _done;
[ObservableProperty] private bool _isStarred;
[ObservableProperty] private bool _isMyDay;
[ObservableProperty] private bool _isSelected;
[ObservableProperty] private TaskStatus _status;
[ObservableProperty] private string? _branch;
[ObservableProperty] private string? _diffStat;
[ObservableProperty] private string? _liveTail;
public string StatusChipClass => Status switch
{
TaskStatus.Running => "running",
TaskStatus.Failed => "error",
TaskStatus.Done => "review",
TaskStatus.Queued => "queued",
_ => "idle",
};
public static TaskRowViewModel FromEntity(TaskEntity t) => new()
{
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,
};
}
- Step 2: Write VM test —
tests/ClaudeDo.Worker.Tests/UiVm/TaskRowViewModelTests.cs:
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.ViewModels.Islands;
using Xunit;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Worker.Tests.UiVm;
public class TaskRowViewModelTests
{
[Theory]
[InlineData(TaskStatus.Running, "running")]
[InlineData(TaskStatus.Failed, "error")]
[InlineData(TaskStatus.Done, "review")]
[InlineData(TaskStatus.Queued, "queued")]
[InlineData(TaskStatus.Manual, "idle")]
public void StatusChipClass_Maps_Correctly(TaskStatus s, string expected)
{
var vm = new TaskRowViewModel { Id = "t" };
vm.Status = s;
Assert.Equal(expected, vm.StatusChipClass);
}
}
- Step 3: Run tests
Run: dotnet test tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj --filter FullyQualifiedName~TaskRowViewModelTests
Expected: PASS.
- Step 4: Commit
git add src/ClaudeDo.Ui/ViewModels/Islands/TaskRowViewModel.cs tests/ClaudeDo.Worker.Tests/UiVm/
git commit -m "feat(ui): TaskRowViewModel with status chip mapping"
Task 14: TasksIslandViewModel — load + add + filter
Files:
-
Modify:
src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs -
Step 1: Implement — load on list selection; add task on Enter; expose header counts:
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
namespace ClaudeDo.Ui.ViewModels.Islands;
public sealed partial class TasksIslandViewModel : ViewModelBase
{
private readonly TaskRepository _tasks;
private ListNavItemViewModel? _currentList;
public event EventHandler? SelectionChanged;
public ObservableCollection<TaskRowViewModel> Items { get; } = new();
[ObservableProperty] private string _newTaskTitle = "";
[ObservableProperty] private TaskRowViewModel? _selectedTask;
[ObservableProperty] private string _headerTitle = "";
[ObservableProperty] private string _headerEyebrow = "";
[ObservableProperty] private string _subtitle = "";
public TasksIslandViewModel(TaskRepository tasks) { _tasks = tasks; }
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();
var all = await _tasks.GetAllAsync();
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.Done /* TODO refine */);
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 _tasks.CreateAsync(entity);
Items.Insert(0, TaskRowViewModel.FromEntity(entity));
NewTaskTitle = "";
UpdateSubtitle();
}
partial void OnSelectedTaskChanged(TaskRowViewModel? value)
{
foreach (var i in Items) i.IsSelected = ReferenceEquals(i, value);
SelectionChanged?.Invoke(this, EventArgs.Empty);
}
}
-
Step 2: Verify repository methods exist —
TaskRepository.GetAllAsyncandCreateAsync. If names differ, adjust. -
Step 3: Build
Run: dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
Expected: success.
- Step 4: Commit
git add src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs
git commit -m "feat(ui): TasksIslandViewModel with smart/virtual/user filtering"
Task 15: TasksIslandView + TaskRowView
Files:
-
Modify:
src/ClaudeDo.Ui/Views/Islands/TasksIslandView.axaml -
Create:
src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml(.cs) -
Step 1:
TaskRowView.axaml— extracted UserControl per README §"Task list":
<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.TaskRowView"
x:DataType="vm:TaskRowViewModel">
<Border Classes="task-row" Classes.selected="{Binding IsSelected}">
<Grid ColumnDefinitions="36,*,32" Margin="10,10">
<Button Grid.Column="0" Classes="check-btn" VerticalAlignment="Center"
Command="{Binding $parent[ItemsControl].DataContext.ToggleDoneCommand}"
CommandParameter="{Binding}">
<Ellipse Width="18" Height="18" Classes="task-check"
Classes.done="{Binding Done}"/>
</Button>
<StackPanel Grid.Column="1" Spacing="6" VerticalAlignment="Center">
<TextBlock Text="{Binding Title}" FontSize="14"
Foreground="{DynamicResource TextBrush}"
TextDecorations="{Binding Done, Converter={StaticResource StrikeIfTrue}}"/>
<StackPanel Orientation="Horizontal" Spacing="8">
<Border Classes="chip" Classes.running="{Binding Status, Converter={StaticResource EqStatusRunning}}">
<TextBlock Text="{Binding Status}" FontSize="10"
FontFamily="{DynamicResource MonoFamily}" Margin="6,2"/>
</Border>
<Border Classes="chip">
<TextBlock Text="{Binding ListName}" FontSize="10" Margin="6,2"/>
</Border>
<Border Classes="chip" IsVisible="{Binding Branch, Converter={StaticResource NotNullToBool}}">
<TextBlock Text="{Binding Branch}" FontFamily="{DynamicResource MonoFamily}" FontSize="10" Margin="6,2"/>
</Border>
<Border Classes="chip" IsVisible="{Binding DiffStat, Converter={StaticResource NotNullToBool}}">
<TextBlock Text="{Binding DiffStat}" FontFamily="{DynamicResource MonoFamily}" FontSize="10" Margin="6,2"/>
</Border>
</StackPanel>
<TextBlock Text="{Binding LiveTail}" FontFamily="{DynamicResource MonoFamily}"
FontSize="11" Foreground="{DynamicResource TextMuteBrush}"
TextTrimming="CharacterEllipsis" MaxLines="1"
IsVisible="{Binding LiveTail, Converter={StaticResource NotNullToBool}}"/>
</StackPanel>
<Button Grid.Column="2" Classes="icon-btn" VerticalAlignment="Center"
Command="{Binding $parent[ItemsControl].DataContext.ToggleStarCommand}"
CommandParameter="{Binding}">
<PathIcon Width="14" Height="14"/>
</Button>
</Grid>
</Border>
</UserControl>
-
Step 2: Add converters —
NotNullToBool,StrikeIfTrue,EqStatusRunning(and one per status). Place undersrc/ClaudeDo.Ui/Converters/. Register them as resources inApp.axaml. (Each converter is ~10 lines; copy the pattern fromStatusColorConverter.cs.) -
Step 3: Add
[RelayCommand]ToggleDone / ToggleStar toTasksIslandViewModel:
[RelayCommand] private async Task ToggleDoneAsync(TaskRowViewModel row)
{
row.Done = !row.Done;
var entity = await _tasks.GetByIdAsync(row.Id);
if (entity != null)
{
entity.Status = row.Done ? TaskStatus.Done : TaskStatus.Manual;
await _tasks.UpdateAsync(entity);
}
UpdateSubtitle();
}
[RelayCommand] private async Task ToggleStarAsync(TaskRowViewModel row)
{
row.IsStarred = !row.IsStarred;
var entity = await _tasks.GetByIdAsync(row.Id);
if (entity != null) { entity.IsStarred = row.IsStarred; await _tasks.UpdateAsync(entity); }
}
- Step 4:
TasksIslandView.axaml— header, add-task box, scrollable list:
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
xmlns:islands="using:ClaudeDo.Ui.Views.Islands"
x:Class="ClaudeDo.Ui.Views.Islands.TasksIslandView"
x:DataType="vm:TasksIslandViewModel">
<DockPanel LastChildFill="True">
<Border DockPanel.Dock="Top" Classes="island-header">
<StackPanel Margin="18,14" Spacing="6">
<TextBlock Classes="eyebrow" Text="{Binding HeaderEyebrow}"/>
<TextBlock FontFamily="{DynamicResource SansFamily}" FontSize="24"
FontWeight="SemiBold" Foreground="{DynamicResource TextBrush}"
Text="{Binding HeaderTitle}"/>
<TextBlock FontFamily="{DynamicResource MonoFamily}" FontSize="11"
Foreground="{DynamicResource TextMuteBrush}" Text="{Binding Subtitle}"/>
</StackPanel>
</Border>
<Border DockPanel.Dock="Top" Margin="18,8">
<TextBox Watermark="Add a task…" Text="{Binding NewTaskTitle, Mode=TwoWay}">
<TextBox.KeyBindings>
<KeyBinding Gesture="Enter" Command="{Binding AddCommand}"/>
</TextBox.KeyBindings>
</TextBox>
</Border>
<ScrollViewer>
<ItemsControl ItemsSource="{Binding Items}" Margin="10">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:TaskRowViewModel">
<islands:TaskRowView/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
</UserControl>
-
Step 5: Selection handler — add
TappedonTaskRowViewsetting((TasksIslandViewModel)DataContext.parent).SelectedTask = vm. Or use a[RelayCommand] private void Select(TaskRowViewModel row) => SelectedTask = row;and bind viaInputElement.Tappedbehavior. -
Step 6: Run app — verify rows render with chips, add-task on Enter prepends a row, selection updates Details.
-
Step 7: Commit
git add src/ClaudeDo.Ui/Views/Islands/ src/ClaudeDo.Ui/Converters/ src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs src/ClaudeDo.Ui/App.axaml
git commit -m "feat(ui): tasks island with rows, chips, add-task, selection"
Phase 6 — Details Island
Task 16: DetailsIslandViewModel — bind selected task + agent state
Files:
-
Modify:
src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs -
Create:
src/ClaudeDo.Ui/ViewModels/Islands/LogLineViewModel.cs -
Step 1:
LogLineViewModel:
namespace ClaudeDo.Ui.ViewModels.Islands;
public enum LogKind { Sys, Tool, Claude, Stdout, Stderr, Done, Msg }
public sealed class LogLineViewModel
{
public required LogKind Kind { get; init; }
public required string Text { get; init; }
public string ClassName => Kind switch
{
LogKind.Sys => "log-sys", LogKind.Tool => "log-tool", LogKind.Claude => "log-claude",
LogKind.Stdout => "log-stdout", LogKind.Stderr => "log-stderr",
LogKind.Done => "log-done", LogKind.Msg => "log-msg",
};
}
- Step 2:
DetailsIslandViewModel— bind everything the AgentStrip + Terminal + Subtasks + Notes need:
using System.Collections.ObjectModel;
using CommunityToolkit.Mvvm.ComponentModel;
using CommunityToolkit.Mvvm.Input;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Services;
namespace ClaudeDo.Ui.ViewModels.Islands;
public sealed partial class DetailsIslandViewModel : ViewModelBase
{
private readonly TaskRepository _tasks;
private readonly SubtaskRepository _subtasks;
private readonly WorkerClient _worker;
[ObservableProperty] private TaskRowViewModel? _task;
[ObservableProperty] private string _editableTitle = "";
[ObservableProperty] private string _notes = "";
[ObservableProperty] private string _promptInput = "";
[ObservableProperty] private string _agentStatusLabel = "Idle";
[ObservableProperty] private string? _model;
[ObservableProperty] private string? _worktreePath;
[ObservableProperty] private string? _branchLine;
[ObservableProperty] private int _turns;
[ObservableProperty] private int _tokens;
[ObservableProperty] private TimeSpan _elapsed;
public ObservableCollection<LogLineViewModel> Log { get; } = new();
public ObservableCollection<SubtaskRowViewModel> Subtasks { get; } = new();
public DetailsIslandViewModel(TaskRepository tasks, SubtaskRepository subtasks, WorkerClient worker)
{
_tasks = tasks; _subtasks = subtasks; _worker = worker;
}
public async void Bind(TaskRowViewModel? row)
{
Task = row;
Log.Clear(); Subtasks.Clear();
if (row == null) return;
var entity = await _tasks.GetByIdAsync(row.Id);
if (entity == null) return;
EditableTitle = entity.Title;
Notes = entity.Notes ?? "";
Model = entity.Model;
WorktreePath = entity.Worktree?.Path;
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} ← main" : null;
AgentStatusLabel = entity.Status.ToString();
foreach (var s in await _subtasks.GetForTaskAsync(row.Id))
Subtasks.Add(new SubtaskRowViewModel { Id = s.Id, Title = s.Title, Done = s.Completed });
}
[RelayCommand] private async Task SendPromptAsync()
{
if (string.IsNullOrWhiteSpace(PromptInput) || Task == null) return;
Log.Add(new LogLineViewModel { Kind = LogKind.Msg, Text = $"[you] {PromptInput}" });
await _worker.SendPromptAsync(Task.Id, PromptInput); // adjust to actual API
PromptInput = "";
}
[RelayCommand] private async Task ApproveMergeAsync() { /* call worker merge */ await Task.CompletedTask; }
[RelayCommand] private async Task StopAsync() { /* call worker stop */ await Task.CompletedTask; }
}
public sealed partial class SubtaskRowViewModel : ViewModelBase
{
public required string Id { get; init; }
[ObservableProperty] private string _title = "";
[ObservableProperty] private bool _done;
}
-
Step 3: Wire SignalR live log — subscribe to
WorkerClientlog events inBindand append toLog. Use the existing event pattern; verify the API surface inWorkerClient.csfirst. -
Step 4: Build + commit
Run: dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj
git add src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs src/ClaudeDo.Ui/ViewModels/Islands/LogLineViewModel.cs
git commit -m "feat(ui): DetailsIslandViewModel with agent state and log"
Task 17: Build DetailsIslandView + AgentStripView + SessionTerminalView
Files:
-
Modify:
src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml -
Create:
src/ClaudeDo.Ui/Views/Islands/AgentStripView.axaml(.cs) -
Create:
src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml(.cs) -
Step 1:
AgentStripView.axaml— three rows per README §"Agent strip":
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
x:Class="ClaudeDo.Ui.Views.Islands.AgentStripView"
x:DataType="vm:DetailsIslandViewModel">
<Border Classes="agent-strip">
<StackPanel Margin="14,12" Spacing="6">
<StackPanel Orientation="Horizontal" Spacing="10">
<Ellipse Width="8" Height="8" Fill="{DynamicResource MossBrush}"/>
<TextBlock Text="{Binding AgentStatusLabel}" FontSize="12"
Foreground="{DynamicResource TextBrush}"/>
<TextBlock Text="{Binding Model}" FontFamily="{DynamicResource MonoFamily}"
FontSize="11" Foreground="{DynamicResource TextDimBrush}"/>
<TextBlock Text="{Binding Turns, StringFormat='turns: {0}'}" FontSize="11"
Foreground="{DynamicResource TextMuteBrush}"/>
<TextBlock Text="{Binding Tokens, StringFormat='tok: {0}'}" FontSize="11"
Foreground="{DynamicResource TextMuteBrush}"/>
</StackPanel>
<TextBlock Text="{Binding WorktreePath}" FontFamily="{DynamicResource MonoFamily}"
FontSize="11" Foreground="{DynamicResource TextDimBrush}"
TextTrimming="CharacterEllipsis"/>
<TextBlock Text="{Binding BranchLine}" FontFamily="{DynamicResource MonoFamily}"
FontSize="11" Foreground="{DynamicResource TextDimBrush}"/>
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,6,0,0">
<Button Content="Open diff"/>
<Button Content="Worktree"/>
<Button Content="Stop" Command="{Binding StopCommand}"/>
<Button Content="Approve & merge" Command="{Binding ApproveMergeCommand}"/>
</StackPanel>
</StackPanel>
</Border>
</UserControl>
- Step 2:
SessionTerminalView.axaml— log + prompt input:
<UserControl xmlns="https://github.com/avaloniaui"
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">
<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}">
<TextBox.KeyBindings>
<KeyBinding Gesture="Enter" Command="{Binding SendPromptCommand}"/>
</TextBox.KeyBindings>
</TextBox>
</Grid>
<ScrollViewer Name="LogScroll" VerticalScrollBarVisibility="Auto">
<ItemsControl ItemsSource="{Binding Log}">
<ItemsControl.ItemTemplate>
<DataTemplate 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>
In code-behind: subscribe to Log.CollectionChanged and call LogScroll.ScrollToEnd().
- Step 3:
DetailsIslandView.axaml— assemble sections per README §"Details island":
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
xmlns:islands="using:ClaudeDo.Ui.Views.Islands"
x:Class="ClaudeDo.Ui.Views.Islands.DetailsIslandView"
x:DataType="vm:DetailsIslandViewModel">
<ScrollViewer>
<StackPanel Margin="18,14" Spacing="14">
<TextBox Text="{Binding EditableTitle, Mode=TwoWay}" FontSize="18"
BorderThickness="0" Background="Transparent"
Foreground="{DynamicResource TextBrush}"/>
<islands:AgentStripView/>
<islands:SessionTerminalView Height="260"/>
<ItemsControl ItemsSource="{Binding Subtasks}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:SubtaskRowViewModel">
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,2">
<CheckBox IsChecked="{Binding Done, Mode=TwoWay}"/>
<TextBlock Text="{Binding Title}" VerticalAlignment="Center"
Foreground="{DynamicResource TextBrush}"/>
</StackPanel>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<TextBox Text="{Binding Notes, Mode=TwoWay}" AcceptsReturn="True"
TextWrapping="Wrap" MinHeight="80" Watermark="Notes…"/>
</StackPanel>
</ScrollViewer>
</UserControl>
- Step 4: Run + visually verify
Run: dotnet run --project src/ClaudeDo.App/ClaudeDo.App.csproj
Expected: clicking a task populates Details with title, agent strip, terminal area, notes.
- Step 5: Commit
git add src/ClaudeDo.Ui/Views/Islands/DetailsIslandView.axaml src/ClaudeDo.Ui/Views/Islands/AgentStripView.axaml src/ClaudeDo.Ui/Views/Islands/AgentStripView.axaml.cs src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml src/ClaudeDo.Ui/Views/Islands/SessionTerminalView.axaml.cs
git commit -m "feat(ui): details island with agent strip, terminal, subtasks, notes"
Phase 7 — Modals
Task 18: Diff modal
Files:
-
Create:
src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml(.cs) -
Create:
src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs -
Step 1: ViewModel — exposes
Files: ObservableCollection<DiffFileVm>,SelectedFile, each file hasHunks: List<DiffLineVm>withKind: Add|Del|Ctx,OldNo,NewNo,Text. Populate fromgit diffoutput (extendGitServicewith a parser if missing — initial scope: stub data). -
Step 2: View — borderless
Window(SystemDecorations="None",WindowStartupLocation="CenterOwner",Background="Transparent"). InnerBorder Classes="modal"withGrid ColumnDefinitions="240,*": leftListBoxof files (each row showing name ++N −Nchips), rightItemsControlof hunk lines styled by kind (delred-tinted,addgreen-tinted,ctxneutral) — per README §"Diff modal". -
Step 3: Wire button —
AgentStripView"Open diff"Button.Clickopens the window with the current task's diff. -
Step 4: Esc closes —
KeyBindingsin modal:<KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/>. -
Step 5: Commit
git add src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml.cs src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs
git commit -m "feat(ui): diff modal with file sidebar and tinted hunks"
Task 19: Worktree modal
Files:
-
Create:
src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml(.cs) -
Create:
src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs -
Step 1: ViewModel —
TreeNodes: ObservableCollection<WorktreeNodeVm>(recursive, withName,Status: 'M'|'A'|null,Children). -
Step 2: View — modal
Window(same chrome pattern as diff modal). Body:TreeViewbound toTreeNodes, each nodeStackPanel Horizontalwith name + status badge (Mpeat-tinted,Amoss-tinted). -
Step 3: Populate — parse
git status --porcelainfrom worktree path; build tree by splitting paths on/. -
Step 4: Wire button + Esc as in Task 18.
-
Step 5: Commit
git add src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml.cs src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs
git commit -m "feat(ui): worktree modal with tree view and M/A badges"
Phase 8 — Animations and keyboard shortcuts
Task 20: Animations
Files:
-
Modify:
src/ClaudeDo.Ui/Design/IslandStyles.axaml -
Step 1: Task-row hover — add
TransitionsonBorder.task-row:
<Style Selector="Border.task-row">
<Setter Property="Transitions">
<Transitions>
<BrushTransition Property="Background" Duration="0:0:0.10"/>
</Transitions>
</Setter>
</Style>
- Step 2: Running pulse — add to
Ellipse.status-pulse:
<Style Selector="Ellipse.status-pulse">
<Style.Animations>
<Animation Duration="0:0:1.2" IterationCount="Infinite" Easing="CubicEaseInOut">
<KeyFrame Cue="0%"><Setter Property="Opacity" Value="0.4"/></KeyFrame>
<KeyFrame Cue="50%"><Setter Property="Opacity" Value="1.0"/></KeyFrame>
<KeyFrame Cue="100%"><Setter Property="Opacity" Value="0.4"/></KeyFrame>
</Animation>
</Style.Animations>
</Style>
-
Step 3: Task-row add — animate inserted row opacity+Y. In
TaskRowView.axaml.cs, onAttachedToVisualTreestart a 0.3sAnimationon opacity (0→1) andTranslateTransform.Y(8→0). -
Step 4: Modal scale-in — modal root
Border Classes="modal"getsRenderTransformwithScaleTransform; on open animate 0.18s fromScaleX/Y=0.98, Opacity=0to1.0, 1.0. -
Step 5: Visual verify — run app, observe pulse, hover, modal animation.
-
Step 6: Commit
git add src/ClaudeDo.Ui/Design/IslandStyles.axaml src/ClaudeDo.Ui/Views/Islands/TaskRowView.axaml.cs src/ClaudeDo.Ui/Views/Modals/
git commit -m "feat(ui): pulse, hover, modal, and row-add animations"
Task 21: Keyboard shortcuts
Files:
-
Modify:
src/ClaudeDo.Ui/Views/MainWindow.axaml(.cs) -
Step 1: Window-level KeyBindings:
<Window.KeyBindings>
<KeyBinding Gesture="OemQuestion" Command="{Binding FocusSearchCommand}"/>
<KeyBinding Gesture="Ctrl+N" Command="{Binding FocusAddTaskCommand}"/>
<KeyBinding Gesture="Space" Command="{Binding ToggleSelectedDoneCommand}"/>
</Window.KeyBindings>
(/ is the OemQuestion gesture without shift on US layouts; on DE layout you may need KeyDown handler instead — verify on the target machine.)
- Step 2: Implement commands on
IslandsShellViewModel:
[RelayCommand] private void FocusSearch() { /* raise event consumed by ListsIslandView code-behind to call Focus() on the search box */ }
[RelayCommand] private void FocusAddTask() { /* same pattern for tasks add box */ }
[RelayCommand] private async Task ToggleSelectedDoneAsync()
{
if (Tasks.SelectedTask is { } row)
await Tasks.ToggleDoneCommand.ExecuteAsync(row);
}
-
Step 3: Esc closes modals — already in modal
KeyBindingsfrom Tasks 18–19. -
Step 4: Visual verify
-
Step 5: Commit
git add src/ClaudeDo.Ui/Views/MainWindow.axaml src/ClaudeDo.Ui/Views/MainWindow.axaml.cs src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs
git commit -m "feat(ui): keyboard shortcuts (/ Ctrl+N Space Esc)"
Phase 9 — Cleanup
Task 22: Remove obsolete views and viewmodels
Files (delete):
-
src/ClaudeDo.Ui/Views/StatusBarView.axaml(.cs) -
src/ClaudeDo.Ui/Views/TaskListView.axaml(.cs) -
src/ClaudeDo.Ui/Views/TaskDetailView.axaml(.cs) -
src/ClaudeDo.Ui/Views/TaskEditorView.axaml(.cs) -
src/ClaudeDo.Ui/Views/ListEditorView.axaml(.cs) -
src/ClaudeDo.Ui/ViewModels/StatusBarViewModel.cs -
src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs -
src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs -
src/ClaudeDo.Ui/ViewModels/TaskEditorViewModel.cs -
src/ClaudeDo.Ui/ViewModels/ListEditorViewModel.cs -
src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs(only if unused) -
src/ClaudeDo.Ui/ViewModels/ListItemViewModel.cs -
src/ClaudeDo.Ui/ViewModels/SubtaskItemViewModel.cs -
src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs -
Step 1: Verify nothing references them
Run: grep -rn "MainWindowViewModel\|TaskListViewModel\|TaskDetailViewModel\|StatusBarViewModel\|ListEditorViewModel\|TaskEditorViewModel\|TaskItemViewModel\|ListItemViewModel\|SubtaskItemViewModel" src/
Expected: no hits outside the files being deleted (and DI registration sites you've already migrated in Task 8).
-
Step 2: Delete with
git rmon every listed file. -
Step 3: Build + run smoke test
Run: dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj && dotnet run --project src/ClaudeDo.App/ClaudeDo.App.csproj
Expected: build succeeds, app launches with new shell, all interactions still work.
- Step 4: Commit
git commit -m "chore(ui): remove obsolete pre-rewrite views and viewmodels"
Acceptance — final pass
Once Task 22 is committed, walk the README's Acceptance checklist (lines 236–249) interactively:
- Three-island layout, correct spacing, collapse <1100px
- Lists sidebar with icons, counts, search, active state
- Task rows with checkbox, title, meta chips, star
- Selection updates Details
- Agent strip shows status, model, turns, tokens, elapsed, worktree, branch
- Session terminal renders all log kinds with distinct colors, auto-scrolls, accepts prompt input
- Diff modal with file sidebar and tinted lines
- Worktree modal with M/A badges
- Status chip tints match
- Fonts: Inter Tight + JetBrains Mono applied
- Motion: row add/toggle, pulse, modal open, hover transitions
- Keyboard shortcuts wired
If any item is missing or visually off, file a follow-up task — do not silently skip.