feat(ui): surface agent roadblocks and run outcome in the detail pane
- Parse CLAUDEDO_BLOCKED roadblocks out of the run result and show them in a colored card between Details and Output (ApplyOutcome / ShowRoadblockCard). - Show the run outcome summary as an OUTCOME card in the Output tab, loaded from the task result (falls back to the run's ErrorMarkdown) and refreshed on finish. - Guard the Session tab so it only appears when there are child outcomes. - Make console resize per-task and proportional (description capped at 2/3, console floored at ~1/3) so a long description no longer spills over the footer. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
|
||||
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
|
||||
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
||||
x:Class="ClaudeDo.Ui.Views.Islands.Detail.DescriptionStepsCard"
|
||||
x:DataType="vm:DetailsIslandViewModel">
|
||||
|
||||
@@ -21,7 +22,7 @@
|
||||
<Button Grid.Column="2"
|
||||
Classes="icon-btn"
|
||||
Margin="0,0,4,0"
|
||||
ToolTip.Tip="Copy formatted (title + description + open steps)"
|
||||
ToolTip.Tip="{loc:Tr details.copyFormattedTip}"
|
||||
Click="OnCopyClick">
|
||||
<PathIcon Data="{StaticResource Icon.Copy}" Width="11" Height="11"/>
|
||||
</Button>
|
||||
@@ -30,6 +31,7 @@
|
||||
<Button Grid.Column="3"
|
||||
Classes="btn"
|
||||
Padding="8,3"
|
||||
ToolTip.Tip="{loc:Tr details.toggleEditPreviewTip}"
|
||||
Command="{Binding ToggleEditDescriptionCommand}">
|
||||
<Panel>
|
||||
<TextBlock Text="Preview" IsVisible="{Binding IsEditingDescription}"/>
|
||||
@@ -40,7 +42,8 @@
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Body -->
|
||||
<!-- Body (scrolls inside the card so the card fills its row to the divider) -->
|
||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||
<StackPanel Margin="14" Spacing="10">
|
||||
|
||||
<!-- Description (always visible) -->
|
||||
@@ -159,6 +162,7 @@
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</DockPanel>
|
||||
</Border>
|
||||
|
||||
|
||||
@@ -72,22 +72,12 @@
|
||||
<Grid DockPanel.Dock="Top" ColumnDefinitions="Auto,*,Auto"
|
||||
Background="{DynamicResource Surface2Brush}" Height="28">
|
||||
|
||||
<!-- Traffic-light dots; green toggles console maximize -->
|
||||
<!-- Traffic-light dots (decorative) -->
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6"
|
||||
Margin="12,0" VerticalAlignment="Center">
|
||||
<Ellipse Classes="dot-red" />
|
||||
<Ellipse Classes="dot-yellow" />
|
||||
<Button Classes="dot-btn"
|
||||
Command="{Binding ToggleConsoleMaximizedCommand}"
|
||||
ToolTip.Tip="{loc:Tr console.maximizeTip}">
|
||||
<Panel>
|
||||
<Ellipse Classes="dot-green" />
|
||||
<PathIcon Data="{StaticResource Icon.ArrowOut}"
|
||||
Width="6" Height="6"
|
||||
Foreground="{DynamicResource MossBrightBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Panel>
|
||||
</Button>
|
||||
<Ellipse Classes="dot-green" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Right cluster: info header (model · turns · diff) + status chip -->
|
||||
@@ -183,6 +173,7 @@
|
||||
<Button Classes="tab-btn"
|
||||
Classes.active="{Binding IsSessionTab}"
|
||||
Content="Session"
|
||||
IsVisible="{Binding HasChildOutcomes}"
|
||||
Command="{Binding SelectTabCommand}"
|
||||
CommandParameter="session" />
|
||||
</StackPanel>
|
||||
@@ -194,6 +185,26 @@
|
||||
<!-- Output: log + review footer, both gated on IsOutputTab -->
|
||||
<DockPanel IsVisible="{Binding IsOutputTab}" LastChildFill="True">
|
||||
|
||||
<!-- Session outcome: the run's result summary, incl. any roadblocks
|
||||
reported (or the error for a hard failure). -->
|
||||
<Border DockPanel.Dock="Top"
|
||||
Margin="12,8,12,4" Padding="10,8"
|
||||
IsVisible="{Binding ShowSessionOutcome}"
|
||||
Background="{DynamicResource Surface2Brush}"
|
||||
BorderBrush="{DynamicResource LineBrush}"
|
||||
BorderThickness="1" CornerRadius="8">
|
||||
<StackPanel Spacing="6">
|
||||
<TextBlock Classes="section-label" Text="OUTCOME" />
|
||||
<ScrollViewer MaxHeight="160" VerticalScrollBarVisibility="Auto">
|
||||
<SelectableTextBlock Text="{Binding SessionOutcome}"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextDimBrush}"
|
||||
FontFamily="{StaticResource MonoFont}"
|
||||
FontSize="{StaticResource FontSizeMono}" />
|
||||
</ScrollViewer>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<!-- Review prompt — sits directly on the terminal, like a shell input line;
|
||||
only while awaiting review. No border/fill so it reads as part of the log. -->
|
||||
<Grid DockPanel.Dock="Bottom"
|
||||
@@ -222,8 +233,10 @@
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="10"
|
||||
VerticalAlignment="Top" Margin="12,2,0,0">
|
||||
<Button Classes="prompt-action accent" Content="[Continue]"
|
||||
ToolTip.Tip="{loc:Tr session.reviewContinueTip}"
|
||||
Command="{Binding RejectReviewCommand}" />
|
||||
<Button Classes="prompt-action" Content="[Reset]"
|
||||
ToolTip.Tip="{loc:Tr session.reviewResetTip}"
|
||||
Command="{Binding ResetReviewCommand}" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
@@ -275,19 +288,15 @@
|
||||
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. -->
|
||||
<!-- Primary action: Approve flows straight into the merge. -->
|
||||
<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"
|
||||
ToolTip.Tip="{loc:Tr agent.openWorktreeTip}"
|
||||
Command="{Binding OpenWorktreeCommand}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||
<TextBlock Text="Worktree" />
|
||||
@@ -336,13 +345,6 @@
|
||||
</ItemsControl>
|
||||
</StackPanel>
|
||||
|
||||
<!-- Empty state: nothing to manage yet -->
|
||||
<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." />
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
|
||||
|
||||
@@ -43,24 +43,55 @@
|
||||
IsVisible="{Binding IsTaskDetailVisible}"
|
||||
Margin="14,12,14,12">
|
||||
<Grid.RowDefinitions>
|
||||
<!-- MinHeight keeps the description visible: the console can never
|
||||
overlap it, whether maximized (code-behind) or dragged. -->
|
||||
<RowDefinition Height="2*" MinHeight="90"/>
|
||||
<!-- Auto: the description sizes to its content so the console takes
|
||||
every spare pixel when it's short. Row limits are proportional
|
||||
and set in code-behind (UpdateRowLimits): the description row is
|
||||
capped at 2/3 of the island and the console row floored at 1/3,
|
||||
so the console can be dragged down to (but not below) 1/3 and a
|
||||
long description never spills over the footer. -->
|
||||
<RowDefinition Height="Auto" MinHeight="90"/>
|
||||
<RowDefinition Height="*"/>
|
||||
</Grid.RowDefinitions>
|
||||
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto">
|
||||
<detail:DescriptionStepsCard VerticalAlignment="Top"/>
|
||||
</ScrollViewer>
|
||||
<detail:WorkConsole Grid.Row="1" Margin="0,10,0,0"/>
|
||||
<detail:DescriptionStepsCard x:Name="DescriptionCard" Grid.Row="0"/>
|
||||
|
||||
<!-- Console row also hosts the roadblock card (docked above the console)
|
||||
so it surfaces at a glance between Details and Output. Keeping it
|
||||
inside row 1 leaves the desc/console resize model untouched. -->
|
||||
<DockPanel Grid.Row="1" Margin="0,10,0,0">
|
||||
<Border DockPanel.Dock="Top"
|
||||
IsVisible="{Binding ShowRoadblockCard}"
|
||||
Margin="0,0,0,10" Padding="12,10"
|
||||
Background="{DynamicResource ReviewTintBrush}"
|
||||
BorderBrush="{DynamicResource ReviewTintBorderBrush}"
|
||||
BorderThickness="1" CornerRadius="10">
|
||||
<StackPanel Spacing="6">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<PathIcon Data="{StaticResource Icon.Warning}"
|
||||
Foreground="{DynamicResource StatusReviewBrush}"
|
||||
Width="14" Height="14" VerticalAlignment="Center"/>
|
||||
<TextBlock Classes="section-label" Text="ROADBLOCK"
|
||||
Foreground="{DynamicResource StatusReviewBrush}"
|
||||
VerticalAlignment="Center"/>
|
||||
</StackPanel>
|
||||
<SelectableTextBlock Text="{Binding Roadblocks}"
|
||||
TextWrapping="Wrap"
|
||||
Foreground="{DynamicResource TextDimBrush}"/>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
<detail:WorkConsole/>
|
||||
</DockPanel>
|
||||
|
||||
<!-- Resize by dragging the console's top edge — a transparent splitter
|
||||
over the gap above the console; no standalone separator bar.
|
||||
Stays draggable while maximized. -->
|
||||
<GridSplitter Grid.Row="1"
|
||||
<GridSplitter x:Name="DetailSplitter" Grid.Row="1"
|
||||
VerticalAlignment="Top"
|
||||
Height="10"
|
||||
HorizontalAlignment="Stretch"
|
||||
ResizeDirection="Rows"
|
||||
Background="Transparent"/>
|
||||
Background="Transparent"
|
||||
DragStarted="OnSplitterDragStarted"
|
||||
DragCompleted="OnSplitterDragCompleted"/>
|
||||
</Grid>
|
||||
|
||||
<!-- Notes mode -->
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
using System.ComponentModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Layout;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Reactive;
|
||||
using ClaudeDo.Ui.ViewModels.Islands;
|
||||
using ClaudeDo.Ui.Views.Modals;
|
||||
using ClaudeDo.Ui.Views.Planning;
|
||||
@@ -11,27 +12,42 @@ namespace ClaudeDo.Ui.Views.Islands;
|
||||
|
||||
public partial class DetailsIslandView : UserControl
|
||||
{
|
||||
private DetailsIslandViewModel? _subscribedVm;
|
||||
// Per-task description height (pixels) once the user drags the splitter.
|
||||
// Keyed by task id so each task keeps its own resize; tasks that were
|
||||
// never dragged stay dynamic (Auto-sized description).
|
||||
private readonly Dictionary<string, double> _descriptionHeights = new();
|
||||
private DetailsIslandViewModel? _vm;
|
||||
|
||||
public DetailsIslandView()
|
||||
{
|
||||
InitializeComponent();
|
||||
DataContextChanged += OnDataContextChanged;
|
||||
// Keep the row limits proportional to the island height: description
|
||||
// capped at 2/3, console floored at 1/3. The GridSplitter honours these
|
||||
// row Min/Max during a drag, so the console stops shrinking at 1/3.
|
||||
DetailBodyGrid.GetObservable(BoundsProperty)
|
||||
.Subscribe(new AnonymousObserver<Rect>(_ => UpdateRowLimits()));
|
||||
}
|
||||
|
||||
private void UpdateRowLimits()
|
||||
{
|
||||
var h = DetailBodyGrid.Bounds.Height;
|
||||
if (h <= 0) return;
|
||||
DetailBodyGrid.RowDefinitions[0].MaxHeight = h * 2.0 / 3.0;
|
||||
DetailBodyGrid.RowDefinitions[1].MinHeight = h / 3.0;
|
||||
}
|
||||
|
||||
private void OnDataContextChanged(object? sender, EventArgs e)
|
||||
{
|
||||
if (_subscribedVm is not null)
|
||||
_subscribedVm.PropertyChanged -= OnVmPropertyChanged;
|
||||
_subscribedVm = DataContext as DetailsIslandViewModel;
|
||||
if (_subscribedVm is not null)
|
||||
{
|
||||
_subscribedVm.PropertyChanged += OnVmPropertyChanged;
|
||||
ApplyConsoleMaximized(_subscribedVm.IsConsoleMaximized);
|
||||
}
|
||||
if (_vm != null)
|
||||
_vm.PropertyChanged -= OnViewModelPropertyChanged;
|
||||
|
||||
if (DataContext is DetailsIslandViewModel vm)
|
||||
{
|
||||
_vm = vm;
|
||||
vm.PropertyChanged += OnViewModelPropertyChanged;
|
||||
ApplyResizeStateForCurrentTask();
|
||||
|
||||
vm.ShowDiffModal = async (diffVm) =>
|
||||
{
|
||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||
@@ -61,22 +77,39 @@ public partial class DetailsIslandView : UserControl
|
||||
}
|
||||
}
|
||||
|
||||
private void OnVmPropertyChanged(object? sender, PropertyChangedEventArgs e)
|
||||
// Restores the resize state for the currently-selected task: a task the
|
||||
// user has dragged before gets its pinned pixel height (cap lifted); a task
|
||||
// never dragged falls back to dynamic sizing (Auto row + the bound cap).
|
||||
private void OnViewModelPropertyChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
|
||||
{
|
||||
if (e.PropertyName == nameof(DetailsIslandViewModel.IsConsoleMaximized)
|
||||
&& sender is DetailsIslandViewModel vm)
|
||||
ApplyConsoleMaximized(vm.IsConsoleMaximized);
|
||||
if (e.PropertyName == nameof(DetailsIslandViewModel.Task))
|
||||
ApplyResizeStateForCurrentTask();
|
||||
}
|
||||
|
||||
// Maximized: shrink the description row to its MinHeight (the console fills
|
||||
// the rest). Restored: back to the 2:1 default. The GridSplitter keeps both
|
||||
// states draggable; MinHeight stops the console from ever covering it.
|
||||
private void ApplyConsoleMaximized(bool maximized)
|
||||
private void ApplyResizeStateForCurrentTask()
|
||||
{
|
||||
// A task dragged before keeps its pixel height (clamped by the row's
|
||||
// 2/3 MaxHeight); a task never dragged stays Auto-sized.
|
||||
DetailBodyGrid.RowDefinitions[0].Height = _vm?.Task?.Id is string id && _descriptionHeights.TryGetValue(id, out var h)
|
||||
? new GridLength(h, GridUnitType.Pixel)
|
||||
: GridLength.Auto;
|
||||
}
|
||||
|
||||
// Pin the (until now Auto-sized) description row to its current pixel
|
||||
// height so the splitter resizes smoothly from there.
|
||||
private void OnSplitterDragStarted(object? sender, VectorEventArgs e)
|
||||
{
|
||||
var descRow = DetailBodyGrid.RowDefinitions[0];
|
||||
descRow.Height = maximized
|
||||
? new GridLength(descRow.MinHeight, GridUnitType.Pixel)
|
||||
: new GridLength(2, GridUnitType.Star);
|
||||
if (descRow.Height.IsAuto)
|
||||
descRow.Height = new GridLength(DescriptionCard.Bounds.Height, GridUnitType.Pixel);
|
||||
}
|
||||
|
||||
// Remember the dragged height for this task so switching tasks keeps each
|
||||
// task's resize independent.
|
||||
private void OnSplitterDragCompleted(object? sender, VectorEventArgs e)
|
||||
{
|
||||
if (_vm?.Task?.Id is string id)
|
||||
_descriptionHeights[id] = DetailBodyGrid.RowDefinitions[0].Height.Value;
|
||||
}
|
||||
|
||||
private async System.Threading.Tasks.Task ShowErrorDialogAsync(string message)
|
||||
|
||||
Reference in New Issue
Block a user