21 KiB
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— addIsGitTab, 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) —IsGitTabbehavior.
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
IsGitTabto 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 & 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"
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 + theGridbody) -
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="❯"
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-stateTextBlock, 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).