Compare commits
5 Commits
07a9d07cf6
...
2dfc4559b1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2dfc4559b1 | ||
|
|
dd3b03b9e4 | ||
|
|
f4416ee1c3 | ||
|
|
42bb79e2b7 | ||
|
|
561028e67b |
@@ -0,0 +1,432 @@
|
|||||||
|
# Git Merge/Review — Shared Foundation + Layer A 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:** Build the shared worker conflict contract (so parallel Layer B/C sessions branch from frozen interfaces) and rework the Git tab into a single Approve+merge cockpit.
|
||||||
|
|
||||||
|
**Architecture:** Phase 0 adds the conflict-resolution contract to `IWorkerClient`/`WorkerClient` (real `_hub.InvokeAsync` bodies — the worker hub methods are implemented later by Layer C; calls simply fail at runtime until then) plus client-side DTOs and test-fake updates, then commits + pushes so B and C branch from it. Phase A reworks `WorkConsole.axaml`'s Git tab and routes single-task merge/approve conflicts into a `RequestConflictResolution` seam (wired to Layer C's resolver by the integrator at merge time).
|
||||||
|
|
||||||
|
**Tech Stack:** .NET 8, Avalonia 12 (Fluent), CommunityToolkit.Mvvm, SignalR, xUnit. Build individual csproj with `-c Release` (`.slnx` needs .NET 9; a running Worker locks `Debug`).
|
||||||
|
|
||||||
|
**Reference spec:** `docs/superpowers/specs/2026-06-05-git-merge-review-rework-design.md`
|
||||||
|
|
||||||
|
**Note on the canonical diff renderer:** the unified diff model/control already exists — `DiffFileViewModel`/`DiffLineViewModel`/`UnifiedDiffParser` (in `src/ClaudeDo.Ui/ViewModels/Modals/`) rendered by `DiffLinesView` (`src/ClaudeDo.Ui/Views/Controls/DiffLinesView.axaml`). `DiffModalView` and `PlanningDiffView` already use it. So "consolidate diff renderers" for this scope is just verifying that (Task A.3); migrating `WorktreeModalView`'s bespoke diff onto `DiffLinesView` is Layer B's job.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## File Structure
|
||||||
|
|
||||||
|
**Phase 0 (foundation — pushed before B/C branch):**
|
||||||
|
- Modify `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs` — 5 new method signatures.
|
||||||
|
- Modify `src/ClaudeDo.Ui/Services/WorkerClient.cs` — 5 `InvokeAsync` bodies + 3 new DTO records.
|
||||||
|
- Modify `tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs` — 5 new `virtual` no-op methods.
|
||||||
|
- Modify `tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs` — 5 new methods on `FakeWorkerClient`.
|
||||||
|
|
||||||
|
**Phase A (Layer A — this session, after foundation commit):**
|
||||||
|
- Modify `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` — `RequestConflictResolution` seam; route Approve/Merge conflicts into it.
|
||||||
|
- Modify `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml` — fuse REVIEW + MERGE sections into one cockpit block.
|
||||||
|
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPlanningTests.cs` (or a sibling test file in the same folder).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 0 — Shared Foundation
|
||||||
|
|
||||||
|
### Task 0.1: Add the conflict contract (interface + client + DTOs)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs`
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add the 5 method signatures to `IWorkerClient`**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs`, after the existing
|
||||||
|
`Task CancelReviewAsync(string taskId);` line (line 45), add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// ── Conflict resolution (worker hub side implemented by Layer C) ──
|
||||||
|
Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch);
|
||||||
|
Task<MergeConflictsDto> GetMergeConflictsAsync(string taskId);
|
||||||
|
Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent);
|
||||||
|
Task<MergeResultDto> ContinueMergeAsync(string taskId);
|
||||||
|
Task AbortMergeAsync(string taskId);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add the 3 DTO records to `WorkerClient.cs`**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Ui/Services/WorkerClient.cs`, immediately after line 534
|
||||||
|
(`public record MergeTargetsDto(...)`), add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record MergeConflictsDto(string TaskId, IReadOnlyList<ConflictFileDto> Files);
|
||||||
|
public record ConflictFileDto(string Path, IReadOnlyList<ConflictHunkDto> Hunks);
|
||||||
|
public record ConflictHunkDto(string Ours, string Theirs, string? Base);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the 5 client method bodies to `WorkerClient.cs`**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Ui/Services/WorkerClient.cs`, right after the `MergeTaskAsync`
|
||||||
|
method (ends at line 270), add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch)
|
||||||
|
=> _hub.InvokeAsync<MergeResultDto>("StartConflictMerge", taskId, targetBranch);
|
||||||
|
|
||||||
|
public Task<MergeConflictsDto> GetMergeConflictsAsync(string taskId)
|
||||||
|
=> _hub.InvokeAsync<MergeConflictsDto>("GetMergeConflicts", taskId);
|
||||||
|
|
||||||
|
public Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent)
|
||||||
|
=> _hub.InvokeAsync("WriteConflictResolution", taskId, path, resolvedContent);
|
||||||
|
|
||||||
|
public Task<MergeResultDto> ContinueMergeAsync(string taskId)
|
||||||
|
=> _hub.InvokeAsync<MergeResultDto>("ContinueMerge", taskId);
|
||||||
|
|
||||||
|
public Task AbortMergeAsync(string taskId)
|
||||||
|
=> _hub.InvokeAsync("AbortMerge", taskId);
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Build the UI project**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.Ui/ClaudeDo.Ui.csproj -c Release`
|
||||||
|
Expected: build FAILS — the two test projects won't compile yet, but the UI project
|
||||||
|
itself should succeed. If the UI project reports "does not implement interface member"
|
||||||
|
it means a body is missing; fix before continuing. (Test projects are fixed in 0.2.)
|
||||||
|
|
||||||
|
### Task 0.2: Update the hand-rolled test fakes
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs`
|
||||||
|
- Modify: `tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Add 5 virtual no-ops to `StubWorkerClient`**
|
||||||
|
|
||||||
|
In `tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs`, after the `MergeTaskAsync` override
|
||||||
|
(line 57), add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public virtual Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch) => Task.FromResult(new MergeResultDto("conflict", System.Array.Empty<string>(), null));
|
||||||
|
public virtual Task<MergeConflictsDto> GetMergeConflictsAsync(string taskId) => Task.FromResult(new MergeConflictsDto(taskId, System.Array.Empty<ConflictFileDto>()));
|
||||||
|
public virtual Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent) => Task.CompletedTask;
|
||||||
|
public virtual Task<MergeResultDto> ContinueMergeAsync(string taskId) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty<string>(), null));
|
||||||
|
public virtual Task AbortMergeAsync(string taskId) => Task.CompletedTask;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Add 5 methods to `FakeWorkerClient`**
|
||||||
|
|
||||||
|
In `tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs`, after the
|
||||||
|
`MergeTaskAsync` method (line 47), add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch) => Task.FromResult(new MergeResultDto("conflict", System.Array.Empty<string>(), null));
|
||||||
|
public Task<MergeConflictsDto> GetMergeConflictsAsync(string taskId) => Task.FromResult(new MergeConflictsDto(taskId, System.Array.Empty<ConflictFileDto>()));
|
||||||
|
public Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent) => Task.CompletedTask;
|
||||||
|
public Task<MergeResultDto> ContinueMergeAsync(string taskId) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty<string>(), null));
|
||||||
|
public Task AbortMergeAsync(string taskId) => Task.CompletedTask;
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 3: Build both test projects**
|
||||||
|
|
||||||
|
Run: `dotnet build tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release && dotnet build tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj -c Release`
|
||||||
|
Expected: both BUILD succeed.
|
||||||
|
|
||||||
|
- [ ] **Step 4: Run the UI test suite to confirm green baseline**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release`
|
||||||
|
Expected: PASS (no behavior changed yet).
|
||||||
|
|
||||||
|
### Task 0.3: Commit and push the foundation
|
||||||
|
|
||||||
|
- [ ] **Step 1: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs src/ClaudeDo.Ui/Services/WorkerClient.cs tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs
|
||||||
|
git commit -m "feat(ui): add conflict-resolution worker contract (foundation for merge rework)"
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Push so Layer B/C can branch from this commit**
|
||||||
|
|
||||||
|
Run: `git push`
|
||||||
|
Expected: pushed to `main`. (First push to git.kuns.dev may fail auth — retry once.)
|
||||||
|
**This commit is the branch point for the Layer B and Layer C kickoff prompts.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase A — Layer A Review/Merge Cockpit
|
||||||
|
|
||||||
|
### Task A.1: Conflict-resolution seam + route Approve/Merge conflicts into it (TDD)
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs`
|
||||||
|
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandConflictSeamTests.cs` (new)
|
||||||
|
|
||||||
|
- [ ] **Step 1: Write the failing test**
|
||||||
|
|
||||||
|
Create `tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandConflictSeamTests.cs`. Mirror
|
||||||
|
the VM-construction harness used in
|
||||||
|
`tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPlanningTests.cs` (same folder) —
|
||||||
|
construct `DetailsIslandViewModel` exactly as that file does, including its
|
||||||
|
`StubWorkerClient` subclass pattern. The test:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
[Fact]
|
||||||
|
public async Task ApproveReview_OnConflict_InvokesConflictResolutionSeam()
|
||||||
|
{
|
||||||
|
string? resolvedTaskId = null;
|
||||||
|
string? resolvedTarget = null;
|
||||||
|
|
||||||
|
// Construct the VM as in DetailsIslandPlanningTests, with a worker stub whose
|
||||||
|
// ApproveReviewAsync returns a conflict result:
|
||||||
|
// public override Task<MergeResultDto?> ApproveReviewAsync(string id, string target)
|
||||||
|
// => Task.FromResult<MergeResultDto?>(new MergeResultDto("conflict", new[]{"a.cs"}, null));
|
||||||
|
var vm = CreateVm(/* worker stub above */);
|
||||||
|
vm.RequestConflictResolution = (taskId, target) =>
|
||||||
|
{
|
||||||
|
resolvedTaskId = taskId; resolvedTarget = target;
|
||||||
|
return System.Threading.Tasks.Task.CompletedTask;
|
||||||
|
};
|
||||||
|
// assign a task in WaitingForReview + a SelectedMergeTarget = "main" via the same
|
||||||
|
// helpers DetailsIslandPlanningTests uses.
|
||||||
|
|
||||||
|
await vm.ApproveReviewCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
Assert.Equal(/* the seeded task id */, resolvedTaskId);
|
||||||
|
Assert.Equal("main", resolvedTarget);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 2: Run the test to verify it fails**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter ApproveReview_OnConflict_InvokesConflictResolutionSeam`
|
||||||
|
Expected: FAIL — `RequestConflictResolution` property does not exist (compile error).
|
||||||
|
|
||||||
|
- [ ] **Step 3: Add the seam property**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs`, beside the other
|
||||||
|
view-wired delegates (`ShowDiffModal`, `ShowMergeModal` around line 387–390), add:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// Invoked when a single-task merge/approve hits a conflict. Wired by the
|
||||||
|
// integrator to Layer C's conflict resolver. Args: (taskId, targetBranch).
|
||||||
|
public Func<string, string, System.Threading.Tasks.Task>? RequestConflictResolution { get; set; }
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 4: Route the Approve conflict branch into the seam**
|
||||||
|
|
||||||
|
In `ApproveReviewAsync` (around line 1453), replace the conflict branch body so it
|
||||||
|
prefers the seam, falling back to the current preview-text behavior:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = await _worker.ApproveReviewAsync(Task.Id, SelectedMergeTarget ?? "");
|
||||||
|
if (result?.Status == "conflict")
|
||||||
|
{
|
||||||
|
if (RequestConflictResolution is not null)
|
||||||
|
{
|
||||||
|
await RequestConflictResolution(Task.Id, SelectedMergeTarget ?? "");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var (text, _, _) = MergePreviewPresenter.Describe(
|
||||||
|
new MergePreviewDto("conflict", result.ConflictFiles, 0));
|
||||||
|
MergePreviewText = text; MergeIsClean = false; MergeIsConflict = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 5: Route the manual Merge conflict branch into the seam**
|
||||||
|
|
||||||
|
In `MergeAsync` (around line 1170), apply the same pattern to its conflict branch:
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
var result = await _worker.MergeTaskAsync(Task.Id, SelectedMergeTarget ?? "", false, "Merge task");
|
||||||
|
if (result.Status == "conflict")
|
||||||
|
{
|
||||||
|
if (RequestConflictResolution is not null)
|
||||||
|
{
|
||||||
|
await RequestConflictResolution(Task.Id, SelectedMergeTarget ?? "");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var (text, _, _) = MergePreviewPresenter.Describe(
|
||||||
|
new MergePreviewDto("conflict", result.ConflictFiles, 0));
|
||||||
|
MergePreviewText = text; MergeIsClean = false; MergeIsConflict = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
await RefreshMergePreviewAsync();
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
- [ ] **Step 6: Run the test to verify it passes**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter ApproveReview_OnConflict_InvokesConflictResolutionSeam`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 7: Run the full UI suite (no regressions)**
|
||||||
|
|
||||||
|
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release`
|
||||||
|
Expected: PASS.
|
||||||
|
|
||||||
|
- [ ] **Step 8: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandConflictSeamTests.cs
|
||||||
|
git commit -m "feat(ui): route single-task merge conflicts into a resolution seam"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task A.2: Fuse the Git tab into one Approve+merge cockpit
|
||||||
|
|
||||||
|
**Files:**
|
||||||
|
- Modify: `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml`
|
||||||
|
|
||||||
|
- [ ] **Step 1: Replace the two Git-tab sections with one cockpit block**
|
||||||
|
|
||||||
|
In `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml`, replace the entire Git
|
||||||
|
`ScrollViewer` body (lines 255–313 — the `<!-- Git: ... -->` block containing the
|
||||||
|
separate `REVIEW` `StackPanel` and the `MERGE & WORKTREE` `StackPanel`) with a single
|
||||||
|
cockpit where Approve sits with the merge target/preview/actions. Keep the existing
|
||||||
|
control class names (`section-label`, `field-label`, `btn`, `btn accent`, `meta`) and
|
||||||
|
the existing bindings (`SelectedMergeTarget`, `MergeTargetBranches`, `MergePreviewText`,
|
||||||
|
`MergeIsClean`, `MergeIsConflict`, `ShowMergePreviewMuted`, `OpenDiffCommand`,
|
||||||
|
`ApproveReviewCommand`, `MergeCommand`, `ShowSingleMerge`, `OpenWorktreeCommand`,
|
||||||
|
`ReviewCombinedDiffCommand`, `MergeAllCommand`, `CanMergeAll`, `MergeAllDisabledReason`,
|
||||||
|
`MergeAllError`):
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<!-- Git: one Approve + merge cockpit -->
|
||||||
|
<ScrollViewer IsVisible="{Binding IsGitTab}" Padding="14,10">
|
||||||
|
<StackPanel Spacing="12" IsVisible="{Binding ShowMergeSection}">
|
||||||
|
<TextBlock Classes="section-label" Text="MERGE" />
|
||||||
|
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Classes="field-label" Text="Target branch" />
|
||||||
|
<ComboBox ItemsSource="{Binding MergeTargetBranches}"
|
||||||
|
SelectedItem="{Binding SelectedMergeTarget, Mode=TwoWay}"
|
||||||
|
HorizontalAlignment="Stretch" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<StackPanel Spacing="0">
|
||||||
|
<TextBlock Classes="meta" Text="{Binding MergePreviewText}" TextWrapping="Wrap"
|
||||||
|
Foreground="{DynamicResource MossBrush}"
|
||||||
|
IsVisible="{Binding MergeIsClean}" />
|
||||||
|
<TextBlock Classes="meta" Text="{Binding MergePreviewText}" TextWrapping="Wrap"
|
||||||
|
Foreground="{DynamicResource BloodBrush}"
|
||||||
|
IsVisible="{Binding MergeIsConflict}" />
|
||||||
|
<TextBlock Classes="meta" Text="{Binding MergePreviewText}" TextWrapping="Wrap"
|
||||||
|
Foreground="{DynamicResource TextMuteBrush}"
|
||||||
|
IsVisible="{Binding ShowMergePreviewMuted}" />
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- Primary action: Approve flows straight into the merge.
|
||||||
|
Approve is the review-gated path; the plain Merge button covers
|
||||||
|
already-reviewed / kept worktrees. -->
|
||||||
|
<WrapPanel Orientation="Horizontal">
|
||||||
|
<Button Classes="btn accent" Content="Approve & Merge" Margin="0,0,8,8"
|
||||||
|
Command="{Binding ApproveReviewCommand}"
|
||||||
|
IsVisible="{Binding IsWaitingForReview}" />
|
||||||
|
<Button Classes="btn accent" Content="Merge" Margin="0,0,8,8"
|
||||||
|
Command="{Binding MergeCommand}"
|
||||||
|
IsVisible="{Binding ShowSingleMerge}" />
|
||||||
|
<Button Classes="btn" Content="Open Diff" Margin="0,0,8,8"
|
||||||
|
Command="{Binding OpenDiffCommand}" />
|
||||||
|
<Button Classes="btn" Margin="0,0,8,8"
|
||||||
|
Command="{Binding OpenWorktreeCommand}">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||||
|
<TextBlock Text="Worktree" />
|
||||||
|
<PathIcon Data="{StaticResource Icon.ArrowOut}" Width="11" Height="11" />
|
||||||
|
</StackPanel>
|
||||||
|
</Button>
|
||||||
|
<Button Classes="btn" Content="Review Combined Diff" Margin="0,0,8,8"
|
||||||
|
Command="{Binding ReviewCombinedDiffCommand}" />
|
||||||
|
<Button Classes="btn accent" Content="Merge All Subtasks" Margin="0,0,0,8"
|
||||||
|
Command="{Binding MergeAllCommand}"
|
||||||
|
IsEnabled="{Binding CanMergeAll}"
|
||||||
|
ToolTip.Tip="{Binding MergeAllDisabledReason}" />
|
||||||
|
</WrapPanel>
|
||||||
|
|
||||||
|
<TextBlock Text="{Binding MergeAllError}"
|
||||||
|
Foreground="{DynamicResource BloodBrush}"
|
||||||
|
TextWrapping="Wrap"
|
||||||
|
IsVisible="{Binding MergeAllError,
|
||||||
|
Converter={x:Static ObjectConverters.IsNotNull}}" />
|
||||||
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
|
```
|
||||||
|
|
||||||
|
Note: the cockpit now shows whenever `ShowMergeSection` is true. `ShowMergeSection`
|
||||||
|
(DetailsIslandViewModel line 161) must be true while `IsWaitingForReview` so the
|
||||||
|
Approve button appears. Check its expression in Step 2.
|
||||||
|
|
||||||
|
- [ ] **Step 2: Verify `ShowMergeSection` covers the review state**
|
||||||
|
|
||||||
|
Read `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` line 161. If
|
||||||
|
`ShowMergeSection` is false while `IsWaitingForReview` (e.g. it requires a non-review
|
||||||
|
state), widen it to also be true when `IsWaitingForReview && WorktreePath != null`, and
|
||||||
|
ensure `OnPropertyChanged(nameof(ShowMergeSection))` already fires on the relevant state
|
||||||
|
transitions (it is notified via `NotifySessionSections`). Make the minimal change needed
|
||||||
|
so the Approve button is visible in review state. If it already covers review, change
|
||||||
|
nothing.
|
||||||
|
|
||||||
|
- [ ] **Step 3: Build the app project**
|
||||||
|
|
||||||
|
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
|
||||||
|
Expected: BUILD succeeds (pulls in Ui + Data).
|
||||||
|
|
||||||
|
- [ ] **Step 4: Visual verification (manual — flag for the user)**
|
||||||
|
|
||||||
|
This is an AXAML layout change with no automated coverage. Launch the app, open a task
|
||||||
|
in `WaitingForReview`, open the Git tab, and confirm: the single MERGE block shows the
|
||||||
|
target combo, the colored preview line, an "Approve & Merge" button (review state), and
|
||||||
|
the diff/worktree/combined/merge-all actions. **Explicitly tell the user this needs a
|
||||||
|
visual pass — do not claim it works without running it.**
|
||||||
|
|
||||||
|
- [ ] **Step 5: Commit**
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git add src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs
|
||||||
|
git commit -m "feat(ui): fuse git tab into one approve+merge cockpit"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Task A.3: Verify diff-renderer consolidation
|
||||||
|
|
||||||
|
**Files:** none modified (verification only).
|
||||||
|
|
||||||
|
- [ ] **Step 1: Confirm DiffModal + Planning already use the canonical renderer**
|
||||||
|
|
||||||
|
Run: `rg -l "DiffLinesView" src/ClaudeDo.Ui/Views`
|
||||||
|
Expected: matches in `Modals/DiffModalView.axaml` and `Planning/PlanningDiffView.axaml`.
|
||||||
|
If `PlanningDiffView.axaml` does NOT use `DiffLinesView`, change its diff `ItemsControl`
|
||||||
|
to a `<controls:DiffLinesView Lines="{Binding SelectedFile.Lines}" />` (matching
|
||||||
|
`DiffModalView.axaml`'s usage) and rebuild the App project. If both already use it, this
|
||||||
|
task is a no-op — record that and move on. (`WorktreeModalView`'s bespoke diff is
|
||||||
|
intentionally left for Layer B.)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Self-Review
|
||||||
|
|
||||||
|
- **Spec coverage:** Foundation contract (spec §"Frozen worker conflict contract") →
|
||||||
|
Task 0.1. Test fakes (spec parallel-boundaries row) → Task 0.2. Branch point (spec
|
||||||
|
§"built & pushed this session") → Task 0.3. Layer A cockpit + Approve/merge flow
|
||||||
|
together (spec §"Layer A") → Task A.2. Single-task approve-on-conflict opens resolver
|
||||||
|
via seam (spec §"Layer A" + §"integration seams") → Task A.1. Diff consolidation
|
||||||
|
(spec §"One diff model") → Task A.3. Output-footer feedback unchanged → not touched
|
||||||
|
(correct). No spec requirement left unmapped for this session's scope.
|
||||||
|
- **Placeholder scan:** none — every code step has concrete code; the only "mirror the
|
||||||
|
existing harness" reference (Task A.1 Step 1) points at a real file with a working
|
||||||
|
pattern, not a TODO.
|
||||||
|
- **Type consistency:** `MergeConflictsDto`/`ConflictFileDto`/`ConflictHunkDto` and the
|
||||||
|
5 method names match between `IWorkerClient` (0.1 Step 1), `WorkerClient` (0.1 Steps
|
||||||
|
2–3), and both fakes (0.2). The seam `RequestConflictResolution` is
|
||||||
|
`Func<string,string,Task>?` everywhere (A.1 Steps 1, 3–5). DTO field names match the
|
||||||
|
spec.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Integration notes (for the integrator merging A + B + C)
|
||||||
|
|
||||||
|
- Wire `DetailsIslandViewModel.RequestConflictResolution` and Layer B's equivalent
|
||||||
|
callback to Layer C's `ConflictResolverViewModel` factory + `ShowConflictResolver`
|
||||||
|
dialog delegate.
|
||||||
|
- Layer C implements the worker hub methods `StartConflictMerge`, `GetMergeConflicts`,
|
||||||
|
`WriteConflictResolution`, `ContinueMerge`, `AbortMerge`; the client side from Task
|
||||||
|
0.1 already calls them by name.
|
||||||
139
docs/superpowers/plans/2026-06-05-git-merge-review-prompts.md
Normal file
139
docs/superpowers/plans/2026-06-05-git-merge-review-prompts.md
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
# Git Merge/Review Rework — Parallel Kickoff Prompts (Layer B & Layer C)
|
||||||
|
|
||||||
|
These are self-contained prompts to paste into two fresh ClaudeDo sessions, each in its
|
||||||
|
own git worktree, run **in parallel** with the main session's Layer A work.
|
||||||
|
|
||||||
|
**Prerequisite — branch point:** Both sessions must branch from `main` **at or after**
|
||||||
|
the foundation commit `feat(ui): add conflict-resolution worker contract (foundation for
|
||||||
|
merge rework)` (Phase 0, Task 0.3 of
|
||||||
|
`docs/superpowers/plans/2026-06-05-git-merge-review-foundation-layerA.md`). That commit
|
||||||
|
adds the frozen `IWorkerClient` conflict contract both layers rely on. Do not start B/C
|
||||||
|
until that commit is pushed.
|
||||||
|
|
||||||
|
**Integration:** Neither session pushes to `main` or merges. Each leaves its branch/
|
||||||
|
worktree for the orchestrator (the main session) to review and merge.
|
||||||
|
|
||||||
|
Design reference for both: `docs/superpowers/specs/2026-06-05-git-merge-review-rework-design.md`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Layer B — Multi-worktree merge cockpit
|
||||||
|
|
||||||
|
```
|
||||||
|
We're reworking ClaudeDo's merge/review UX. Your job is Layer B: a multi-worktree merge
|
||||||
|
cockpit. The overall design is in docs/superpowers/specs/2026-06-05-git-merge-review-rework-design.md
|
||||||
|
(read the "Layer B" section and "Parallel boundaries" table first). A shared foundation
|
||||||
|
commit ("add conflict-resolution worker contract") is already on main — branch from it.
|
||||||
|
|
||||||
|
First, create an isolated worktree for this work (use the superpowers:using-git-worktrees
|
||||||
|
skill). Then write a plan (superpowers:writing-plans) for just Layer B and implement it
|
||||||
|
with superpowers:subagent-driven-development (sonnet subagents, TDD, commit per task).
|
||||||
|
|
||||||
|
Scope:
|
||||||
|
- Rework WorktreesOverviewModalView + WorktreesOverviewModalViewModel into a batch-merge
|
||||||
|
cockpit: list mergeable worktrees, multi-select N, pick ONE target branch, "Merge all".
|
||||||
|
- Skip-and-continue: loop the EXISTING IWorkerClient.MergeTaskAsync(taskId, target,
|
||||||
|
removeWorktree:false, msg) over the selected tasks. Clean ones merge; conflicting ones
|
||||||
|
(MergeTaskAsync returns Status=="conflict", auto-aborts leaving the tree clean) are
|
||||||
|
collected into a "needs resolution" list shown with live progress.
|
||||||
|
- Each conflict row gets a "Resolve" button that invokes a seam:
|
||||||
|
public Func<string, string, Task>? RequestConflictResolution { get; set; } // (taskId, targetBranch)
|
||||||
|
Define this callback property on the cockpit VM; leave it unwired (the orchestrator
|
||||||
|
wires it to Layer C's resolver at merge time). Do NOT reference any ConflictResolver
|
||||||
|
type.
|
||||||
|
- Migrate WorktreeModalView's bespoke inline diff onto the canonical DiffLinesView
|
||||||
|
control (src/ClaudeDo.Ui/Views/Controls/DiffLinesView.axaml) using DiffFileViewModel/
|
||||||
|
DiffLineViewModel/UnifiedDiffParser (src/ClaudeDo.Ui/ViewModels/Modals/). This removes
|
||||||
|
the last duplicate diff renderer.
|
||||||
|
|
||||||
|
Reuse these existing IWorkerClient methods (already implemented): MergeTaskAsync,
|
||||||
|
GetMergeTargetsAsync, GetWorktreesOverviewAsync, SetWorktreeStateAsync,
|
||||||
|
CleanupFinishedWorktreesAsync, ForceRemoveWorktreeAsync.
|
||||||
|
|
||||||
|
Do NOT touch (other layers own them): any worker-side files (WorkerHub, TaskMergeService,
|
||||||
|
GitService), IWorkerClient.cs / WorkerClient.cs, WorkConsole.axaml,
|
||||||
|
DetailsIslandViewModel.cs, or create the ConflictResolver UI.
|
||||||
|
|
||||||
|
Build with: dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release (a running
|
||||||
|
Worker locks Debug — use Release). Keep locales/en.json and de.json keys in parity if you
|
||||||
|
add any. If you change IWorkerClient (you shouldn't need to), update the hand-rolled fakes
|
||||||
|
in tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs and
|
||||||
|
tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs. No tests that spawn
|
||||||
|
the real claude CLI.
|
||||||
|
|
||||||
|
Commit per task with Conventional Commits. Do NOT push to main and do NOT merge — leave
|
||||||
|
your worktree/branch for the orchestrator. Flag any AXAML layout for visual verification
|
||||||
|
rather than claiming it works.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Layer C — Inline conflict resolver
|
||||||
|
|
||||||
|
```
|
||||||
|
We're reworking ClaudeDo's merge/review UX. Your job is Layer C: an in-app, VSCode-style
|
||||||
|
inline conflict resolver, plus the worker plumbing it needs. The overall design is in
|
||||||
|
docs/superpowers/specs/2026-06-05-git-merge-review-rework-design.md (read the "Layer C",
|
||||||
|
"Frozen worker conflict contract", and "Parallel boundaries" sections first). A shared
|
||||||
|
foundation commit ("add conflict-resolution worker contract") is already on main — branch
|
||||||
|
from it. That commit already wired the CLIENT side (IWorkerClient + WorkerClient call
|
||||||
|
these hub methods by name); your job includes implementing the matching WORKER hub methods.
|
||||||
|
|
||||||
|
First, create an isolated worktree (superpowers:using-git-worktrees). Then write a plan
|
||||||
|
(superpowers:writing-plans) for Layer C and implement it with
|
||||||
|
superpowers:subagent-driven-development (sonnet subagents, TDD, commit per task).
|
||||||
|
|
||||||
|
Worker side — implement these 5 hub methods in WorkerHub (names/params/returns MUST match
|
||||||
|
the client calls already shipped in the foundation):
|
||||||
|
- StartConflictMerge(string taskId, string targetBranch) -> MergeResultDto
|
||||||
|
Calls TaskMergeService.MergeAsync with leaveConflictsInTree:true (the overload/flag
|
||||||
|
already exists — used today by PlanningMergeOrchestrator). Leaves .git/MERGE_HEAD in
|
||||||
|
the list's WorkingDir, returns Status="conflict" + conflict file list.
|
||||||
|
- GetMergeConflicts(string taskId) -> MergeConflictsDto
|
||||||
|
For each conflicted file (git diff --name-only --diff-filter=U), read ours/theirs/base
|
||||||
|
via `git show :2:<path>` / `:3:<path>` / `:1:<path>`. Add GitService helpers as needed.
|
||||||
|
- WriteConflictResolution(string taskId, string path, string resolvedContent) -> void
|
||||||
|
Write resolvedContent to the file in WorkingDir and `git add` it.
|
||||||
|
- ContinueMerge(string taskId) -> MergeResultDto
|
||||||
|
Wrap the EXISTING TaskMergeService.ContinueMergeAsync (git add -A → re-check
|
||||||
|
diff --diff-filter=U → git commit). Currently service-level only; expose it on the hub.
|
||||||
|
- AbortMerge(string taskId) -> void
|
||||||
|
Wrap the EXISTING TaskMergeService.AbortMergeAsync (git merge --abort).
|
||||||
|
|
||||||
|
Define worker-side DTO records that serialize identically to the client records already in
|
||||||
|
WorkerClient.cs:
|
||||||
|
MergeConflictsDto(string TaskId, IReadOnlyList<ConflictFileDto> Files)
|
||||||
|
ConflictFileDto(string Path, IReadOnlyList<ConflictHunkDto> Hunks)
|
||||||
|
ConflictHunkDto(string Ours, string Theirs, string? Base)
|
||||||
|
(place beside the other hub DTOs in WorkerHub.cs). MergeResultDto already exists.
|
||||||
|
|
||||||
|
UI side — new files only:
|
||||||
|
- ConflictResolverViewModel + ConflictResolverView. On open: StartConflictMergeAsync then
|
||||||
|
GetMergeConflictsAsync(taskId). Per conflict hunk show ours vs theirs stacked with
|
||||||
|
buttons Accept Current / Accept Incoming / Accept Both / Edit manually, plus a free-text
|
||||||
|
box for the merged result of that hunk. Use the UI conflict model from the design
|
||||||
|
(ConflictFile { Path, Hunks[] }, ConflictHunk { Ours, Theirs, Base, Resolution }) —
|
||||||
|
shape it so a future 3-way pane needs no model change.
|
||||||
|
- When every file is resolved: WriteConflictResolutionAsync per file, then
|
||||||
|
ContinueMergeAsync(taskId) (Status "merged" closes; "conflict" means not fully resolved,
|
||||||
|
stay open). AbortMergeAsync(taskId) cancels.
|
||||||
|
- Expose a factory Func<string, ConflictResolverViewModel> and a
|
||||||
|
Func<ConflictResolverViewModel, Task> ShowConflictResolver dialog delegate for the
|
||||||
|
orchestrator to wire to Layer A/B's RequestConflictResolution(taskId, target) seams.
|
||||||
|
|
||||||
|
Do NOT touch (other layers own them): WorkerClient.cs, IWorkerClient.cs (already wired),
|
||||||
|
WorkConsole.axaml, DetailsIslandViewModel.cs, WorktreesOverviewModalView/VM. You WILL need
|
||||||
|
to add the 5 worker hub methods + GitService conflict reads.
|
||||||
|
|
||||||
|
Tests: add worker tests for the conflict reads / continue / abort using real SQLite + real
|
||||||
|
git (follow existing GitService/TaskMergeService test patterns). NEVER spawn the real
|
||||||
|
claude CLI. If you change IWorkerClient (you should NOT — client is frozen), update the
|
||||||
|
fakes in both test projects.
|
||||||
|
|
||||||
|
Build with: dotnet build src/ClaudeDo.Worker/ClaudeDo.Worker.csproj -c Release and
|
||||||
|
dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release (a running Worker locks
|
||||||
|
Debug). Keep locales/en.json and de.json in parity for any new UI strings.
|
||||||
|
|
||||||
|
Commit per task with Conventional Commits. Do NOT push to main and do NOT merge — leave
|
||||||
|
your worktree/branch for the orchestrator. Flag the resolver UI for visual verification.
|
||||||
|
```
|
||||||
@@ -0,0 +1,197 @@
|
|||||||
|
# Git Tab / Merge & Review Rework — Design
|
||||||
|
|
||||||
|
Date: 2026-06-05
|
||||||
|
Status: Approved
|
||||||
|
|
||||||
|
## Goal
|
||||||
|
|
||||||
|
Make handling merges and reviews as simple as possible in the Terminal component's
|
||||||
|
Git tab, and rework the diff viewers and worktree modals along the way. The work is
|
||||||
|
split into three layers built across separate sessions, with a shared foundation that
|
||||||
|
is built and pushed first so the parallel sessions branch from frozen contracts.
|
||||||
|
|
||||||
|
The user mostly trusts task output but wants the diff one click away for important
|
||||||
|
work, and wants to land several independently-queued worktrees without per-task
|
||||||
|
hopping or hand-resolving conflicts in an external editor.
|
||||||
|
|
||||||
|
## Layers
|
||||||
|
|
||||||
|
- **Layer A — Review/merge cockpit** (this session). Single-task review + merge UX in
|
||||||
|
the Git tab; consolidate the four diff renderers into one `DiffView`.
|
||||||
|
- **Layer B — Multi-worktree merge cockpit** (parallel session). Batch-merge N
|
||||||
|
worktrees into one target, skip-and-continue, conflicts collected for resolution.
|
||||||
|
- **Layer C — Inline conflict resolver** (parallel session). VSCode-style inline hunk
|
||||||
|
resolver plus the worker-side conflict plumbing it needs.
|
||||||
|
|
||||||
|
They stack: A defines the single-task flow, B reuses it for many tasks, both funnel
|
||||||
|
conflicts into C.
|
||||||
|
|
||||||
|
## Shared foundation (built & pushed this session, before B/C branch)
|
||||||
|
|
||||||
|
Everything B and C depend on lands first on `main`. B and C branch from that commit.
|
||||||
|
|
||||||
|
### 1. One diff model + one `DiffView` control
|
||||||
|
|
||||||
|
Today there are four diff renderers and two parallel diff models:
|
||||||
|
|
||||||
|
- `DiffLinesView.axaml` (used by `DiffModalView`)
|
||||||
|
- the inline diff `ItemsControl` in `WorktreeModalView.axaml`
|
||||||
|
- `PlanningDiffView.axaml`
|
||||||
|
- their backing models: `DiffFileViewModel`/`DiffLineViewModel` (+ `UnifiedDiffParser`)
|
||||||
|
vs `WorktreeNodeViewModel`/`WorktreeDiffLineViewModel`
|
||||||
|
|
||||||
|
Collapse into a single canonical diff model + parser + a `DiffView` UserControl. All
|
||||||
|
diff rendering across the app goes through `DiffView`.
|
||||||
|
|
||||||
|
- Model: `DiffFileViewModel { Path, AddCount, DelCount, Lines }`,
|
||||||
|
`DiffLineViewModel { OldNo, NewNo, Kind (Add|Del|Ctx|File|Hunk), Text }`.
|
||||||
|
- Parser: one static `UnifiedDiffParser.Parse(rawUnifiedDiff)` returning the model.
|
||||||
|
- `DiffView` exposes a `Files` styled property (file list + selected-file lines), or a
|
||||||
|
simpler `Lines` property for single-file use — Layer A decides the exact surface
|
||||||
|
while building it, but the type names above are frozen so B and C can bind to them.
|
||||||
|
|
||||||
|
### 2. Frozen worker conflict contract
|
||||||
|
|
||||||
|
Added to `IWorkerClient` (and `WorkerClient` with stub bodies that throw
|
||||||
|
`NotSupportedException`) plus new DTOs, so A and B compile against the interface while
|
||||||
|
C provides the real worker-side implementation.
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
// IWorkerClient additions (signatures frozen this session)
|
||||||
|
Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch);
|
||||||
|
Task<MergeConflictsDto> GetMergeConflictsAsync(string taskId);
|
||||||
|
Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent);
|
||||||
|
Task<MergeResultDto> ContinueMergeAsync(string taskId);
|
||||||
|
Task AbortMergeAsync(string taskId);
|
||||||
|
```
|
||||||
|
|
||||||
|
- `StartConflictMergeAsync` performs the merge with `leaveConflictsInTree: true` (the
|
||||||
|
worker already supports this flag — used today by the planning orchestrator) and
|
||||||
|
returns `MergeResultDto` with `Status="conflict"` and the conflict file list, leaving
|
||||||
|
`.git/MERGE_HEAD` in place in the list's `WorkingDir`.
|
||||||
|
- `GetMergeConflictsAsync` returns each conflicted file with ours/theirs/base content,
|
||||||
|
read via `git show :2:<path>` (ours), `:3:<path>` (theirs), `:1:<path>` (base).
|
||||||
|
- `WriteConflictResolutionAsync` writes resolved content to the file in `WorkingDir`
|
||||||
|
and `git add`s it.
|
||||||
|
- `ContinueMergeAsync` wraps the existing `TaskMergeService.ContinueMergeAsync`
|
||||||
|
(`git add -A` → re-check `git diff --name-only --diff-filter=U` → `git commit`).
|
||||||
|
- `AbortMergeAsync` wraps the existing `TaskMergeService.AbortMergeAsync`
|
||||||
|
(`git merge --abort`).
|
||||||
|
|
||||||
|
New DTOs (defined in the worker hub DTO file, mirrored client-side):
|
||||||
|
|
||||||
|
```csharp
|
||||||
|
public record MergeConflictsDto(string TaskId, IReadOnlyList<ConflictFileDto> Files);
|
||||||
|
public record ConflictFileDto(string Path, IReadOnlyList<ConflictHunkDto> Hunks);
|
||||||
|
public record ConflictHunkDto(string Ours, string Theirs, string? Base);
|
||||||
|
```
|
||||||
|
|
||||||
|
Existing DTOs reused unchanged: `MergeResultDto(Status, ConflictFiles, ErrorMessage)`,
|
||||||
|
`MergePreviewDto`, `MergeTargetsDto`.
|
||||||
|
|
||||||
|
### 3. Conflict data model (UI)
|
||||||
|
|
||||||
|
`ConflictFile { Path, Hunks[] }`, `ConflictHunk { Ours, Theirs, Base, Resolution }`.
|
||||||
|
Shaped so a future 3-way merge pane needs no model change (Layer C is the inline
|
||||||
|
resolver now; the model leaves room for 3-way later).
|
||||||
|
|
||||||
|
### 4. Integration seams (delegates, wired by the integrator at merge)
|
||||||
|
|
||||||
|
A's and B's cockpits hold a `RequestConflictResolution(string taskId)` callback (an
|
||||||
|
`Action<string>` or `Func<string, Task>`). They never reference Layer C's resolver
|
||||||
|
types. The integrator connects these callbacks to C's `ConflictResolverViewModel`
|
||||||
|
factory when merging the three branches together.
|
||||||
|
|
||||||
|
## Parallel boundaries (verified disjoint)
|
||||||
|
|
||||||
|
| Area | A (this session) | B (parallel) | C (parallel) |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `DiffView` + diff model/parser | builds | reuses | reuses |
|
||||||
|
| `WorkConsole.axaml` / `DetailsIslandViewModel` | owns | — | — |
|
||||||
|
| `DiffModalView` + `PlanningDiffView` | migrates to `DiffView` | — | — |
|
||||||
|
| `WorktreesOverviewModalView/VM` + `WorktreeModalView` | — | owns | — |
|
||||||
|
| `WorkerHub` / `TaskMergeService` / `GitService` | — | — | owns |
|
||||||
|
| New `ConflictResolverView/VM` + conflict UI model | — | — | owns |
|
||||||
|
| `IWorkerClient` / `WorkerClient` | adds frozen stubs + DTOs | reuses `MergeTaskAsync` | fills stub bodies |
|
||||||
|
| Test fakes (`IWorkerClient`) in both test projects | adds new no-op methods | — | makes them functional if needed |
|
||||||
|
|
||||||
|
The only file C and A both touch is `WorkerClient.cs` (C replaces the stub bodies A
|
||||||
|
wrote). Contained; reconciled at integration. Everything else is disjoint.
|
||||||
|
|
||||||
|
## Layer A — review/merge cockpit (this session)
|
||||||
|
|
||||||
|
- The Git tab becomes the single Approve + merge surface. `Approve` and the merge
|
||||||
|
target / preview / diff flow together as one block (no separate REVIEW vs
|
||||||
|
MERGE & WORKTREE sections).
|
||||||
|
- `Continue` (reject → requeue with feedback) and `Reset` (reject → idle) **stay** in
|
||||||
|
the Output tab footer — unchanged.
|
||||||
|
- The diff is shown via the unified `DiffView` opened as a modal from the cockpit. No
|
||||||
|
inline diff recap in the tab (the island is too small).
|
||||||
|
- On a single-task **Approve that conflicts**: instead of today's auto-abort, call
|
||||||
|
`StartConflictMergeAsync` and fire `RequestConflictResolution(taskId)`. This leaves
|
||||||
|
the main checkout mid-merge until the user resolves or aborts (behavior change,
|
||||||
|
intended). The callback is inert until Layer C is merged; the integrator wires it.
|
||||||
|
- Migrate `DiffModalView` and `PlanningDiffView` onto the new `DiffView`.
|
||||||
|
|
||||||
|
### Behavior change accepted
|
||||||
|
|
||||||
|
Today `MergeTask`/`ApproveReview` use `leaveConflictsInTree: false` (auto-abort on
|
||||||
|
conflict). Under this design, an Approve that conflicts leaves the merge in progress
|
||||||
|
and opens the resolver. The mid-merge guard (`IsMidMergeAsync`) still prevents a second
|
||||||
|
concurrent merge.
|
||||||
|
|
||||||
|
## Layer B — multi-worktree merge cockpit (parallel)
|
||||||
|
|
||||||
|
- Rework `WorktreesOverviewModalView`/`WorktreesOverviewModalViewModel` into a
|
||||||
|
batch-merge cockpit: list mergeable worktrees, select N, choose one target branch
|
||||||
|
(single target — 99% of the time everything goes to the same branch), "Merge all".
|
||||||
|
- **Skip-and-continue**: client-side loop calling the existing
|
||||||
|
`MergeTaskAsync(taskId, target, removeWorktree, msg)` per selected task. Clean merges
|
||||||
|
apply; conflicting ones are collected (existing `MergeTaskAsync` auto-aborts on
|
||||||
|
conflict, leaving the tree clean) into a "needs resolution" list with live progress.
|
||||||
|
- Each conflict row exposes a **Resolve** action → `RequestConflictResolution(taskId)`
|
||||||
|
(wired to Layer C at integration).
|
||||||
|
- Per-task diff via the shared `DiffView`; migrate `WorktreeModalView`'s inline diff
|
||||||
|
onto it.
|
||||||
|
- B touches no worker files — keeps it parallel-safe.
|
||||||
|
|
||||||
|
## Layer C — inline conflict resolver (parallel)
|
||||||
|
|
||||||
|
### Worker side
|
||||||
|
|
||||||
|
Implement the five frozen contract methods:
|
||||||
|
|
||||||
|
- Add hub methods `StartConflictMerge`, `GetMergeConflicts`, `WriteConflictResolution`,
|
||||||
|
`ContinueMerge`, `AbortMerge` in `WorkerHub`.
|
||||||
|
- `StartConflictMerge` calls the existing `TaskMergeService.MergeAsync` overload with
|
||||||
|
`leaveConflictsInTree: true`.
|
||||||
|
- `ContinueMerge` / `AbortMerge` wrap the existing `TaskMergeService.ContinueMergeAsync`
|
||||||
|
/ `AbortMergeAsync` (currently service-level only, not hub-exposed).
|
||||||
|
- `GetMergeConflicts` reads ours/theirs/base per conflicted file via
|
||||||
|
`git show :2:/:3:/:1:`; add the `GitService` helpers needed.
|
||||||
|
- `WriteConflictResolution` writes the resolved content to `WorkingDir` and stages it.
|
||||||
|
- Fill the `WorkerClient` stub bodies (real SignalR `InvokeAsync` calls).
|
||||||
|
- Update the hand-rolled `IWorkerClient` fakes in both test projects.
|
||||||
|
|
||||||
|
### UI
|
||||||
|
|
||||||
|
- New `ConflictResolverView` + `ConflictResolverViewModel`. Per conflict hunk, show
|
||||||
|
ours vs theirs stacked, with buttons **Accept Current / Accept Incoming / Accept Both
|
||||||
|
/ Edit manually** plus a free-text box for the merged result of that hunk.
|
||||||
|
- When every file's hunks are resolved → `ContinueMergeAsync(taskId)` → `MergeResultDto`
|
||||||
|
(`merged` closes the resolver; `conflict` means not fully resolved, stay open).
|
||||||
|
- `AbortMergeAsync(taskId)` cancels and aborts the merge.
|
||||||
|
- Expose a factory (`Func<string, ConflictResolverViewModel>`) the integrator wires to
|
||||||
|
A's and B's `RequestConflictResolution` callbacks.
|
||||||
|
|
||||||
|
## Build / test
|
||||||
|
|
||||||
|
`.slnx` needs .NET 9; on .NET 8 build individual csproj with `-c Release` (a running
|
||||||
|
Worker locks `Debug`). Run the relevant test projects. No tests that spawn the real
|
||||||
|
`claude` CLI. Keep `en.json`/`de.json` localization keys in parity.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
- Full 3-way synchronized merge editor (model leaves room; not built now).
|
||||||
|
- Per-task differing merge targets in the batch (single target only).
|
||||||
|
- Any CI/PR tooling (direct push-to-main workflow).
|
||||||
@@ -43,6 +43,13 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
Task RejectReviewToQueueAsync(string taskId, string feedback);
|
Task RejectReviewToQueueAsync(string taskId, string feedback);
|
||||||
Task RejectReviewToIdleAsync(string taskId);
|
Task RejectReviewToIdleAsync(string taskId);
|
||||||
Task CancelReviewAsync(string taskId);
|
Task CancelReviewAsync(string taskId);
|
||||||
|
|
||||||
|
// ── Conflict resolution (worker hub side implemented by Layer C) ──
|
||||||
|
Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch);
|
||||||
|
Task<MergeConflictsDto> GetMergeConflictsAsync(string taskId);
|
||||||
|
Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent);
|
||||||
|
Task<MergeResultDto> ContinueMergeAsync(string taskId);
|
||||||
|
Task AbortMergeAsync(string taskId);
|
||||||
Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||||
Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default);
|
Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default);
|
||||||
Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default);
|
Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||||
|
|||||||
@@ -269,6 +269,21 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
"MergeTask", taskId, targetBranch, removeWorktree, commitMessage);
|
"MergeTask", taskId, targetBranch, removeWorktree, commitMessage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch)
|
||||||
|
=> _hub.InvokeAsync<MergeResultDto>("StartConflictMerge", taskId, targetBranch);
|
||||||
|
|
||||||
|
public Task<MergeConflictsDto> GetMergeConflictsAsync(string taskId)
|
||||||
|
=> _hub.InvokeAsync<MergeConflictsDto>("GetMergeConflicts", taskId);
|
||||||
|
|
||||||
|
public Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent)
|
||||||
|
=> _hub.InvokeAsync("WriteConflictResolution", taskId, path, resolvedContent);
|
||||||
|
|
||||||
|
public Task<MergeResultDto> ContinueMergeAsync(string taskId)
|
||||||
|
=> _hub.InvokeAsync<MergeResultDto>("ContinueMerge", taskId);
|
||||||
|
|
||||||
|
public Task AbortMergeAsync(string taskId)
|
||||||
|
=> _hub.InvokeAsync("AbortMerge", taskId);
|
||||||
|
|
||||||
public Task<MergeTargetsDto?> GetMergeTargetsAsync(string taskId)
|
public Task<MergeTargetsDto?> GetMergeTargetsAsync(string taskId)
|
||||||
=> TryInvokeAsync<MergeTargetsDto>("GetMergeTargets", taskId);
|
=> TryInvokeAsync<MergeTargetsDto>("GetMergeTargets", taskId);
|
||||||
|
|
||||||
@@ -532,6 +547,9 @@ public sealed record WorktreeResetDto(int Removed, int TasksAffected, bool Block
|
|||||||
public record MergeResultDto(string Status, IReadOnlyList<string> ConflictFiles, string? ErrorMessage);
|
public record MergeResultDto(string Status, IReadOnlyList<string> ConflictFiles, string? ErrorMessage);
|
||||||
public record MergePreviewDto(string Status, IReadOnlyList<string> ConflictFiles, int ChangedFileCount);
|
public record MergePreviewDto(string Status, IReadOnlyList<string> ConflictFiles, int ChangedFileCount);
|
||||||
public record MergeTargetsDto(string DefaultBranch, IReadOnlyList<string> LocalBranches);
|
public record MergeTargetsDto(string DefaultBranch, IReadOnlyList<string> LocalBranches);
|
||||||
|
public record MergeConflictsDto(string TaskId, IReadOnlyList<ConflictFileDto> Files);
|
||||||
|
public record ConflictFileDto(string Path, IReadOnlyList<ConflictHunkDto> Hunks);
|
||||||
|
public record ConflictHunkDto(string Ours, string Theirs, string? Base);
|
||||||
public sealed record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType);
|
public sealed record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType);
|
||||||
public sealed record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
|
public sealed record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
|
||||||
public sealed record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
|
public sealed record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null);
|
||||||
|
|||||||
@@ -1470,10 +1470,14 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async System.Threading.Tasks.Task ParkReviewAsync()
|
private async System.Threading.Tasks.Task ResetReviewAsync()
|
||||||
{
|
{
|
||||||
if (Task is null || !_worker.IsConnected) return;
|
if (Task is null || !_worker.IsConnected || ConfirmAsync is null) return;
|
||||||
try { await _worker.RejectReviewToIdleAsync(Task.Id); }
|
var branchName = $"claudedo/{Task.Id.Replace("-", "")}";
|
||||||
|
var ok = await ConfirmAsync(
|
||||||
|
$"Reset working tree?\nThis discards branch {branchName} (and all changes) and returns the task to Idle.");
|
||||||
|
if (!ok) return;
|
||||||
|
try { await _worker.ResetTaskAsync(Task.Id); }
|
||||||
catch { /* stale review action; broadcast reconciles */ }
|
catch { /* stale review action; broadcast reconciles */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -36,14 +36,15 @@
|
|||||||
<Setter Property="Foreground" Value="{StaticResource TextMuteBrush}" />
|
<Setter Property="Foreground" Value="{StaticResource TextMuteBrush}" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.prompt-action /template/ ContentPresenter">
|
<Style Selector="Button.prompt-action /template/ ContentPresenter">
|
||||||
<Setter Property="Background" Value="Transparent" />
|
<Setter Property="Background" Value="Transparent" />
|
||||||
|
<Setter Property="TextElement.Foreground" Value="{StaticResource TextMuteBrush}" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.prompt-action:pointerover /template/ ContentPresenter">
|
<Style Selector="Button.prompt-action:pointerover /template/ ContentPresenter">
|
||||||
<Setter Property="Background" Value="Transparent" />
|
<Setter Property="Background" Value="Transparent" />
|
||||||
<Setter Property="TextElement.Foreground" Value="{StaticResource TextBrush}" />
|
<Setter Property="TextElement.Foreground" Value="{StaticResource TextBrush}" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.prompt-action.accent">
|
<Style Selector="Button.prompt-action.accent /template/ ContentPresenter">
|
||||||
<Setter Property="Foreground" Value="{StaticResource AccentBrush}" />
|
<Setter Property="TextElement.Foreground" Value="{StaticResource AccentBrush}" />
|
||||||
</Style>
|
</Style>
|
||||||
<Style Selector="Button.prompt-action.accent:pointerover /template/ ContentPresenter">
|
<Style Selector="Button.prompt-action.accent:pointerover /template/ ContentPresenter">
|
||||||
<Setter Property="TextElement.Foreground" Value="{StaticResource MossBrightBrush}" />
|
<Setter Property="TextElement.Foreground" Value="{StaticResource MossBrightBrush}" />
|
||||||
@@ -198,10 +199,10 @@
|
|||||||
FontSize="{StaticResource FontSizeMono}" />
|
FontSize="{StaticResource FontSizeMono}" />
|
||||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="10"
|
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="10"
|
||||||
VerticalAlignment="Top" Margin="12,2,0,0">
|
VerticalAlignment="Top" Margin="12,2,0,0">
|
||||||
<Button Classes="prompt-action accent" Content="[Retry]"
|
<Button Classes="prompt-action accent" Content="[Continue]"
|
||||||
Command="{Binding RejectReviewCommand}" />
|
Command="{Binding RejectReviewCommand}" />
|
||||||
<Button Classes="prompt-action" Content="[Reset]"
|
<Button Classes="prompt-action" Content="[Reset]"
|
||||||
Command="{Binding ParkReviewCommand}" />
|
Command="{Binding ResetReviewCommand}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
|
|||||||
@@ -58,6 +58,11 @@ public abstract class StubWorkerClient : IWorkerClient
|
|||||||
public virtual Task RejectReviewToQueueAsync(string taskId, string feedback) => Task.CompletedTask;
|
public virtual Task RejectReviewToQueueAsync(string taskId, string feedback) => Task.CompletedTask;
|
||||||
public virtual Task RejectReviewToIdleAsync(string taskId) => Task.CompletedTask;
|
public virtual Task RejectReviewToIdleAsync(string taskId) => Task.CompletedTask;
|
||||||
public virtual Task CancelReviewAsync(string taskId) => Task.CompletedTask;
|
public virtual Task CancelReviewAsync(string taskId) => Task.CompletedTask;
|
||||||
|
public virtual Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch) => Task.FromResult(new MergeResultDto("conflict", System.Array.Empty<string>(), null));
|
||||||
|
public virtual Task<MergeConflictsDto> GetMergeConflictsAsync(string taskId) => Task.FromResult(new MergeConflictsDto(taskId, System.Array.Empty<ConflictFileDto>()));
|
||||||
|
public virtual Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent) => Task.CompletedTask;
|
||||||
|
public virtual Task<MergeResultDto> ContinueMergeAsync(string taskId) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty<string>(), null));
|
||||||
|
public virtual Task AbortMergeAsync(string taskId) => Task.CompletedTask;
|
||||||
public virtual Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
public virtual Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
public virtual Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
public virtual Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
public virtual Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
public virtual Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
|
|||||||
@@ -45,6 +45,11 @@ sealed class FakeWorkerClient : IWorkerClient
|
|||||||
public Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch) => Task.FromResult<MergeResultDto?>(null);
|
public Task<MergeResultDto?> ApproveReviewAsync(string taskId, string targetBranch) => Task.FromResult<MergeResultDto?>(null);
|
||||||
public Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch) => Task.FromResult<MergePreviewDto?>(null);
|
public Task<MergePreviewDto?> PreviewMergeAsync(string taskId, string targetBranch) => Task.FromResult<MergePreviewDto?>(null);
|
||||||
public Task<MergeResultDto> MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty<string>(), null));
|
public Task<MergeResultDto> MergeTaskAsync(string taskId, string targetBranch, bool removeWorktree, string commitMessage) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty<string>(), null));
|
||||||
|
public Task<MergeResultDto> StartConflictMergeAsync(string taskId, string targetBranch) => Task.FromResult(new MergeResultDto("conflict", System.Array.Empty<string>(), null));
|
||||||
|
public Task<MergeConflictsDto> GetMergeConflictsAsync(string taskId) => Task.FromResult(new MergeConflictsDto(taskId, System.Array.Empty<ConflictFileDto>()));
|
||||||
|
public Task WriteConflictResolutionAsync(string taskId, string path, string resolvedContent) => Task.CompletedTask;
|
||||||
|
public Task<MergeResultDto> ContinueMergeAsync(string taskId) => Task.FromResult(new MergeResultDto("merged", System.Array.Empty<string>(), null));
|
||||||
|
public Task AbortMergeAsync(string taskId) => Task.CompletedTask;
|
||||||
public Task RejectReviewToQueueAsync(string taskId, string feedback) => Task.CompletedTask;
|
public Task RejectReviewToQueueAsync(string taskId, string feedback) => Task.CompletedTask;
|
||||||
public Task RejectReviewToIdleAsync(string taskId) => Task.CompletedTask;
|
public Task RejectReviewToIdleAsync(string taskId) => Task.CompletedTask;
|
||||||
public Task CancelReviewAsync(string taskId) => Task.CompletedTask;
|
public Task CancelReviewAsync(string taskId) => Task.CompletedTask;
|
||||||
|
|||||||
Reference in New Issue
Block a user