docs(review): add implementation plan for terminal-style review controls

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-05 08:21:33 +02:00
parent 266e6d191b
commit 096519b978

View File

@@ -0,0 +1,522 @@
# Terminal-style Review Controls 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:** Move review feedback into the Output (terminal) tab as a prompt-style input with `[Retry]`/`[Reset]` actions, and relocate Approve + all merge/worktree controls to a new **Git** tab.
**Architecture:** Pure UI-layer change in `ClaudeDo.Ui`. Add an `IsGitTab` computed flag to `DetailsIslandViewModel`, re-home existing XAML blocks across three tabs (Output · Git · Session) in `WorkConsole.axaml`, add a bottom-docked review footer to the Output tab, and intercept Enter in `WorkConsole.axaml.cs`. No worker-side or `IWorkerClient` changes; no ViewModel command renames.
**Tech Stack:** .NET 8, Avalonia 12 (Fluent), CommunityToolkit.Mvvm, xUnit (ClaudeDo.Ui.Tests).
**Reference spec:** `docs/superpowers/specs/2026-06-05-terminal-review-design.md`
**Build/test note (from CLAUDE.md):** A running Worker locks `Debug` output — build UI in `-c Release`:
`dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
`dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release`
---
## File Structure
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs` — add `IsGitTab`, wire notifications.
- Modify: `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml` — add Git tab button; split tab bodies; add Output-tab review footer; update Session empty-state text.
- Modify: `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml.cs` — Enter-to-Retry key handling.
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandTabsTests.cs` (create) — `IsGitTab` behavior.
---
### Task 1: Add `IsGitTab` tab flag to the ViewModel
**Files:**
- Modify: `src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs:139-147`
- Test: `tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandTabsTests.cs` (create)
- [ ] **Step 1: Write the failing test**
Create `tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandTabsTests.cs`. Mirror the
construction pattern from `DetailsIslandPrepModeTests.cs` (temp SQLite db,
`TestDbFactory`, `StubWorkerClient`, `NullServiceProvider`, `StubNotesApi`).
```csharp
using ClaudeDo.Data;
using ClaudeDo.Ui.Services;
using ClaudeDo.Ui.ViewModels.Islands;
using Microsoft.EntityFrameworkCore;
namespace ClaudeDo.Ui.Tests.ViewModels;
public class DetailsIslandTabsTests : IDisposable
{
private readonly string _dbPath;
public DetailsIslandTabsTests()
{
_dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_tabs_test_{Guid.NewGuid():N}.db");
using var ctx = NewContext();
ctx.Database.EnsureCreated();
}
public void Dispose()
{
try { File.Delete(_dbPath); } catch { }
try { File.Delete(_dbPath + "-wal"); } catch { }
try { File.Delete(_dbPath + "-shm"); } catch { }
}
private ClaudeDoDbContext NewContext()
{
var opts = new DbContextOptionsBuilder<ClaudeDoDbContext>()
.UseSqlite($"Data Source={_dbPath}")
.Options;
return new ClaudeDoDbContext(opts);
}
private sealed class TestDbFactory : IDbContextFactory<ClaudeDoDbContext>
{
private readonly Func<ClaudeDoDbContext> _create;
public TestDbFactory(Func<ClaudeDoDbContext> create) => _create = create;
public ClaudeDoDbContext CreateDbContext() => _create();
}
private sealed class StubNotesApi : ClaudeDo.Ui.Services.Interfaces.INotesApi
{
public Task<List<DailyNoteDto>> ListAsync(DateOnly day) => Task.FromResult(new List<DailyNoteDto>());
public Task<DailyNoteDto?> AddAsync(DateOnly day, string text) => Task.FromResult<DailyNoteDto?>(null);
public Task UpdateAsync(string id, string text) => Task.CompletedTask;
public Task DeleteAsync(string id) => Task.CompletedTask;
}
private sealed class NullServiceProvider : IServiceProvider
{
public object? GetService(Type serviceType) => null;
}
// StubWorkerClient is abstract — use a concrete no-op subclass (same pattern as DetailsIslandPrepModeTests).
private sealed class DefaultStub : StubWorkerClient { }
private DetailsIslandViewModel NewVm()
{
var factory = new TestDbFactory(NewContext);
return new DetailsIslandViewModel(factory, new DefaultStub(), new NullServiceProvider(), new StubNotesApi());
}
[Fact]
public void SelectTab_git_sets_IsGitTab_and_clears_others()
{
var vm = NewVm();
vm.SelectTabCommand.Execute("git");
Assert.True(vm.IsGitTab);
Assert.False(vm.IsOutputTab);
Assert.False(vm.IsSessionTab);
}
[Fact]
public void Default_tab_is_output_not_git()
{
var vm = NewVm();
Assert.True(vm.IsOutputTab);
Assert.False(vm.IsGitTab);
}
}
```
- [ ] **Step 2: Run test to verify it fails**
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter DetailsIslandTabsTests`
Expected: FAIL — compile error, `DetailsIslandViewModel` has no `IsGitTab`.
- [ ] **Step 3: Add `IsGitTab` to the ViewModel**
In `DetailsIslandViewModel.cs`, find the `SelectedTab` property notifications and the
tab getters (around lines 139-147). Add the `IsGitTab` notification and getter:
```csharp
[NotifyPropertyChangedFor(nameof(IsOutputTab))]
[NotifyPropertyChangedFor(nameof(IsSessionTab))]
[NotifyPropertyChangedFor(nameof(IsGitTab))]
```
```csharp
public bool IsOutputTab => SelectedTab == "output";
public bool IsGitTab => SelectedTab == "git";
public bool IsSessionTab => SelectedTab == "session";
```
(Leave `SelectTab` unchanged — it already accepts any string and defaults to `"output"`.)
- [ ] **Step 4: Run test to verify it passes**
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release --filter DetailsIslandTabsTests`
Expected: PASS (2 tests).
- [ ] **Step 5: Commit**
```bash
git add src/ClaudeDo.Ui/ViewModels/Islands/DetailsIslandViewModel.cs tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandTabsTests.cs
git commit -m "feat(ui): add IsGitTab flag to work console view model"
```
---
### Task 2: Add the Git tab button and move the merge/worktree block onto it
**Files:**
- Modify: `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml:124-135` (tab strip)
- Modify: `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml:164-273` (tab body)
- [ ] **Step 1: Add the Git tab button**
In the tab strip `StackPanel` (lines 124-135), insert a Git button between the Output
and Session buttons:
```xml
<StackPanel Orientation="Horizontal">
<Button Classes="tab-btn"
Classes.active="{Binding IsOutputTab}"
Content="Output"
Command="{Binding SelectTabCommand}"
CommandParameter="output" />
<Button Classes="tab-btn"
Classes.active="{Binding IsGitTab}"
Content="Git"
Command="{Binding SelectTabCommand}"
CommandParameter="git" />
<Button Classes="tab-btn"
Classes.active="{Binding IsSessionTab}"
Content="Session"
Command="{Binding SelectTabCommand}"
CommandParameter="session" />
</StackPanel>
```
- [ ] **Step 2: Move the "Merge & worktree" block to a new Git-tab ScrollViewer**
In the tab body `Grid` (starts line 139), the body currently holds the Output
`ScrollViewer` (`IsVisible="{Binding IsOutputTab}"`, lines 142-162) and the Session
`ScrollViewer` (`IsVisible="{Binding IsSessionTab}"`, lines 165-273).
Cut the **entire "Merge & worktree management" `StackPanel`** — the block currently at
lines 195-241, beginning with the comment `<!-- Merge & worktree management -->` and the
`<StackPanel Spacing="10" IsVisible="{Binding ShowMergeSection}">` and ending at its
matching `</StackPanel>` after the `MergeAllError` `TextBlock` (line 241).
Add a new Git-tab `ScrollViewer` between the Output and Session `ScrollViewer`s, and
paste the cut block inside it:
```xml
<!-- Git: merge target, approve, diff, worktree -->
<ScrollViewer IsVisible="{Binding IsGitTab}" Padding="14,10">
<StackPanel Spacing="14">
<!-- Approve (review-gated) -->
<StackPanel Spacing="8" IsVisible="{Binding IsWaitingForReview}">
<TextBlock Classes="section-label" Text="REVIEW" />
<Button Classes="btn accent" Content="Approve"
Command="{Binding ApproveReviewCommand}" />
</StackPanel>
<!-- Merge & worktree management (moved from Session tab) -->
<StackPanel Spacing="10" IsVisible="{Binding ShowMergeSection}">
<TextBlock Classes="section-label" Text="MERGE &amp; WORKTREE" />
<StackPanel Spacing="4">
<TextBlock Classes="field-label" Text="Merge target" />
<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>
<WrapPanel Orientation="Horizontal">
<Button Classes="btn" Content="Open Diff" Margin="0,0,8,8"
Command="{Binding OpenDiffCommand}" />
<Button Classes="btn accent" Content="Merge" Margin="0,0,8,8"
Command="{Binding MergeCommand}"
IsVisible="{Binding ShowSingleMerge}" />
<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>
</StackPanel>
</ScrollViewer>
```
- [ ] **Step 3: Remove the old review block from the Session tab**
In the Session `ScrollViewer` (`IsVisible="{Binding IsSessionTab}"`), delete the
**"Review controls" `StackPanel`** currently at lines 168-193 (the
`<!-- Review controls -->` comment, the `<StackPanel Spacing="8" IsVisible="{Binding IsWaitingForReview}">`,
the REVIEW label, Feedback label, the `ReviewFeedback` TextBox, and the four buttons).
After this and Step 2, the Session tab's `StackPanel` should contain only the Child
outcomes block (lines 244-263) and the empty-state `TextBlock` (lines 266-270).
- [ ] **Step 4: Build and verify it compiles**
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
Expected: Build succeeded, 0 errors.
- [ ] **Step 5: Commit**
```bash
git add src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml
git commit -m "feat(ui): add Git tab and move merge/approve controls onto it"
```
---
### Task 3: Add the prompt-style review footer to the Output tab
**Files:**
- Modify: `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml` (Output-tab area + the `Grid` body)
- [ ] **Step 1: Restructure the Output tab body to dock a footer below the log**
The body `Grid` (line 139) overlays all three tab `ScrollViewer`s. Replace the Output
`ScrollViewer` (lines 142-162) with a `DockPanel` that keeps the log filling and docks
the review footer at the bottom. Keep `Name="LogScroll"` on the `ScrollViewer` (the
code-behind references it). Use this exact markup:
```xml
<!-- Output: log + review footer, both gated on IsOutputTab -->
<DockPanel IsVisible="{Binding IsOutputTab}" LastChildFill="True">
<!-- Review footer (terminal prompt) — only while awaiting review -->
<Border DockPanel.Dock="Bottom"
IsVisible="{Binding IsWaitingForReview}"
Background="{DynamicResource Surface2Brush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,1,0,0"
Padding="10,6">
<DockPanel LastChildFill="True">
<StackPanel DockPanel.Dock="Right" Orientation="Horizontal" Spacing="8"
VerticalAlignment="Bottom" Margin="8,0,0,0">
<Button Classes="btn accent" Content="Retry"
Command="{Binding RejectReviewCommand}" />
<Button Classes="btn" Content="Reset"
Command="{Binding ParkReviewCommand}" />
</StackPanel>
<TextBlock DockPanel.Dock="Left" Text="&#x276F;"
FontFamily="{StaticResource MonoFont}"
Foreground="{DynamicResource TextMuteBrush}"
VerticalAlignment="Top" Margin="0,4,8,0" />
<TextBox Name="ReviewInput"
Text="{Binding ReviewFeedback, Mode=TwoWay}"
AcceptsReturn="True"
TextWrapping="Wrap"
MaxHeight="160"
PlaceholderText="Feedback for the next run…"
Background="Transparent"
BorderThickness="0"
Padding="0,2"
FontFamily="{StaticResource MonoFont}"
FontSize="{StaticResource FontSizeMono}" />
</DockPanel>
</Border>
<ScrollViewer Name="LogScroll"
VerticalScrollBarVisibility="Visible"
AllowAutoHide="False"
Padding="12,8,12,4">
<ItemsControl ItemsSource="{Binding Log}">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="vm:LogLineViewModel">
<Grid ColumnDefinitions="60,*" Margin="0,1">
<TextBlock Grid.Column="0"
Classes="log-ts"
Text="{Binding TimestampFormatted}" />
<SelectableTextBlock Grid.Column="1"
Text="{Binding Text}" Tag="{Binding ClassName}"
Foreground="{DynamicResource TextDimBrush}"
TextWrapping="Wrap" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
```
- [ ] **Step 2: Build and verify it compiles**
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
Expected: Build succeeded, 0 errors.
- [ ] **Step 3: Commit**
```bash
git add src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml
git commit -m "feat(ui): add terminal review footer with Retry/Reset to Output tab"
```
---
### Task 4: Enter-to-Retry key handling
**Files:**
- Modify: `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml.cs`
- [ ] **Step 1: Add the KeyDown handler**
In `WorkConsole.axaml.cs`, add `using Avalonia.Input;` at the top. Add a handler that
runs `RejectReviewCommand` on Enter (without Shift) and lets Shift+Enter insert a
newline. Wire it from the `ReviewInput` TextBox. Full file:
```csharp
using System;
using System.Collections.Specialized;
using Avalonia.Controls;
using Avalonia.Input;
using ClaudeDo.Ui.ViewModels.Islands;
namespace ClaudeDo.Ui.Views.Islands.Detail;
public partial class WorkConsole : UserControl
{
private INotifyCollectionChanged? _log;
public WorkConsole()
{
InitializeComponent();
DataContextChanged += OnDataContextChanged;
}
private void OnDataContextChanged(object? sender, EventArgs e)
{
if (_log is not null)
_log.CollectionChanged -= OnLogChanged;
_log = (DataContext as DetailsIslandViewModel)?.Log;
if (_log is not null)
_log.CollectionChanged += OnLogChanged;
}
private void OnLogChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
if (e.Action != NotifyCollectionChangedAction.Add) return;
EventHandler? handler = null;
handler = (_, _) =>
{
LogScroll.LayoutUpdated -= handler;
LogScroll.ScrollToEnd();
};
LogScroll.LayoutUpdated += handler;
}
private void OnReviewInputKeyDown(object? sender, KeyEventArgs e)
{
if (e.Key != Key.Enter || e.KeyModifiers.HasFlag(KeyModifiers.Shift))
return;
if (DataContext is DetailsIslandViewModel vm &&
vm.RejectReviewCommand.CanExecute(null))
{
vm.RejectReviewCommand.Execute(null);
}
e.Handled = true;
}
}
```
- [ ] **Step 2: Wire the handler in XAML**
On the `ReviewInput` TextBox added in Task 3, add the event hookup attribute:
```xml
<TextBox Name="ReviewInput"
KeyDown="OnReviewInputKeyDown"
Text="{Binding ReviewFeedback, Mode=TwoWay}"
```
- [ ] **Step 3: Build and verify it compiles**
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
Expected: Build succeeded, 0 errors.
- [ ] **Step 4: Commit**
```bash
git add src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml.cs
git commit -m "feat(ui): send Retry on Enter in the review prompt"
```
---
### Task 5: Update the Session empty-state copy
**Files:**
- Modify: `src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml` (empty-state `TextBlock`, was line 266-270)
- [ ] **Step 1: Reword the empty-state text**
The Session empty-state still says review/merge controls appear there. Replace its
`Text` so it reflects that those moved:
```xml
<TextBlock IsVisible="{Binding ShowSessionEmpty}"
Classes="meta"
Foreground="{DynamicResource TextMuteBrush}"
TextWrapping="Wrap"
Text="Nothing to manage yet — subtask outcomes appear here once the run finishes. Review in the Output tab, merge in the Git tab." />
```
- [ ] **Step 2: Build and verify it compiles**
Run: `dotnet build src/ClaudeDo.App/ClaudeDo.App.csproj -c Release`
Expected: Build succeeded, 0 errors.
- [ ] **Step 3: Commit**
```bash
git add src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml
git commit -m "docs(ui): reword Session empty-state for relocated review/merge controls"
```
---
### Task 6: Final verification
- [ ] **Step 1: Run the full UI test project**
Run: `dotnet test tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -c Release`
Expected: all tests PASS.
- [ ] **Step 2: Manual visual verification (cannot be auto-verified — flag to user)**
Launch the app with a task in `WaitingForReview` and confirm:
- Output tab shows the prompt footer (`` + input + `[Retry]` `[Reset]`) only while awaiting review; it is hidden otherwise.
- Typing + **Enter** sends Retry (requeues with feedback); **Shift+Enter** inserts a newline; **Enter on empty input** does nothing.
- `[Reset]` parks the task to Idle.
- Git tab shows **Approve** + merge target + Open Diff / Merge / Worktree / Review Combined Diff / Merge All Subtasks.
- Session tab shows only subtask outcomes / the reworded empty state.
- Tab switching highlights the active tab correctly (Output ↔ Git ↔ Session).