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