Files
ClaudeDo/docs/superpowers/plans/2026-06-05-terminal-review.md
2026-06-05 08:21:33 +02:00

21 KiB
Raw Permalink Blame History

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).

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:

    [NotifyPropertyChangedFor(nameof(IsOutputTab))]
    [NotifyPropertyChangedFor(nameof(IsSessionTab))]
    [NotifyPropertyChangedFor(nameof(IsGitTab))]
    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
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:

        <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 ScrollViewers, and paste the cut block inside it:

        <!-- 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
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"

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 ScrollViewers. 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:

        <!-- 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
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:

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:

              <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
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:

            <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
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).