1637 lines
64 KiB
Markdown
1637 lines
64 KiB
Markdown
# 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 & 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 18–19.
|
||
|
||
- [ ] **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 236–249) interactively:
|
||
|
||
- [ ] Three-island layout, correct spacing, collapse <1100px
|
||
- [ ] Lists sidebar with icons, counts, search, active state
|
||
- [ ] Task rows with checkbox, title, meta chips, star
|
||
- [ ] Selection updates Details
|
||
- [ ] Agent strip shows status, model, turns, tokens, elapsed, worktree, branch
|
||
- [ ] Session terminal renders all log kinds with distinct colors, auto-scrolls, accepts prompt input
|
||
- [ ] Diff modal with file sidebar and tinted lines
|
||
- [ ] Worktree modal with M/A badges
|
||
- [ ] Status chip tints match
|
||
- [ ] Fonts: Inter Tight + JetBrains Mono applied
|
||
- [ ] Motion: row add/toggle, pulse, modal open, hover transitions
|
||
- [ ] Keyboard shortcuts wired
|
||
|
||
If any item is missing or visually off, file a follow-up task — do not silently skip.
|