Files
ClaudeDo/docs/superpowers/plans/2026-04-20-ui-rewrite-islands.md

64 KiB
Raw Blame History

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 handoff
  • docs/UI Rewrite/design_handoff_claudedo/Tokens.axaml — design tokens
  • docs/UI Rewrite/design_handoff_claudedo/IslandStyles.axaml — control styles
  • docs/UI Rewrite/design_handoff_claudedo/styles.css — measurement source of truth
  • docs/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, Notes to TaskEntity. Seed My Day, Important, Planned Lists on install.
  • Running and Review lists are virtual filters (status-based), not seeded list rows.
  • Window: chromeless (SystemDecorations="None" + ExtendClientAreaToDecorationsHint="True").
  • Fonts: embed Inter Tight and JetBrains 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 (replace MainWindowViewModel)
  • 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 testtests/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 in src/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 inside Configure(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 three AddColumn<> calls for is_starred, is_my_day, notes. If column names mismatch, edit them by hand.

  • Step 3: Apply migration in test (already covered by EnsureCreated in 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/ and src/ClaudeDo.App/ for EnsureCreated, Migrate, or existing tag seeding ("agent" tag).

  • Modify: the discovered seeder OR create src/ClaudeDo.Data/Seeding/DefaultListsSeeder.cs if 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 testtests/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 after Database.Migrate() in ClaudeDo.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 in src/ClaudeDo.Ui/Assets/Fonts/. SIL OFL license — include OFL.txt next to each family.

  • Step 2: Mark as Avalonia resources — in src/ClaudeDo.Ui/ClaudeDo.Ui.csproj add (or extend the existing <ItemGroup> for AvaloniaResource):

<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.axaml and copy its <ResourceDictionary> content into src/ClaudeDo.Ui/Design/Tokens.axaml.

  • Step 2: Replace placeholder font URIs — anywhere the file references avares://YourApp/..., replace with avares://ClaudeDo.Ui/Assets/Fonts/#Inter Tight and avares://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 #2A3330
    • TextBrush #E4EBE4, TextDimBrush #9AA8A0, TextMuteBrush #6B7973, TextFaintBrush #4A5550
    • MossBrush #7C9166, SageBrush #8B9D7A, PeatBrush #D4A574, BloodBrush #C87060
    • IslandRadius 14, ModalRadius 12, ChipRadius 10, RowRadius 8, ButtonRadius 6
    • MotionFast 0:0:0.12, MotionBase 0:0:0.18, MotionSlow 0:0:0.30
    • IslandShadow, ModalShadow BoxShadow values
    • SansFamily, MonoFamily FontFamily values

    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 from docs/UI Rewrite/design_handoff_claudedo/IslandStyles.axaml into src/ClaudeDo.Ui/Design/IslandStyles.axaml.

  • Step 2: Confirm classed selectors present — file must define styles for at least:

    • Border.island, Border.island-header
    • Border.list-item, Border.list-item.active
    • TextBox.search
    • Border.task-row, Border.task-row.selected
    • Ellipse.task-check, Ellipse.task-check.done
    • Border.chip and status variants chip.running, chip.review, chip.error, chip.queued, chip.idle
    • Border.agent-strip and status variants
    • Border.terminal, TextBlock.log-sys, log-tool, log-claude, log-stdout, log-stderr, log-done, log-msg
    • Button.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 wherever MainWindowViewModel is 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: Builddotnet 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 IslandsShellViewModel compiles:

// 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 ListNavItemViewModel and TaskRowViewModel so 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: Builddotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj should 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 handlersMainWindow.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 ListKind enum 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 — add Kind property:
public required ListKind Kind { get; init; }
  • Step 3: Trigger LoadAsync — in IslandsShellViewModel constructor, 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 Button or attach a Tapped handler that sets ((ListsIslandViewModel)DataContext).SelectedList = item. Quick path: replace Border with a Button 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 active class.

  • 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 testtests/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 existTaskRepository.GetAllAsync and CreateAsync. 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 convertersNotNullToBool, StrikeIfTrue, EqStatusRunning (and one per status). Place under src/ClaudeDo.Ui/Converters/. Register them as resources in App.axaml. (Each converter is ~10 lines; copy the pattern from StatusColorConverter.cs.)

  • Step 3: Add [RelayCommand] ToggleDone / ToggleStar to TasksIslandViewModel:

[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 Tapped on TaskRowView setting ((TasksIslandViewModel)DataContext.parent).SelectedTask = vm. Or use a [RelayCommand] private void Select(TaskRowViewModel row) => SelectedTask = row; and bind via InputElement.Tapped behavior.

  • 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 WorkerClient log events in Bind and append to Log. Use the existing event pattern; verify the API surface in WorkerClient.cs first.

  • 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 &amp; 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 has Hunks: List<DiffLineVm> with Kind: Add|Del|Ctx, OldNo, NewNo, Text. Populate from git diff output (extend GitService with a parser if missing — initial scope: stub data).

  • Step 2: View — borderless Window (SystemDecorations="None", WindowStartupLocation="CenterOwner", Background="Transparent"). Inner Border Classes="modal" with Grid ColumnDefinitions="240,*": left ListBox of files (each row showing name + +N N chips), right ItemsControl of hunk lines styled by kind (del red-tinted, add green-tinted, ctx neutral) — per README §"Diff modal".

  • Step 3: Wire buttonAgentStripView "Open diff" Button.Click opens the window with the current task's diff.

  • Step 4: Esc closesKeyBindings in 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: ViewModelTreeNodes: ObservableCollection<WorktreeNodeVm> (recursive, with Name, Status: 'M'|'A'|null, Children).

  • Step 2: View — modal Window (same chrome pattern as diff modal). Body: TreeView bound to TreeNodes, each node StackPanel Horizontal with name + status badge (M peat-tinted, A moss-tinted).

  • Step 3: Populate — parse git status --porcelain from 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 Transitions on Border.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, on AttachedToVisualTree start a 0.3s Animation on opacity (0→1) and TranslateTransform.Y (8→0).

  • Step 4: Modal scale-in — modal root Border Classes="modal" gets RenderTransform with ScaleTransform; on open animate 0.18s from ScaleX/Y=0.98, Opacity=0 to 1.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 KeyBindings from Tasks 1819.

  • 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 rm on 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 236249) 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.