# Repo Import List Helper 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:** Add a helper that scans parent folders for git repos and bulk-creates lists (with `WorkingDir` pre-filled) for the repos the user ticks.

**Architecture:** A pure `RepoScanner` finds git repos under a parent folder. A `RepoImportModalViewModel` loads existing lists' working dirs, merges scanned candidates into a checklist (marking already-added repos), and creates `ListEntity` rows for ticked-new repos via `ListRepository`. `RepoImportModalView` hosts the checklist and a folder picker. Two entry points open the modal: a Help-menu item (handled by `IslandsShellViewModel`) and a folder button in the Lists island (handled by `ListsIslandViewModel`). Each entry point reloads the Lists island after the modal closes.

**Tech Stack:** .NET 8, Avalonia 12, CommunityToolkit.Mvvm source generators, EF Core (SQLite), xUnit.

---

## File Structure

**Create:**
- `src/ClaudeDo.Ui/Services/RepoScanner.cs` — pure filesystem scan; `RepoCandidate` record + `RepoScanner.Scan`.
- `src/ClaudeDo.Ui/ViewModels/Modals/RepoImportItemViewModel.cs` — one checklist row.
- `src/ClaudeDo.Ui/ViewModels/Modals/RepoImportModalViewModel.cs` — modal VM (load, merge, create) + static `BuildCandidates`.
- `src/ClaudeDo.Ui/Views/Modals/RepoImportModalView.axaml` (+ `.axaml.cs`) — modal window + folder picker.
- `tests/ClaudeDo.Ui.Tests/RepoScannerTests.cs` — scanner unit tests.
- `tests/ClaudeDo.Ui.Tests/RepoImportCandidatesTests.cs` — merge/dedupe/already-added unit tests.

**Modify:**
- `src/ClaudeDo.App/Program.cs` — register `RepoImportModalViewModel` (transient) + a `Func<RepoImportModalViewModel>`; pass the Func into `IslandsShellViewModel`.
- `src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs` — `ShowRepoImportModal` Func + `OpenRepoImportCommand`.
- `src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml` — folder button beside `+ New list`.
- `src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml.cs` — wire `ShowRepoImportModal`.
- `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs` — `ShowRepoImportModal` Func + `OpenRepoImportCommand`; inject `Func<RepoImportModalViewModel>`.
- `src/ClaudeDo.Ui/Views/MainWindow.axaml` — Help-menu item `Add repos as lists…`.
- `src/ClaudeDo.Ui/Views/MainWindow.axaml.cs` — wire `ShowRepoImportModal`.
- `src/ClaudeDo.Ui/CLAUDE.md` — document the new modal + entry points.

---

## Task 1: RepoScanner

**Files:**
- Create: `src/ClaudeDo.Ui/Services/RepoScanner.cs`
- Test: `tests/ClaudeDo.Ui.Tests/RepoScannerTests.cs`

- [ ] **Step 1: Write the failing tests**

Create `tests/ClaudeDo.Ui.Tests/RepoScannerTests.cs`:

```csharp
using ClaudeDo.Ui.Services;

namespace ClaudeDo.Ui.Tests;

public sealed class RepoScannerTests : IDisposable
{
    private readonly string _root =
        Path.Combine(Path.GetTempPath(), "repo-scan-" + Guid.NewGuid().ToString("N"));

    public RepoScannerTests() => Directory.CreateDirectory(_root);

    public void Dispose()
    {
        try { Directory.Delete(_root, recursive: true); } catch { }
    }

    private string MakeDir(string name)
    {
        var p = Path.Combine(_root, name);
        Directory.CreateDirectory(p);
        return p;
    }

    [Fact]
    public void Scan_ReturnsSubfoldersWithGitDirectory()
    {
        var repo = MakeDir("repo-a");
        Directory.CreateDirectory(Path.Combine(repo, ".git"));

        var result = RepoScanner.Scan(_root);

        Assert.Single(result);
        Assert.Equal("repo-a", result[0].Name);
        Assert.Equal(repo, result[0].FullPath);
    }

    [Fact]
    public void Scan_TreatsDotGitFileAsRepo()
    {
        var repo = MakeDir("worktree-repo");
        File.WriteAllText(Path.Combine(repo, ".git"), "gitdir: ../somewhere");

        var result = RepoScanner.Scan(_root);

        Assert.Single(result);
        Assert.Equal("worktree-repo", result[0].Name);
    }

    [Fact]
    public void Scan_IgnoresPlainFolders()
    {
        MakeDir("not-a-repo");

        var result = RepoScanner.Scan(_root);

        Assert.Empty(result);
    }

    [Fact]
    public void Scan_IsNotRecursive()
    {
        var nested = MakeDir(Path.Combine("outer", "inner"));
        Directory.CreateDirectory(Path.Combine(nested, ".git"));
        // outer itself has no .git

        var result = RepoScanner.Scan(_root);

        Assert.Empty(result);
    }

    [Fact]
    public void Scan_ReturnsEmptyForMissingFolder()
    {
        var result = RepoScanner.Scan(Path.Combine(_root, "does-not-exist"));

        Assert.Empty(result);
    }
}
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `dotnet test tests/ClaudeDo.Ui.Tests --filter RepoScannerTests`
Expected: FAIL — `RepoScanner` / `RepoCandidate` do not exist (compile error).

- [ ] **Step 3: Implement RepoScanner**

Create `src/ClaudeDo.Ui/Services/RepoScanner.cs`:

```csharp
namespace ClaudeDo.Ui.Services;

public sealed record RepoCandidate(string Name, string FullPath);

public static class RepoScanner
{
    public static IReadOnlyList<RepoCandidate> Scan(string parentFolder)
    {
        if (string.IsNullOrWhiteSpace(parentFolder) || !Directory.Exists(parentFolder))
            return Array.Empty<RepoCandidate>();

        var result = new List<RepoCandidate>();
        IEnumerable<string> subdirs;
        try { subdirs = Directory.EnumerateDirectories(parentFolder); }
        catch { return Array.Empty<RepoCandidate>(); }

        foreach (var dir in subdirs)
        {
            var gitPath = Path.Combine(dir, ".git");
            if (Directory.Exists(gitPath) || File.Exists(gitPath))
                result.Add(new RepoCandidate(Path.GetFileName(dir), dir));
        }
        return result;
    }
}
```

- [ ] **Step 4: Run tests to verify they pass**

Run: `dotnet test tests/ClaudeDo.Ui.Tests --filter RepoScannerTests`
Expected: PASS (5 tests).

- [ ] **Step 5: Commit**

```bash
git add src/ClaudeDo.Ui/Services/RepoScanner.cs tests/ClaudeDo.Ui.Tests/RepoScannerTests.cs
git commit -m "feat(ui): add RepoScanner for git repo discovery"
```

---

## Task 2: RepoImportItemViewModel

**Files:**
- Create: `src/ClaudeDo.Ui/ViewModels/Modals/RepoImportItemViewModel.cs`

No dedicated test (trivial display VM; covered indirectly by Task 3).

- [ ] **Step 1: Implement the item VM**

Create `src/ClaudeDo.Ui/ViewModels/Modals/RepoImportItemViewModel.cs`:

```csharp
using CommunityToolkit.Mvvm.ComponentModel;

namespace ClaudeDo.Ui.ViewModels.Modals;

public sealed partial class RepoImportItemViewModel : ViewModelBase
{
    public string Name { get; init; } = "";
    public string FullPath { get; init; } = "";

    // True when a list already points at this path. Such rows are shown ticked + disabled.
    public bool AlreadyAdded { get; init; }
    public bool CanToggle => !AlreadyAdded;

    [ObservableProperty] private bool _isChecked;
}
```

- [ ] **Step 2: Build to verify it compiles**

Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj`
Expected: Build succeeded.

- [ ] **Step 3: Commit**

```bash
git add src/ClaudeDo.Ui/ViewModels/Modals/RepoImportItemViewModel.cs
git commit -m "feat(ui): add RepoImportItemViewModel"
```

---

## Task 3: RepoImportModalViewModel

**Files:**
- Create: `src/ClaudeDo.Ui/ViewModels/Modals/RepoImportModalViewModel.cs`
- Test: `tests/ClaudeDo.Ui.Tests/RepoImportCandidatesTests.cs`

The pure `BuildCandidates` static method is the tested seam (dedupe + already-added marking). `LoadAsync`/`CreateAsync` touch the DB and are verified manually.

- [ ] **Step 1: Write the failing tests**

Create `tests/ClaudeDo.Ui.Tests/RepoImportCandidatesTests.cs`:

```csharp
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Modals;

namespace ClaudeDo.Ui.Tests;

public sealed class RepoImportCandidatesTests
{
    [Fact]
    public void BuildCandidates_NewRepo_IsCheckedAndNotAlreadyAdded()
    {
        var found = new[] { new RepoCandidate("repo-a", @"C:\src\repo-a") };
        var current = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        var existing = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        var items = RepoImportModalViewModel.BuildCandidates(found, current, existing);

        Assert.Single(items);
        Assert.True(items[0].IsChecked);
        Assert.False(items[0].AlreadyAdded);
        Assert.Equal("repo-a", items[0].Name);
    }

    [Fact]
    public void BuildCandidates_ExistingWorkingDir_IsMarkedAlreadyAdded()
    {
        var found = new[] { new RepoCandidate("repo-a", @"C:\src\repo-a") };
        var current = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
        var existing = new HashSet<string>(new[] { @"c:\src\repo-a" }, StringComparer.OrdinalIgnoreCase);

        var items = RepoImportModalViewModel.BuildCandidates(found, current, existing);

        Assert.Single(items);
        Assert.True(items[0].AlreadyAdded);
        Assert.True(items[0].IsChecked); // already-added rows render ticked
    }

    [Fact]
    public void BuildCandidates_SkipsPathsAlreadyShown()
    {
        var found = new[] { new RepoCandidate("repo-a", @"C:\src\repo-a") };
        var current = new HashSet<string>(new[] { @"c:\src\repo-a" }, StringComparer.OrdinalIgnoreCase);
        var existing = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

        var items = RepoImportModalViewModel.BuildCandidates(found, current, existing);

        Assert.Empty(items);
    }
}
```

- [ ] **Step 2: Run tests to verify they fail**

Run: `dotnet test tests/ClaudeDo.Ui.Tests --filter RepoImportCandidatesTests`
Expected: FAIL — `RepoImportModalViewModel` does not exist (compile error).

- [ ] **Step 3: Implement the modal VM**

Create `src/ClaudeDo.Ui/ViewModels/Modals/RepoImportModalViewModel.cs`:

```csharp
using System.Collections.ObjectModel;
using System.ComponentModel;
using ClaudeDo.Data;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Ui.Services;
using CommunityToolkit.Mvvm.Input;
using Microsoft.EntityFrameworkCore;

namespace ClaudeDo.Ui.ViewModels.Modals;

public sealed partial class RepoImportModalViewModel : ViewModelBase
{
    private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
    private readonly HashSet<string> _existingDirs = new(StringComparer.OrdinalIgnoreCase);

    public ObservableCollection<RepoImportItemViewModel> Repos { get; } = new();

    public Action? CloseAction { get; set; }

    public int CreateCount => Repos.Count(r => r.IsChecked && !r.AlreadyAdded);
    public bool CanCreate => CreateCount > 0;
    public string CreateButtonText => $"Create {CreateCount} list(s)";

    public RepoImportModalViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory)
    {
        _dbFactory = dbFactory;
    }

    public async Task LoadAsync(CancellationToken ct = default)
    {
        Repos.Clear();
        _existingDirs.Clear();

        await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
        var lists = new ListRepository(ctx);
        foreach (var l in await lists.GetAllAsync(ct))
        {
            if (!string.IsNullOrWhiteSpace(l.WorkingDir))
                _existingDirs.Add(l.WorkingDir!);
        }
        NotifyCreateState();
    }

    public void AddFolders(IEnumerable<string> folders)
    {
        var current = new HashSet<string>(
            Repos.Select(r => r.FullPath), StringComparer.OrdinalIgnoreCase);

        foreach (var folder in folders)
        {
            var found = RepoScanner.Scan(folder);
            foreach (var item in BuildCandidates(found, current, _existingDirs))
            {
                item.PropertyChanged += OnItemChanged;
                Repos.Add(item);
                current.Add(item.FullPath);
            }
        }
        NotifyCreateState();
    }

    public static List<RepoImportItemViewModel> BuildCandidates(
        IEnumerable<RepoCandidate> found,
        IReadOnlySet<string> currentPaths,
        IReadOnlySet<string> existingDirs)
    {
        var items = new List<RepoImportItemViewModel>();
        foreach (var c in found)
        {
            if (currentPaths.Contains(c.FullPath)) continue;
            items.Add(new RepoImportItemViewModel
            {
                Name = c.Name,
                FullPath = c.FullPath,
                AlreadyAdded = existingDirs.Contains(c.FullPath),
                IsChecked = true,
            });
        }
        return items;
    }

    private void OnItemChanged(object? sender, PropertyChangedEventArgs e)
    {
        if (e.PropertyName == nameof(RepoImportItemViewModel.IsChecked))
            NotifyCreateState();
    }

    private void NotifyCreateState()
    {
        OnPropertyChanged(nameof(CreateCount));
        OnPropertyChanged(nameof(CanCreate));
        OnPropertyChanged(nameof(CreateButtonText));
    }

    [RelayCommand]
    private async Task CreateAsync()
    {
        var toCreate = Repos.Where(r => r.IsChecked && !r.AlreadyAdded).ToList();
        if (toCreate.Count > 0)
        {
            await using var ctx = await _dbFactory.CreateDbContextAsync();
            var lists = new ListRepository(ctx);
            foreach (var r in toCreate)
            {
                await lists.AddAsync(new ListEntity
                {
                    Id = Guid.NewGuid().ToString("N"),
                    Name = r.Name,
                    WorkingDir = r.FullPath,
                    DefaultCommitType = CommitTypeRegistry.DefaultType,
                    CreatedAt = DateTime.UtcNow,
                });
            }
        }
        CloseAction?.Invoke();
    }

    [RelayCommand]
    private void Cancel() => CloseAction?.Invoke();
}
```

- [ ] **Step 4: Run tests to verify they pass**

Run: `dotnet test tests/ClaudeDo.Ui.Tests --filter RepoImportCandidatesTests`
Expected: PASS (3 tests).

- [ ] **Step 5: Commit**

```bash
git add src/ClaudeDo.Ui/ViewModels/Modals/RepoImportModalViewModel.cs tests/ClaudeDo.Ui.Tests/RepoImportCandidatesTests.cs
git commit -m "feat(ui): add RepoImportModalViewModel with candidate merge logic"
```

---

## Task 4: RepoImportModalView

**Files:**
- Create: `src/ClaudeDo.Ui/Views/Modals/RepoImportModalView.axaml`
- Create: `src/ClaudeDo.Ui/Views/Modals/RepoImportModalView.axaml.cs`

Modeled on `AboutModalView.axaml` (header/body/footer) and `ListSettingsModalView.axaml.cs` (folder picker).

- [ ] **Step 1: Create the view XAML**

Create `src/ClaudeDo.Ui/Views/Modals/RepoImportModalView.axaml`:

```xml
<Window xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
        x:Class="ClaudeDo.Ui.Views.Modals.RepoImportModalView"
        x:DataType="vm:RepoImportModalViewModel"
        Title="Add repos as lists"
        Width="560" Height="480"
        WindowDecorations="None"
        ExtendClientAreaToDecorationsHint="True"
        WindowStartupLocation="CenterOwner"
        Background="{DynamicResource SurfaceBrush}">
  <Window.KeyBindings>
    <KeyBinding Gesture="Escape" Command="{Binding CancelCommand}"/>
  </Window.KeyBindings>
  <Border BorderBrush="{DynamicResource LineBrush}" BorderThickness="1">
    <Grid RowDefinitions="36,Auto,*,52">

      <!-- Header -->
      <Border Grid.Row="0" Background="{DynamicResource DeepBrush}"
              BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,0,0,1">
        <Grid ColumnDefinitions="*,Auto" Margin="14,0">
          <TextBlock Text="ADD REPOS AS LISTS" FontFamily="{DynamicResource MonoFont}" FontSize="11"
                     LetterSpacing="1.4" Foreground="{DynamicResource TextBrush}" VerticalAlignment="Center"/>
          <Button Grid.Column="1" Classes="icon-btn" Content="✕" FontSize="12"
                  Command="{Binding CancelCommand}" VerticalAlignment="Center"/>
        </Grid>
      </Border>

      <!-- Add folder row -->
      <Border Grid.Row="1" Padding="16,12,16,4">
        <Button Content="Add folder…" Click="AddFolderClicked" HorizontalAlignment="Left"/>
      </Border>

      <!-- Repo checklist -->
      <ScrollViewer Grid.Row="2" Padding="16,4,16,8">
        <ItemsControl ItemsSource="{Binding Repos}">
          <ItemsControl.ItemTemplate>
            <DataTemplate DataType="vm:RepoImportItemViewModel">
              <Grid ColumnDefinitions="Auto,*,Auto" Margin="0,4">
                <CheckBox Grid.Column="0"
                          IsChecked="{Binding IsChecked, Mode=TwoWay}"
                          IsEnabled="{Binding CanToggle}"
                          VerticalAlignment="Center"/>
                <StackPanel Grid.Column="1" Margin="6,0" VerticalAlignment="Center">
                  <TextBlock Text="{Binding Name}" Foreground="{DynamicResource TextBrush}" FontSize="13"/>
                  <TextBlock Text="{Binding FullPath}" Foreground="{DynamicResource TextFaintBrush}"
                             FontFamily="{DynamicResource MonoFont}" FontSize="10"
                             TextTrimming="CharacterEllipsis"/>
                </StackPanel>
                <TextBlock Grid.Column="2" Text="(already added)"
                           Foreground="{DynamicResource TextFaintBrush}" FontSize="11"
                           VerticalAlignment="Center"
                           IsVisible="{Binding AlreadyAdded}"/>
              </Grid>
            </DataTemplate>
          </ItemsControl.ItemTemplate>
        </ItemsControl>
      </ScrollViewer>

      <!-- Footer -->
      <Border Grid.Row="3" Background="{DynamicResource DeepBrush}"
              BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,1,0,0">
        <StackPanel Orientation="Horizontal" Spacing="8" HorizontalAlignment="Right"
                    VerticalAlignment="Center" Margin="16,0">
          <Button Content="Cancel" Command="{Binding CancelCommand}" MinWidth="90"/>
          <Button Content="{Binding CreateButtonText}" Command="{Binding CreateCommand}"
                  IsEnabled="{Binding CanCreate}" MinWidth="120" Classes="accent"/>
        </StackPanel>
      </Border>

    </Grid>
  </Border>
</Window>
```

- [ ] **Step 2: Create the code-behind with folder picker**

Create `src/ClaudeDo.Ui/Views/Modals/RepoImportModalView.axaml.cs`:

```csharp
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using ClaudeDo.Ui.ViewModels.Modals;

namespace ClaudeDo.Ui.Views.Modals;

public partial class RepoImportModalView : Window
{
    public RepoImportModalView()
    {
        InitializeComponent();
    }

    private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e)
    {
        if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
            BeginMoveDrag(e);
    }

    private async void AddFolderClicked(object? sender, RoutedEventArgs e)
    {
        if (DataContext is not RepoImportModalViewModel vm) return;
        var top = TopLevel.GetTopLevel(this);
        if (top is null) return;

        var folders = await top.StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
        {
            Title = "Choose folders containing repos",
            AllowMultiple = true,
        });
        if (folders.Count == 0) return;

        vm.AddFolders(folders.Select(f => f.Path.LocalPath));
    }
}
```

- [ ] **Step 3: Build to verify it compiles**

Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj`
Expected: Build succeeded. (`TitleBar_PointerPressed` is unused for now but kept for parity with other modals; if the build warns as error, leave it — other modals keep the same handler.)

- [ ] **Step 4: Commit**

```bash
git add src/ClaudeDo.Ui/Views/Modals/RepoImportModalView.axaml src/ClaudeDo.Ui/Views/Modals/RepoImportModalView.axaml.cs
git commit -m "feat(ui): add RepoImportModalView"
```

---

## Task 5: DI registration

**Files:**
- Modify: `src/ClaudeDo.App/Program.cs:106` (after `ListSettingsModalViewModel` registration)

- [ ] **Step 1: Register the modal VM and its factory**

In `src/ClaudeDo.App/Program.cs`, after the line `sc.AddTransient<ListSettingsModalViewModel>();` add:

```csharp
        sc.AddTransient<RepoImportModalViewModel>();
        sc.AddTransient<Func<RepoImportModalViewModel>>(sp => () => sp.GetRequiredService<RepoImportModalViewModel>());
```

(`RepoImportModalViewModel` is in namespace `ClaudeDo.Ui.ViewModels.Modals`, already imported in `Program.cs` via the existing modal VM usings — verify the using is present; if not, add `using ClaudeDo.Ui.ViewModels.Modals;`.)

- [ ] **Step 2: Build to verify it compiles**

Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj`
Expected: Build succeeded.

- [ ] **Step 3: Commit**

```bash
git add src/ClaudeDo.App/Program.cs
git commit -m "chore(di): register RepoImportModalViewModel"
```

---

## Task 6: Lists island entry point

**Files:**
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs`
- Modify: `src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml`
- Modify: `src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml.cs`

- [ ] **Step 1: Add Func + command to the VM**

In `src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs`, next to the existing `ShowListSettingsModal` property (around line 30), add:

```csharp
    public Func<RepoImportModalViewModel, System.Threading.Tasks.Task>? ShowRepoImportModal { get; set; }
```

Then add a command (place it near `CreateListAsync`, e.g. after the `OpenWorktreesOverviewAsync` command around line 71):

```csharp
    [RelayCommand]
    private async System.Threading.Tasks.Task OpenRepoImportAsync()
    {
        if (ShowRepoImportModal is null || _services is null) return;
        var vm = _services.GetRequiredService<RepoImportModalViewModel>();
        await vm.LoadAsync();
        await ShowRepoImportModal(vm);
        await LoadAsync();
    }
```

(`RepoImportModalViewModel` is in `ClaudeDo.Ui.ViewModels.Modals`, already imported at the top of this file.)

- [ ] **Step 2: Add the folder button in XAML**

In `src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml`, replace the existing `+ New list` button block (lines 171-183) with a row that holds both the new-list button and a folder-scan button:

```xml
        <!-- New list + import row -->
        <Grid ColumnDefinitions="*,Auto" Margin="0,4,0,0">
          <Button Grid.Column="0" Classes="new-list-btn"
                  Command="{Binding CreateListCommand}">
            <StackPanel Orientation="Horizontal" Spacing="6">
              <PathIcon Data="{StaticResource Icon.Plus}"
                        Width="13" Height="13"
                        Foreground="{DynamicResource TextMuteBrush}"
                        VerticalAlignment="Center"/>
              <TextBlock Text="New list" FontSize="12"
                         Foreground="{DynamicResource TextMuteBrush}"
                         VerticalAlignment="Center"/>
            </StackPanel>
          </Button>
          <Button Grid.Column="1" Classes="icon-btn" Margin="6,0,0,0"
                  Command="{Binding OpenRepoImportCommand}"
                  ToolTip.Tip="Add repos as lists">
            <PathIcon Data="{StaticResource Icon.Folder}"
                      Width="14" Height="14"
                      Foreground="{DynamicResource TextMuteBrush}"/>
          </Button>
        </Grid>
```

- [ ] **Step 3: Wire the Func in the code-behind**

In `src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml.cs`, inside the `DataContextChanged` handler (after the `vm.ShowWorktreesOverviewModal = ...` assignment, before the closing brace of the `if` block around line 66), add:

```csharp
                vm.ShowRepoImportModal = async modal =>
                {
                    var window = new RepoImportModalView { DataContext = modal };
                    modal.CloseAction = () => window.Close();
                    var top = TopLevel.GetTopLevel(this) as Window;
                    if (top is null) window.Show();
                    else await window.ShowDialog(top);
                };
```

(`RepoImportModalView` is in `ClaudeDo.Ui.Views.Modals`, already imported in this file.)

- [ ] **Step 4: Build to verify it compiles**

Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj`
Expected: Build succeeded.

- [ ] **Step 5: Commit**

```bash
git add src/ClaudeDo.Ui/ViewModels/Islands/ListsIslandViewModel.cs src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml src/ClaudeDo.Ui/Views/Islands/ListsIslandView.axaml.cs
git commit -m "feat(ui): add repo import button to Lists island"
```

---

## Task 7: Help-menu entry point

**Files:**
- Modify: `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs`
- Modify: `src/ClaudeDo.App/Program.cs` (pass the Func into the shell VM)
- Modify: `src/ClaudeDo.Ui/Views/MainWindow.axaml`
- Modify: `src/ClaudeDo.Ui/Views/MainWindow.axaml.cs`

- [ ] **Step 1: Add Func, factory field, and command to the shell VM**

In `src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs`:

(a) Near the `ShowAboutModal` property (line 44), add:

```csharp
    public Func<RepoImportModalViewModel, Task>? ShowRepoImportModal { get; set; }
```

(b) Add a backing field for the factory next to `_worktreesOverviewVmFactory` (declared as a private readonly field elsewhere in the class). Add:

```csharp
    private readonly Func<RepoImportModalViewModel>? _repoImportVmFactory;
```

(c) Add a parameter to the public constructor (line 162-171) — append after `mergeVmFactory`:

```csharp
        Func<MergeModalViewModel> mergeVmFactory,
        Func<RepoImportModalViewModel> repoImportVmFactory)
```

and in the constructor body assign it (next to `_mergeVmFactory = mergeVmFactory;`):

```csharp
        _repoImportVmFactory = repoImportVmFactory;
```

(d) Add the command near `OpenAbout` (line 256):

```csharp
    [RelayCommand]
    private async Task OpenRepoImport()
    {
        if (ShowRepoImportModal is null || _repoImportVmFactory is null) return;
        var vm = _repoImportVmFactory();
        await vm.LoadAsync();
        await ShowRepoImportModal(vm);
        if (Lists is not null) await Lists.LoadAsync();
    }
```

(`RepoImportModalViewModel` is in `ClaudeDo.Ui.ViewModels.Modals`, already imported in this file.)

- [ ] **Step 2: Pass the Func into the shell VM in DI**

`IslandsShellViewModel` is registered with `sc.AddSingleton<IslandsShellViewModel>();` (Program.cs:123), which resolves constructor params from the container. Since Task 5 registered `Func<RepoImportModalViewModel>`, no change to the registration call is required — the new constructor parameter resolves automatically. Verify by building in Step 5.

- [ ] **Step 3: Add the Help-menu item**

In `src/ClaudeDo.Ui/Views/MainWindow.axaml`, inside the Help `MenuItem` (after the `About…` item at line 74), add:

```xml
              <MenuItem Header="Add repos as lists…" Command="{Binding OpenRepoImportCommand}"/>
```

- [ ] **Step 4: Wire the Func in MainWindow code-behind**

In `src/ClaudeDo.Ui/Views/MainWindow.axaml.cs`, inside `OnDataContextChanged` (after the `vm.ShowWorktreesOverviewModal = ...` block, before the closing brace of the `if` at line 65), add:

```csharp
            vm.ShowRepoImportModal = async (modal) =>
            {
                var dlg = new RepoImportModalView { DataContext = modal };
                modal.CloseAction = () => dlg.Close();
                await dlg.ShowDialog(this);
            };
```

(`RepoImportModalView` is in `ClaudeDo.Ui.Views.Modals`, already imported in this file.)

- [ ] **Step 5: Build to verify it compiles**

Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj`
Expected: Build succeeded (this also builds the Ui project).

- [ ] **Step 6: Commit**

```bash
git add src/ClaudeDo.Ui/ViewModels/IslandsShellViewModel.cs src/ClaudeDo.Ui/Views/MainWindow.axaml src/ClaudeDo.Ui/Views/MainWindow.axaml.cs
git commit -m "feat(ui): add 'Add repos as lists' Help-menu entry point"
```

---

## Task 8: Manual verification + docs

**Files:**
- Modify: `src/ClaudeDo.Ui/CLAUDE.md`

- [ ] **Step 1: Run the full Ui test suite**

Run: `dotnet test tests/ClaudeDo.Ui.Tests`
Expected: PASS (all tests, including the new `RepoScannerTests` and `RepoImportCandidatesTests`).

- [ ] **Step 2: Manual smoke test**

Launch the app (`dotnet run --project src/ClaudeDo.App/ClaudeDo.App.csproj`). Verify:
- Lists island shows a folder button next to `+ New list`; clicking it opens the modal.
- Help menu shows `Add repos as lists…`; clicking it opens the same modal.
- `Add folder…` → pick a parent folder containing git repos → repos appear as ticked rows; non-repo subfolders are absent.
- A repo that already has a list appears ticked, disabled, with `(already added)`.
- The confirm button reads `Create N list(s)` and is disabled when N is 0.
- Confirming creates the lists; they appear in the Lists island immediately after the modal closes.

Note: if you cannot run the GUI in this environment, state that explicitly rather than claiming the UI works.

- [ ] **Step 3: Update CLAUDE.md**

In `src/ClaudeDo.Ui/CLAUDE.md`, under the `## Views` section, add a bullet:

```markdown
- **RepoImportModalView** — bulk-creates lists from git repos discovered under chosen parent folders. Opened via the folder button beside "New list" in the Lists island, or the "Add repos as lists…" Help-menu item. Repos already wired to a list show as disabled/"(already added)".
```

- [ ] **Step 4: Commit**

```bash
git add src/ClaudeDo.Ui/CLAUDE.md
git commit -m "docs(ui): document RepoImportModalView"
```

---

## Self-Review Notes

- **Spec coverage:** Entry points (Help menu — Task 7; Lists island button — Task 6); `RepoScanner` non-recursive `.git` dir/file detection (Task 1); `RepoImportModalViewModel` load existing dirs + merge + create (Task 3); already-added disabled rows + `(already added)` label (Tasks 2/3/4); combined multi-folder checklist with path dedupe (Task 3 `AddFolders`); defaults Name/WorkingDir/DefaultCommitType (Task 3 `CreateAsync`); reload Lists island after close (Tasks 6/7); DI registration (Task 5); tests for scanner + merge logic (Tasks 1/3). All spec sections map to a task.
- **Type consistency:** `RepoCandidate(Name, FullPath)`, `RepoScanner.Scan`, `RepoImportItemViewModel{Name,FullPath,AlreadyAdded,CanToggle,IsChecked}`, `RepoImportModalViewModel{Repos,CreateCount,CanCreate,CreateButtonText,LoadAsync,AddFolders,BuildCandidates,CreateCommand,CancelCommand,ShowRepoImportModal,CloseAction}` used consistently across tasks.
- **YAGNI:** No recursive scan, no inline rename, no per-list model/prompt/agent during import — all explicitly out of scope.
