# Layer B — Multi-Worktree Merge Cockpit 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:** Turn the worktrees-overview modal into a batch-merge cockpit (multi-select N worktrees → one target branch → "Merge all" with skip-and-continue conflict collection), and migrate `WorktreeModalView`'s bespoke inline diff onto the canonical `DiffLinesView`.

**Architecture:** The cockpit VM keeps depending on the concrete `WorkerClient` (the overview/cleanup/state methods live only on `WorkerClient`, not `IWorkerClient`). The batch loop is extracted into a delegate-driven method `MergeSelectedAsync(Func<...> mergeFn)` so it is unit-testable with a fake merge function and a never-connected `WorkerClient`. Clean merges (`Status=="merged"`) update the row; conflicts (`Status=="conflict"`, which `MergeTaskAsync` already auto-aborts) are collected into a `ConflictRows` list whose rows expose a `Resolve` button wired to an inert `RequestConflictResolution(taskId, targetBranch)` seam. The diff migration replaces the right-pane `ItemsControl` in `WorktreeModalView` with `DiffLinesView`, feeding it `DiffLineViewModel`s produced by `UnifiedDiffParser`, and deletes the now-dead `WorktreeDiffLineViewModel`/`WorktreeDiffLineKind`.

**Tech Stack:** .NET 8, Avalonia 12, CommunityToolkit.Mvvm source generators, xUnit. Build UI with `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`; run `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release`.

**Frozen contracts reused (do NOT modify):**
- `WorkerClient.MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage) -> Task<MergeResultDto>`
- `WorkerClient.GetMergeTargetsAsync(string taskId) -> Task<MergeTargetsDto?>` (`MergeTargetsDto(string DefaultBranch, IReadOnlyList<string> LocalBranches)`)
- `MergeResultDto(string Status, IReadOnlyList<string> ConflictFiles, string? ErrorMessage)` — `Status` is `"merged" | "conflict" | "blocked" | <other>`
- `WorkerClient.GetWorktreesOverviewAsync`, `CleanupFinishedWorktreesAsync`, `SetWorktreeStateAsync`, `ForceRemoveWorktreeAsync`
- `GitService.GetFileDiffAsync(worktreePath, baseCommit?, relativePath)` returns a `git diff` blob including the `diff --git` header (so `UnifiedDiffParser.Parse` handles it)
- `DiffLinesView` (`Lines` styled property, `IEnumerable?`), `DiffLineViewModel`, `DiffFileViewModel`, `UnifiedDiffParser.Parse` / `.Flatten`

**Do NOT touch:** any worker-side files (`WorkerHub`, `TaskMergeService`, `GitService`), `IWorkerClient.cs` / `WorkerClient.cs`, `WorkConsole.axaml`, `DetailsIslandViewModel.cs`, and do not create any `ConflictResolver` UI or reference any `ConflictResolver` type.

---

## File Structure

- `src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs` — **modify.** Add `BatchMergeOutcome` enum; add `IsChecked`/`MergeOutcome` (+ derived) to the row VM; add `MergeTargets`, `SelectedTarget`, `SelectedCount`, `IsMerging`, `BatchProgress`, `ConflictRows`, the `RequestConflictResolution` seam, `MergeSelectedAsync`, `MergeAllCommand`, `ResolveConflictCommand`, `ToggleSelectAllCommand`, target loading, and per-row check subscription. Keep all existing context-menu commands/wiring intact.
- `src/ClaudeDo.Ui/Views/Modals/WorktreesOverviewModalView.axaml` — **modify.** Add a per-row checkbox + outcome badge, a target `ComboBox` + "Merge all" button + progress text in the toolbar, and a "Needs resolution" panel listing `ConflictRows` with `Resolve` buttons.
- `src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs` — **modify.** Replace `SelectedFileDiffLines` element type with `DiffLineViewModel` produced via `UnifiedDiffParser`; delete `WorktreeDiffLineKind` and `WorktreeDiffLineViewModel`.
- `src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml` — **modify.** Replace the right-pane `ItemsControl` with `ctl:DiffLinesView`; drop the `DiffLineKindToBrushConverter` resource.
- `src/ClaudeDo.Localization/locales/en.json` + `de.json` — **modify.** Add new `modals.worktreesOverview.*` and `vm.worktreesOverview.*` keys (keep parity).
- `tests/ClaudeDo.Ui.Tests/ViewModels/WorktreesOverviewBatchMergeTests.cs` — **create.** Unit tests for `MergeSelectedAsync` skip-and-continue, conflict collection, progress, selection gating, and the resolve seam.

No `IWorkerClient` change → no test-fake updates needed.

---

## Task 1: Row-level batch state (outcome enum + row VM fields)

**Files:**
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs`
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/WorktreesOverviewBatchMergeTests.cs`

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

Create `tests/ClaudeDo.Ui.Tests/ViewModels/WorktreesOverviewBatchMergeTests.cs`:

```csharp
using ClaudeDo.Data.Models;
using ClaudeDo.Ui.ViewModels.Modals;
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
using Xunit;

namespace ClaudeDo.Ui.Tests.ViewModels;

public class WorktreesOverviewBatchMergeTests
{
    private static WorktreeOverviewRowViewModel ActiveRow(string id) => new()
    {
        TaskId = id,
        TaskTitle = $"Task {id}",
        TaskStatus = TaskStatus.WaitingForReview,
        State = WorktreeState.Active,
    };

    [Fact]
    public void Row_outcome_helpers_reflect_state()
    {
        var row = ActiveRow("a");
        Assert.Equal(BatchMergeOutcome.None, row.MergeOutcome);
        Assert.False(row.IsConflict);

        row.MergeOutcome = BatchMergeOutcome.Conflict;
        Assert.True(row.IsConflict);

        row.MergeOutcome = BatchMergeOutcome.Merged;
        Assert.False(row.IsConflict);
    }
}
```

- [ ] **Step 2: Run test to verify it fails**

Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter WorktreesOverviewBatchMergeTests`
Expected: FAIL — `BatchMergeOutcome` and `MergeOutcome`/`IsConflict` do not exist (compile error).

- [ ] **Step 3: Add the enum and row fields**

In `WorktreesOverviewModalViewModel.cs`, add the enum just above `WorktreeOverviewRowViewModel`:

```csharp
public enum BatchMergeOutcome { None, Merging, Merged, Conflict, Blocked, Failed }
```

Inside `WorktreeOverviewRowViewModel`, add after the existing `_isSelected` field:

```csharp
    [ObservableProperty] private bool _isChecked;
    [ObservableProperty]
    [NotifyPropertyChangedFor(nameof(IsConflict))]
    [NotifyPropertyChangedFor(nameof(HasOutcome))]
    private BatchMergeOutcome _mergeOutcome;

    public bool IsConflict => MergeOutcome == BatchMergeOutcome.Conflict;
    public bool HasOutcome => MergeOutcome != BatchMergeOutcome.None;
```

- [ ] **Step 4: Run test to verify it passes**

Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter WorktreesOverviewBatchMergeTests`
Expected: PASS (1 test).

- [ ] **Step 5: Commit**

```bash
git add src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs tests/ClaudeDo.Ui.Tests/ViewModels/WorktreesOverviewBatchMergeTests.cs
git commit -m "feat(ui): add batch-merge row state to worktrees cockpit VM"
```

---

## Task 2: Batch orchestration (`MergeSelectedAsync` skip-and-continue)

**Files:**
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs`
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/WorktreesOverviewBatchMergeTests.cs`

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

Append to `WorktreesOverviewBatchMergeTests.cs`. The helper builds a VM with a never-connected `WorkerClient` (the loop never touches it) and seeds `Rows` directly:

```csharp
    private static WorktreesOverviewModalViewModel NewVm() =>
        new(new ClaudeDo.Ui.Services.WorkerClient("http://127.0.0.1:1/hub"), () => null!);

    private static MergeResultDto Merged() => new("merged", System.Array.Empty<string>(), null);
    private static MergeResultDto Conflict() => new("conflict", new[] { "f.cs" }, null);
    private static MergeResultDto Blocked() => new("blocked", System.Array.Empty<string>(), "blocked");

    [Fact]
    public async System.Threading.Tasks.Task MergeSelected_only_processes_checked_active_rows()
    {
        var vm = NewVm();
        var a = ActiveRow("a"); a.IsChecked = true;
        var b = ActiveRow("b"); b.IsChecked = false;          // unchecked -> skipped
        var c = ActiveRow("c"); c.IsChecked = true; c.State = WorktreeState.Merged; // not active -> skipped
        vm.Rows.Add(a); vm.Rows.Add(b); vm.Rows.Add(c);
        vm.SelectedTarget = "main";

        var seen = new System.Collections.Generic.List<string>();
        await vm.MergeSelectedAsync((id, target, remove, msg) =>
        {
            seen.Add(id);
            Assert.Equal("main", target);
            Assert.False(remove);                              // removeWorktree must be false
            return System.Threading.Tasks.Task.FromResult(Merged());
        });

        Assert.Equal(new[] { "a" }, seen);
        Assert.Equal(BatchMergeOutcome.Merged, a.MergeOutcome);
        Assert.False(a.IsChecked);                             // cleared after merge
    }

    [Fact]
    public async System.Threading.Tasks.Task MergeSelected_continues_past_conflict_and_collects_it()
    {
        var vm = NewVm();
        var a = ActiveRow("a"); a.IsChecked = true;
        var b = ActiveRow("b"); b.IsChecked = true;
        var c = ActiveRow("c"); c.IsChecked = true;
        vm.Rows.Add(a); vm.Rows.Add(b); vm.Rows.Add(c);
        vm.SelectedTarget = "main";

        await vm.MergeSelectedAsync((id, target, remove, msg) =>
            System.Threading.Tasks.Task.FromResult(id == "b" ? Conflict() : Merged()));

        Assert.Equal(BatchMergeOutcome.Merged, a.MergeOutcome);
        Assert.Equal(BatchMergeOutcome.Conflict, b.MergeOutcome);
        Assert.Equal(BatchMergeOutcome.Merged, c.MergeOutcome);   // continued past the conflict
        Assert.Contains(b, vm.ConflictRows);
        Assert.Single(vm.ConflictRows);
    }

    [Fact]
    public async System.Threading.Tasks.Task MergeSelected_maps_blocked_and_exception_to_failure_outcomes()
    {
        var vm = NewVm();
        var a = ActiveRow("a"); a.IsChecked = true;
        var b = ActiveRow("b"); b.IsChecked = true;
        vm.Rows.Add(a); vm.Rows.Add(b);
        vm.SelectedTarget = "main";

        await vm.MergeSelectedAsync((id, target, remove, msg) => id == "a"
            ? System.Threading.Tasks.Task.FromResult(Blocked())
            : throw new System.InvalidOperationException("boom"));

        Assert.Equal(BatchMergeOutcome.Blocked, a.MergeOutcome);
        Assert.Equal(BatchMergeOutcome.Failed, b.MergeOutcome);
        Assert.Empty(vm.ConflictRows);
        Assert.False(vm.IsMerging);
    }

    [Fact]
    public async System.Threading.Tasks.Task MergeSelected_noop_when_no_target()
    {
        var vm = NewVm();
        var a = ActiveRow("a"); a.IsChecked = true;
        vm.Rows.Add(a);
        vm.SelectedTarget = null;

        var called = false;
        await vm.MergeSelectedAsync((id, t, r, m) => { called = true; return System.Threading.Tasks.Task.FromResult(Merged()); });

        Assert.False(called);
        Assert.Equal(BatchMergeOutcome.None, a.MergeOutcome);
    }
```

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

Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter WorktreesOverviewBatchMergeTests`
Expected: FAIL — `MergeSelectedAsync`, `ConflictRows`, `IsMerging`, `SelectedTarget` do not exist (compile error).

- [ ] **Step 3: Implement the orchestration + cockpit fields**

In `WorktreesOverviewModalViewModel.cs`, add these `using`s if missing: `using ClaudeDo.Ui.Services;` (already present). Add fields/properties to `WorktreesOverviewModalViewModel` (after the existing `_selectedRow` field):

```csharp
    [ObservableProperty][NotifyCanExecuteChangedFor(nameof(MergeAllCommand))] private string? _selectedTarget;
    [ObservableProperty][NotifyCanExecuteChangedFor(nameof(MergeAllCommand))] private int _selectedCount;
    [ObservableProperty][NotifyCanExecuteChangedFor(nameof(MergeAllCommand))] private bool _isMerging;
    [ObservableProperty] private string? _batchProgress;

    public ObservableCollection<string> MergeTargets { get; } = new();
    public ObservableCollection<WorktreeOverviewRowViewModel> ConflictRows { get; } = new();

    /// Inert seam wired by the integrator to Layer C's resolver at merge time. (taskId, targetBranch)
    public Func<string, string, Task>? RequestConflictResolution { get; set; }
```

Add a helper to enumerate rows regardless of grouped/flat mode, plus the orchestration method:

```csharp
    public IEnumerable<WorktreeOverviewRowViewModel> AllRows =>
        IsGlobal ? Groups.SelectMany(g => g.Rows) : Rows;

    public async Task MergeSelectedAsync(
        Func<string, string, bool, string, Task<MergeResultDto>> mergeFn,
        CancellationToken ct = default)
    {
        var target = SelectedTarget;
        if (string.IsNullOrWhiteSpace(target)) return;

        var selected = AllRows.Where(r => r.IsChecked && r.IsActive).ToList();
        if (selected.Count == 0) return;

        IsMerging = true;
        ConflictRows.Clear();
        var done = 0;
        try
        {
            foreach (var row in selected)
            {
                ct.ThrowIfCancellationRequested();
                row.MergeOutcome = BatchMergeOutcome.Merging;
                BatchProgress = Loc.T("vm.worktreesOverview.batchProgress", ++done, selected.Count);

                MergeResultDto result;
                try
                {
                    result = await mergeFn(row.TaskId, target!, false,
                        Loc.T("vm.merge.commitMessage", row.TaskTitle));
                }
                catch
                {
                    row.MergeOutcome = BatchMergeOutcome.Failed;
                    continue;
                }

                switch (result.Status)
                {
                    case "merged":
                        row.MergeOutcome = BatchMergeOutcome.Merged;
                        row.State = WorktreeState.Merged;
                        row.IsChecked = false;
                        break;
                    case "conflict":
                        row.MergeOutcome = BatchMergeOutcome.Conflict;
                        ConflictRows.Add(row);
                        break;
                    case "blocked":
                        row.MergeOutcome = BatchMergeOutcome.Blocked;
                        break;
                    default:
                        row.MergeOutcome = BatchMergeOutcome.Failed;
                        break;
                }
            }
            BatchProgress = Loc.T("vm.worktreesOverview.batchDone",
                selected.Count(r => r.MergeOutcome == BatchMergeOutcome.Merged), ConflictRows.Count);
        }
        finally
        {
            IsMerging = false;
        }
    }
```

> Note: `Loc.T` keys are added in Task 5; they resolve to the key name (harmless) until then, so tests pass now.

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

Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter WorktreesOverviewBatchMergeTests`
Expected: PASS (5 tests).

- [ ] **Step 5: Commit**

```bash
git add src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs tests/ClaudeDo.Ui.Tests/ViewModels/WorktreesOverviewBatchMergeTests.cs
git commit -m "feat(ui): add skip-and-continue batch merge orchestration"
```

---

## Task 3: Selection tracking, target loading, commands + resolve seam

**Files:**
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs`
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/WorktreesOverviewBatchMergeTests.cs`

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

Append to `WorktreesOverviewBatchMergeTests.cs`:

```csharp
    [Fact]
    public void SelectedCount_tracks_checked_active_rows()
    {
        var vm = NewVm();
        var a = ActiveRow("a");
        var b = ActiveRow("b");
        var merged = ActiveRow("c"); merged.State = WorktreeState.Merged;
        vm.AddRowForTest(a); vm.AddRowForTest(b); vm.AddRowForTest(merged);

        Assert.Equal(0, vm.SelectedCount);
        a.IsChecked = true;
        Assert.Equal(1, vm.SelectedCount);
        b.IsChecked = true;
        merged.IsChecked = true;                 // not active -> not counted
        Assert.Equal(2, vm.SelectedCount);
        a.IsChecked = false;
        Assert.Equal(1, vm.SelectedCount);
    }

    [Fact]
    public void ResolveConflict_invokes_seam_with_task_and_target()
    {
        var vm = NewVm();
        vm.SelectedTarget = "release";
        var row = ActiveRow("x"); row.MergeOutcome = BatchMergeOutcome.Conflict;

        (string Task, string Target)? captured = null;
        vm.RequestConflictResolution = (taskId, target) => { captured = (taskId, target); return System.Threading.Tasks.Task.CompletedTask; };

        vm.ResolveConflictCommand.Execute(row);

        Assert.Equal(("x", "release"), captured);
    }

    [Fact]
    public void MergeAll_canExecute_requires_target_selection_and_idle()
    {
        var vm = NewVm();
        var a = ActiveRow("a");
        vm.AddRowForTest(a);

        Assert.False(vm.MergeAllCommand.CanExecute(null));   // no selection, no target
        a.IsChecked = true;
        Assert.False(vm.MergeAllCommand.CanExecute(null));   // still no target
        vm.SelectedTarget = "main";
        Assert.True(vm.MergeAllCommand.CanExecute(null));
        vm.IsMerging = true;
        Assert.False(vm.MergeAllCommand.CanExecute(null));   // busy
    }
```

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

Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter WorktreesOverviewBatchMergeTests`
Expected: FAIL — `AddRowForTest`, `ResolveConflictCommand`, `MergeAllCommand` do not exist (compile error).

- [ ] **Step 3: Implement subscription, commands, target loading**

In `WorktreesOverviewModalViewModel.cs`:

(a) Add a row-hook that recomputes `SelectedCount` when a row's `IsChecked` changes, and a test seam to add a hooked row. Add these methods to the class:

```csharp
    private void HookRow(WorktreeOverviewRowViewModel row)
    {
        row.PropertyChanged += (_, e) =>
        {
            if (e.PropertyName is nameof(WorktreeOverviewRowViewModel.IsChecked)
                               or nameof(WorktreeOverviewRowViewModel.State))
                RecomputeSelected();
        };
    }

    private void RecomputeSelected() =>
        SelectedCount = AllRows.Count(r => r.IsChecked && r.IsActive);

    // Test seam: adds a row to the flat list with selection tracking wired up.
    internal void AddRowForTest(WorktreeOverviewRowViewModel row)
    {
        HookRow(row);
        Rows.Add(row);
    }
```

(b) In `LoadAsync`, call `HookRow(row)` everywhere a row is added. Replace the two add sites:

In the grouped branch, change `foreach (var row in grp) group.Rows.Add(row);` to:

```csharp
                    foreach (var row in grp) { HookRow(row); group.Rows.Add(row); }
```

In the flat branch, change `foreach (var row in ordered) Rows.Add(row);` to:

```csharp
                foreach (var row in ordered) { HookRow(row); Rows.Add(row); }
```

Also, at the start of `LoadAsync` after `IsBusy = true;`, reset batch UI state and (re)load merge targets at the end of the `try`:

After `Rows.Clear(); Groups.Clear();` add:

```csharp
            ConflictRows.Clear();
            SelectedCount = 0;
            BatchProgress = null;
```

At the very end of the `try` block (after the if/else that fills rows/groups) add:

```csharp
            await LoadMergeTargetsAsync();
```

(c) Add target loading. The branch list is repo-level, so query it from the first active row:

```csharp
    private async Task LoadMergeTargetsAsync()
    {
        var anchor = AllRows.FirstOrDefault(r => r.IsActive);
        if (anchor is null) { MergeTargets.Clear(); SelectedTarget = null; return; }
        try
        {
            var targets = await _worker.GetMergeTargetsAsync(anchor.TaskId);
            MergeTargets.Clear();
            if (targets is null) { SelectedTarget = null; return; }
            foreach (var b in targets.LocalBranches) MergeTargets.Add(b);
            SelectedTarget = MergeTargets.Contains(targets.DefaultBranch)
                ? targets.DefaultBranch
                : MergeTargets.FirstOrDefault();
        }
        catch { MergeTargets.Clear(); SelectedTarget = null; }
    }
```

(d) Add the commands:

```csharp
    private bool CanMergeAll() => !IsMerging && SelectedCount > 0 && !string.IsNullOrWhiteSpace(SelectedTarget);

    [RelayCommand(CanExecute = nameof(CanMergeAll))]
    private Task MergeAll() => MergeSelectedAsync(_worker.MergeTaskAsync);

    [RelayCommand]
    private void ResolveConflict(WorktreeOverviewRowViewModel? row)
    {
        if (row is null) return;
        RequestConflictResolution?.Invoke(row.TaskId, SelectedTarget ?? "");
    }

    [RelayCommand]
    private void ToggleSelectAll()
    {
        var actives = AllRows.Where(r => r.IsActive).ToList();
        var allChecked = actives.Count > 0 && actives.All(r => r.IsChecked);
        foreach (var r in actives) r.IsChecked = !allChecked;
    }
```

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

Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter WorktreesOverviewBatchMergeTests`
Expected: PASS (8 tests total in this file).

- [ ] **Step 5: Build the app project to confirm the VM compiles against generated commands**

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

- [ ] **Step 6: Commit**

```bash
git add src/ClaudeDo.Ui/ViewModels/Modals/WorktreesOverviewModalViewModel.cs tests/ClaudeDo.Ui.Tests/ViewModels/WorktreesOverviewBatchMergeTests.cs
git commit -m "feat(ui): wire batch selection, target loading and resolve seam"
```

---

## Task 4: Cockpit view — checkboxes, target picker, Merge all, conflicts panel

**Files:**
- Modify: `src/ClaudeDo.Ui/Views/Modals/WorktreesOverviewModalView.axaml`

This task is AXAML only (no logic) → no new unit test; flag for visual verification.

- [ ] **Step 1: Add the batch toolbar controls**

In `WorktreesOverviewModalView.axaml`, replace the toolbar `StackPanel` (currently containing Refresh, Cleanup finished, StatusMessage) with one that adds select-all, the target picker, the Merge-all button and progress text. Replace the inner `<StackPanel Orientation="Horizontal" Spacing="8">...</StackPanel>` of the toolbar `Border` with:

```xml
        <StackPanel Orientation="Horizontal" Spacing="8">
          <Button Classes="btn" Content="{loc:Tr modals.worktreesOverview.refresh}" Command="{Binding RefreshCommand}" IsEnabled="{Binding !IsBusy}"/>
          <Button Classes="btn" Content="{loc:Tr modals.worktreesOverview.cleanupFinished}" Command="{Binding CleanupFinishedCommand}" IsEnabled="{Binding !IsBusy}"/>
          <Button Classes="btn" Content="{loc:Tr modals.worktreesOverview.selectAll}" Command="{Binding ToggleSelectAllCommand}"/>
          <Border Width="1" Background="{DynamicResource LineBrush}" Margin="4,2"/>
          <TextBlock Text="{loc:Tr modals.worktreesOverview.targetLabel}" VerticalAlignment="Center" Foreground="{DynamicResource TextDimBrush}"/>
          <ComboBox MinWidth="160"
                    ItemsSource="{Binding MergeTargets}"
                    SelectedItem="{Binding SelectedTarget, Mode=TwoWay}"/>
          <Button Classes="btn accent"
                  Content="{loc:Tr modals.worktreesOverview.mergeAll}"
                  Command="{Binding MergeAllCommand}"/>
          <TextBlock Text="{Binding SelectedCount, StringFormat='{}{0} selected'}"
                     VerticalAlignment="Center" Foreground="{DynamicResource TextDimBrush}"/>
          <TextBlock Text="{Binding BatchProgress}" VerticalAlignment="Center" Margin="8,0,0,0"
                     Foreground="{DynamicResource TextDimBrush}"/>
          <TextBlock Text="{Binding StatusMessage}" VerticalAlignment="Center" Margin="8,0,0,0"
                     Foreground="{DynamicResource TextDimBrush}"/>
        </StackPanel>
```

- [ ] **Step 2: Add a checkbox + outcome badge to the row template**

In the `WorktreeRowTemplate` `DataTemplate`, change the row `Grid` to add a leading checkbox column and a trailing outcome column. Replace the `<Grid ColumnDefinitions="*,90,80,80">...</Grid>` (the whole grid, lines for Task/State/Diff/Age) with:

```xml
        <Grid ColumnDefinitions="Auto,*,90,90,80,80">
          <CheckBox Grid.Column="0" VerticalAlignment="Center" Margin="0,0,8,0"
                    IsChecked="{Binding IsChecked, Mode=TwoWay}"
                    IsEnabled="{Binding IsActive}"
                    IsVisible="{Binding IsActive}"/>
          <StackPanel Grid.Column="1" Orientation="Vertical" Spacing="2">
            <TextBlock Classes="title" Text="{Binding TaskTitle}"/>
            <StackPanel Orientation="Horizontal" Spacing="4">
              <TextBlock Classes="meta" Text="{Binding TaskStatus}"/>
              <TextBlock Classes="meta" Text="•"
                         IsVisible="{Binding !PathExistsOnDisk}"/>
              <TextBlock Classes="meta" Text="{loc:Tr modals.worktreesOverview.phantom}" Foreground="{DynamicResource StatusErrorBrush}"
                         IsVisible="{Binding !PathExistsOnDisk}"
                         ToolTip.Tip="{loc:Tr modals.worktreesOverview.phantomTooltip}"/>
            </StackPanel>
          </StackPanel>
          <TextBlock Grid.Column="2" Classes="meta" VerticalAlignment="Center"
                     Text="{Binding MergeOutcome}"
                     IsVisible="{Binding HasOutcome}"/>
          <Border Grid.Column="3" CornerRadius="3" Padding="6,2" VerticalAlignment="Center"
                  Background="{Binding State, Converter={StaticResource WorktreeStateColor}}">
            <TextBlock Classes="meta" Text="{Binding State}" Foreground="{DynamicResource TextBrush}"
                       HorizontalAlignment="Center"/>
          </Border>
          <TextBlock Grid.Column="4" Classes="meta" Text="{Binding DiffStat}" VerticalAlignment="Center"/>
          <TextBlock Grid.Column="5" Classes="meta" Text="{Binding AgeText}" VerticalAlignment="Center"/>
        </Grid>
```

Then update the column-header `Grid` (the one with `ColumnDefinitions="*,90,80,80"` near the ScrollViewer top) to match the new column layout:

```xml
          <Grid ColumnDefinitions="Auto,*,90,90,80,80" Margin="12,0,12,4">
            <TextBlock Grid.Column="1" Classes="eyebrow" Text="{loc:Tr modals.worktreesOverview.columnTask}"/>
            <TextBlock Grid.Column="2" Classes="eyebrow" Text="{loc:Tr modals.worktreesOverview.columnOutcome}"/>
            <TextBlock Grid.Column="3" Classes="eyebrow" Text="{loc:Tr modals.worktreesOverview.columnState}"/>
            <TextBlock Grid.Column="4" Classes="eyebrow" Text="{loc:Tr modals.worktreesOverview.columnDiff}"/>
            <TextBlock Grid.Column="5" Classes="eyebrow" Text="{loc:Tr modals.worktreesOverview.columnAge}"/>
          </Grid>
```

- [ ] **Step 3: Add the "Needs resolution" panel**

Inside the content `ScrollViewer`'s root `StackPanel`, at the very top (before the column-header `Grid`), add a conflicts panel that only shows when there are conflicts:

```xml
          <Border IsVisible="{Binding ConflictRows.Count}"
                  Background="{DynamicResource ErrorTintBrush}"
                  BorderBrush="{DynamicResource StatusErrorBrush}"
                  BorderThickness="1" CornerRadius="6" Padding="12,8" Margin="0,0,0,12">
            <StackPanel Spacing="6">
              <TextBlock Classes="eyebrow" Text="{loc:Tr modals.worktreesOverview.needsResolution}"/>
              <ItemsControl ItemsSource="{Binding ConflictRows}">
                <ItemsControl.ItemTemplate>
                  <DataTemplate x:DataType="vm:WorktreeOverviewRowViewModel">
                    <Grid ColumnDefinitions="*,Auto" Margin="0,2">
                      <TextBlock Grid.Column="0" Classes="meta" VerticalAlignment="Center"
                                 Text="{Binding TaskTitle}"/>
                      <Button Grid.Column="1" Classes="btn"
                              Content="{loc:Tr modals.worktreesOverview.resolve}"
                              Command="{Binding $parent[Window].((vm:WorktreesOverviewModalViewModel)DataContext).ResolveConflictCommand}"
                              CommandParameter="{Binding}"/>
                    </Grid>
                  </DataTemplate>
                </ItemsControl.ItemTemplate>
              </ItemsControl>
            </StackPanel>
          </Border>
```

> `IsVisible="{Binding ConflictRows.Count}"` uses Avalonia's int→bool coercion (0 = false). If the build flags this, change to a value converter already present, but int→bool is supported.

- [ ] **Step 4: Build the app to verify the AXAML compiles**

Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
Expected: Build succeeded (compiled bindings resolve against the new VM members).

- [ ] **Step 5: Commit**

```bash
git add src/ClaudeDo.Ui/Views/Modals/WorktreesOverviewModalView.axaml
git commit -m "feat(ui): batch-merge cockpit view with checkboxes and conflicts panel"
```

---

## Task 5: Localization keys (en + de parity)

**Files:**
- Modify: `src/ClaudeDo.Localization/locales/en.json`
- Modify: `src/ClaudeDo.Localization/locales/de.json`

- [ ] **Step 1: Add the new keys to `en.json`**

Under `modals.worktreesOverview`, add:

```json
      "columnOutcome": "RESULT",
      "selectAll": "Select all",
      "targetLabel": "Target",
      "mergeAll": "Merge all",
      "needsResolution": "NEEDS RESOLUTION",
      "resolve": "Resolve"
```

Under `vm.worktreesOverview`, add:

```json
      "batchProgress": "Merging {0}/{1}…",
      "batchDone": "Merged {0}, {1} need resolution."
```

- [ ] **Step 2: Add the matching keys to `de.json`**

Under `modals.worktreesOverview`:

```json
      "columnOutcome": "ERGEBNIS",
      "selectAll": "Alle auswählen",
      "targetLabel": "Ziel",
      "mergeAll": "Alle mergen",
      "needsResolution": "ZU LÖSEN",
      "resolve": "Lösen"
```

Under `vm.worktreesOverview`:

```json
      "batchProgress": "Merge {0}/{1}…",
      "batchDone": "{0} gemergt, {1} zu lösen."
```

- [ ] **Step 3: Run the localization parity test**

Run: `dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release`
Expected: PASS (en/de key parity holds).

- [ ] **Step 4: Commit**

```bash
git add src/ClaudeDo.Localization/locales/en.json src/ClaudeDo.Localization/locales/de.json
git commit -m "feat(i18n): add batch-merge cockpit strings (en/de)"
```

---

## Task 6: Migrate `WorktreeModalView` diff onto `DiffLinesView`

**Files:**
- Modify: `src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs`
- Modify: `src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml`

- [ ] **Step 1: Switch the VM to the canonical diff model**

In `WorktreeModalViewModel.cs`:

(a) Delete the now-dead types at the top of the file:

```csharp
public enum WorktreeDiffLineKind { Header, Hunk, Added, Removed, Context }

public sealed partial class WorktreeDiffLineViewModel : ViewModelBase
{
    public required string Text { get; init; }
    public required WorktreeDiffLineKind Kind { get; init; }
}
```

(b) Change the collection declaration from:

```csharp
    public ObservableCollection<WorktreeDiffLineViewModel> SelectedFileDiffLines { get; } = new();
```

to:

```csharp
    public ObservableCollection<DiffLineViewModel> SelectedFileDiffLines { get; } = new();
```

(c) Replace the body of `LoadFileDiffAsync` (the `foreach (var line in diff.Split('\n'))` block) so it parses via `UnifiedDiffParser`. The method becomes:

```csharp
    private async Task LoadFileDiffAsync(WorktreeNodeViewModel? node)
    {
        SelectedFileDiffLines.Clear();

        if (node is null || node.IsDirectory || string.IsNullOrEmpty(node.RelativePath))
            return;

        string diff;
        try
        {
            diff = await _git.GetFileDiffAsync(WorktreePath, BaseCommit, node.RelativePath);
        }
        catch
        {
            return;
        }

        foreach (var line in UnifiedDiffParser.Flatten(UnifiedDiffParser.Parse(diff)))
            SelectedFileDiffLines.Add(line);
    }
```

(`DiffLineViewModel`, `DiffFileViewModel`, and `UnifiedDiffParser` are all in the same `ClaudeDo.Ui.ViewModels.Modals` namespace, so no new `using` is required.)

- [ ] **Step 2: Build to confirm the VM compiles and nothing else referenced the deleted types**

Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
Expected: Build succeeded. (If a compile error names `WorktreeDiffLineViewModel`/`WorktreeDiffLineKind` outside this file or the view, that reference must be migrated too — there should be none besides `WorktreeModalView.axaml`, handled next.)

- [ ] **Step 3: Swap the view's inline diff for `DiffLinesView`**

In `WorktreeModalView.axaml`:

(a) Remove the now-unused converter resource. Delete:

```xml
  <Window.Resources>
    <converters:DiffLineKindToBrushConverter x:Key="DiffLineKindToBrush"/>
  </Window.Resources>
```

(b) Replace the right-pane `ScrollViewer`'s `ItemsControl` (the `SelectableTextBlock` template bound to `SelectedFileDiffLines`) with the canonical control. Replace:

```xml
          <ItemsControl ItemsSource="{Binding SelectedFileDiffLines}">
            <ItemsControl.ItemTemplate>
              <DataTemplate DataType="vm:WorktreeDiffLineViewModel">
                <SelectableTextBlock Text="{Binding Text}"
                                     FontFamily="{DynamicResource MonoFont}"
                                     FontSize="{StaticResource FontSizeMono}"
                                     Foreground="{Binding Kind, Converter={StaticResource DiffLineKindToBrush}}"
                                     TextWrapping="NoWrap"/>
              </DataTemplate>
            </ItemsControl.ItemTemplate>
          </ItemsControl>
```

with:

```xml
          <ctl:DiffLinesView Lines="{Binding SelectedFileDiffLines}"/>
```

(The `xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"` namespace is already declared at the top of this file.)

- [ ] **Step 4: Build the app to verify the AXAML compiles**

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

- [ ] **Step 5: Commit**

```bash
git add src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml
git commit -m "refactor(ui): render worktree modal diff via canonical DiffLinesView"
```

---

## Task 7: Full build + test sweep

**Files:** none (verification only).

- [ ] **Step 1: Build the whole app**

Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
Expected: Build succeeded, 0 errors.

- [ ] **Step 2: Run the UI + localization test projects**

Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release`
Then: `dotnet test tests/ClaudeDo.Localization.Tests/ClaudeDo.Localization.Tests.csproj -c Release`
Expected: PASS (all green, including the 8 new batch-merge tests).

- [ ] **Step 3: Flag visual-verification gaps**

The cockpit toolbar/checkbox/conflicts-panel layout and the migrated `WorktreeModalView` diff rendering are AXAML changes that cannot be verified headlessly. Report to the user that these need a visual pass (run the app, open the worktrees overview, select several worktrees, pick a target, "Merge all", and open a worktree diff).

---

## Self-Review Notes

- **Spec coverage:** batch-merge cockpit (Tasks 1–4), skip-and-continue + conflict collection (Task 2), single target picker (Tasks 3–4), Resolve → `RequestConflictResolution(taskId, targetBranch)` seam left unwired (Tasks 3–4), `WorktreeModalView` diff migration to `DiffLinesView` (Task 6), no worker files touched, no `IWorkerClient` change, locales in parity (Task 5). ✔
- **No ConflictResolver reference:** the seam is a bare `Func<string,string,Task>?`; no Layer C type is named. ✔
- **Type consistency:** `BatchMergeOutcome`, `MergeOutcome`, `IsConflict`, `HasOutcome`, `MergeSelectedAsync`, `ConflictRows`, `SelectedTarget`, `SelectedCount`, `IsMerging`, `BatchProgress`, `RequestConflictResolution`, `MergeAllCommand`, `ResolveConflictCommand`, `ToggleSelectAllCommand`, `AddRowForTest`, `AllRows` are used consistently across tasks. ✔
