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:
mika kuns
2026-06-09 09:34:37 +02:00
parent a41b8de47a
commit 763732a9b3
5 changed files with 201 additions and 102 deletions

View File

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