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

1637 lines
64 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 `Border`s — 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 test**`tests/ClaudeDo.Worker.Tests/UiSchema/TaskEntityFlagsTests.cs`
```csharp
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`:
```csharp
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)`:
```csharp
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**
```bash
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:
```bash
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**
```bash
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:
```bash
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`
```csharp
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`:
```csharp
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**
```bash
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`):
```xml
<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**
```bash
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**
```bash
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**
```bash
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:
```xml
<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**
```bash
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:
```csharp
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:
```csharp
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 `IslandsShellViewModel` compiles:
```csharp
// 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:
```csharp
// 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.csproj` should succeed.
- [ ] **Step 4: Commit**
```bash
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`**:
```xml
<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`:
```csharp
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:
```xml
<!-- 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**
```bash
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**:
```csharp
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:
```csharp
public required ListKind Kind { get; init; }
```
- [ ] **Step 3: Trigger `LoadAsync`** — in `IslandsShellViewModel` constructor, after wiring events:
```csharp
_ = 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**
```bash
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`:
```xml
<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**
```bash
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)":
```csharp
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`:
```csharp
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**
```bash
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:
```csharp
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.GetAllAsync` and `CreateAsync`. If names differ, adjust.
- [ ] **Step 3: Build**
Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj`
Expected: success.
- [ ] **Step 4: Commit**
```bash
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":
```xml
<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 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`:
```csharp
[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:
```xml
<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**
```bash
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`**:
```csharp
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:
```csharp
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`
```bash
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":
```xml
<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:
```xml
<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":
```xml
<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**
```bash
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 button**`AgentStripView` "Open diff" `Button.Click` opens the window with the current task's diff.
- [ ] **Step 4: Esc closes**`KeyBindings` in modal: `<KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/>`.
- [ ] **Step 5: Commit**
```bash
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, 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**
```bash
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`:
```xml
<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`:
```xml
<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**
```bash
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**:
```xml
<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`**:
```csharp
[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**
```bash
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**
```bash
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.