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:
@@ -62,14 +62,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
partial void OnIsNotesModeChanged(bool value) => OnPropertyChanged(nameof(IsTaskDetailVisible));
|
partial void OnIsNotesModeChanged(bool value) => OnPropertyChanged(nameof(IsTaskDetailVisible));
|
||||||
partial void OnIsPrepModeChanged(bool value) => OnPropertyChanged(nameof(IsTaskDetailVisible));
|
partial void OnIsPrepModeChanged(bool value) => OnPropertyChanged(nameof(IsTaskDetailVisible));
|
||||||
|
|
||||||
// Console maximize: green dot shrinks the description row to its MinHeight so
|
|
||||||
// the WorkConsole fills the rest. The row stays draggable and never overlaps.
|
|
||||||
// Applied in DetailsIslandView code-behind (RowDefinitions can't bind).
|
|
||||||
[ObservableProperty] private bool _isConsoleMaximized;
|
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private void ToggleConsoleMaximized() => IsConsoleMaximized = !IsConsoleMaximized;
|
|
||||||
|
|
||||||
public NotesEditorViewModel Notes { get; private set; } = null!;
|
public NotesEditorViewModel Notes { get; private set; } = null!;
|
||||||
|
|
||||||
// Current task row (set by IslandsShellViewModel via Bind)
|
// Current task row (set by IslandsShellViewModel via Bind)
|
||||||
@@ -161,14 +153,15 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
public bool ShowMergeSection =>
|
public bool ShowMergeSection =>
|
||||||
WorktreePath != null || Task?.IsPlanningParent == true || HasChildOutcomes;
|
WorktreePath != null || Task?.IsPlanningParent == true || HasChildOutcomes;
|
||||||
|
|
||||||
// Nothing to manage yet (idle/queued/running standalone): show a hint.
|
|
||||||
public bool ShowSessionEmpty =>
|
|
||||||
!IsWaitingForReview && !ShowMergeSection && !HasChildOutcomes;
|
|
||||||
|
|
||||||
private void NotifySessionSections()
|
private void NotifySessionSections()
|
||||||
{
|
{
|
||||||
|
OnPropertyChanged(nameof(HasChildOutcomes));
|
||||||
OnPropertyChanged(nameof(ShowMergeSection));
|
OnPropertyChanged(nameof(ShowMergeSection));
|
||||||
OnPropertyChanged(nameof(ShowSessionEmpty));
|
|
||||||
|
// The Session tab is only visible when it has outcomes; if it just
|
||||||
|
// emptied while selected, fall back to Output so the body isn't blank.
|
||||||
|
if (!HasChildOutcomes && SelectedTab == "session")
|
||||||
|
SelectedTab = "output";
|
||||||
}
|
}
|
||||||
|
|
||||||
public string TurnsText => $"{Turns}/{EffectiveMaxTurns}";
|
public string TurnsText => $"{Turns}/{EffectiveMaxTurns}";
|
||||||
@@ -184,6 +177,53 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
IsFailed ? "The session ended with an error." :
|
IsFailed ? "The session ended with an error." :
|
||||||
IsCancelled ? "The session was cancelled." : "";
|
IsCancelled ? "The session was cancelled." : "";
|
||||||
|
|
||||||
|
// The session's outcome summary — the task's Result minus any roadblock
|
||||||
|
// section (those get their own card), falling back to the run's
|
||||||
|
// ErrorMarkdown for hard failures. Shown once a run has finished.
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowSessionOutcome))]
|
||||||
|
private string? _sessionOutcome;
|
||||||
|
|
||||||
|
public bool ShowSessionOutcome =>
|
||||||
|
!string.IsNullOrWhiteSpace(SessionOutcome)
|
||||||
|
&& (IsWaitingForReview || IsDone || IsFailed || IsCancelled);
|
||||||
|
|
||||||
|
// The roadblocks the agent emitted (CLAUDEDO_BLOCKED), parsed out of the
|
||||||
|
// run result so they can surface as a distinct colored card.
|
||||||
|
[ObservableProperty]
|
||||||
|
[NotifyPropertyChangedFor(nameof(ShowRoadblockCard))]
|
||||||
|
private string? _roadblocks;
|
||||||
|
|
||||||
|
public bool ShowRoadblockCard =>
|
||||||
|
!string.IsNullOrWhiteSpace(Roadblocks)
|
||||||
|
&& (IsWaitingForReview || IsDone || IsFailed || IsCancelled);
|
||||||
|
|
||||||
|
// Worker writes roadblocks into the result under this header
|
||||||
|
// (TaskRunner.ComposeReviewResult). Split it back out for display.
|
||||||
|
private const string RoadblockMarker = "Roadblocks reported during the run:";
|
||||||
|
|
||||||
|
private void ApplyOutcome(string? result, string? errorFallback)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(result))
|
||||||
|
{
|
||||||
|
SessionOutcome = errorFallback;
|
||||||
|
Roadblocks = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var idx = result.IndexOf(RoadblockMarker, StringComparison.Ordinal);
|
||||||
|
if (idx < 0)
|
||||||
|
{
|
||||||
|
SessionOutcome = result;
|
||||||
|
Roadblocks = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var summary = result[..idx].TrimEnd().TrimEnd('⚠').TrimEnd();
|
||||||
|
SessionOutcome = string.IsNullOrWhiteSpace(summary) ? null : summary;
|
||||||
|
Roadblocks = result[(idx + RoadblockMarker.Length)..].Trim();
|
||||||
|
}
|
||||||
|
|
||||||
public string SessionLabel => "claude-session";
|
public string SessionLabel => "claude-session";
|
||||||
|
|
||||||
// Short task-id badge, e.g. "#T1A"
|
// Short task-id badge, e.g. "#T1A"
|
||||||
@@ -230,6 +270,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
OnPropertyChanged(nameof(IsAgentSectionEnabled));
|
OnPropertyChanged(nameof(IsAgentSectionEnabled));
|
||||||
OnPropertyChanged(nameof(ShowRoadblock));
|
OnPropertyChanged(nameof(ShowRoadblock));
|
||||||
OnPropertyChanged(nameof(RoadblockMessage));
|
OnPropertyChanged(nameof(RoadblockMessage));
|
||||||
|
OnPropertyChanged(nameof(ShowSessionOutcome));
|
||||||
|
OnPropertyChanged(nameof(ShowRoadblockCard));
|
||||||
NotifySessionSections();
|
NotifySessionSections();
|
||||||
}
|
}
|
||||||
[ObservableProperty] private string? _model;
|
[ObservableProperty] private string? _model;
|
||||||
@@ -364,9 +406,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
public bool ShowMergePreviewMuted =>
|
public bool ShowMergePreviewMuted =>
|
||||||
!MergeIsClean && !MergeIsConflict && !string.IsNullOrEmpty(MergePreviewText);
|
!MergeIsClean && !MergeIsConflict && !string.IsNullOrEmpty(MergePreviewText);
|
||||||
|
|
||||||
public bool ShowSingleMerge =>
|
|
||||||
WorktreePath != null && Task?.IsPlanningParent != true;
|
|
||||||
|
|
||||||
// Claude CLI stream-json parser + buffer for partial text deltas
|
// Claude CLI stream-json parser + buffer for partial text deltas
|
||||||
private readonly StreamLineFormatter _formatter = new();
|
private readonly StreamLineFormatter _formatter = new();
|
||||||
private readonly StringBuilder _claudeBuf = new();
|
private readonly StringBuilder _claudeBuf = new();
|
||||||
@@ -439,6 +478,21 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
catch { }
|
catch { }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Reload the session outcome (task Result incl. roadblocks, or the run's
|
||||||
|
// error) so it appears as soon as a run finishes.
|
||||||
|
private async System.Threading.Tasks.Task RefreshOutcomeAsync(string taskId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using var ctx = await _dbFactory.CreateDbContextAsync();
|
||||||
|
var entity = await ctx.Tasks.AsNoTracking().FirstOrDefaultAsync(t => t.Id == taskId);
|
||||||
|
var latestRun = await new TaskRunRepository(ctx).GetLatestByTaskIdAsync(taskId);
|
||||||
|
if (Task?.Id != taskId) return;
|
||||||
|
ApplyOutcome(entity?.Result, latestRun?.ErrorMarkdown);
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
public DetailsIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IWorkerClient worker, IServiceProvider services, INotesApi notesApi)
|
public DetailsIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IWorkerClient worker, IServiceProvider services, INotesApi notesApi)
|
||||||
{
|
{
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
@@ -492,6 +546,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
// Re-query to pick up worktree created during the run.
|
// Re-query to pick up worktree created during the run.
|
||||||
_ = RefreshWorktreeAsync(taskId);
|
_ = RefreshWorktreeAsync(taskId);
|
||||||
_ = RefreshChildOutcomeAsync(taskId);
|
_ = RefreshChildOutcomeAsync(taskId);
|
||||||
|
_ = RefreshOutcomeAsync(taskId);
|
||||||
};
|
};
|
||||||
|
|
||||||
_worker.WorktreeUpdatedEvent += taskId =>
|
_worker.WorktreeUpdatedEvent += taskId =>
|
||||||
@@ -755,6 +810,8 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
CanMergeAll = false;
|
CanMergeAll = false;
|
||||||
MergeAllDisabledReason = null;
|
MergeAllDisabledReason = null;
|
||||||
MergeAllError = null;
|
MergeAllError = null;
|
||||||
|
SessionOutcome = null;
|
||||||
|
Roadblocks = null;
|
||||||
_claudeBuf.Clear();
|
_claudeBuf.Clear();
|
||||||
|
|
||||||
if (row == null)
|
if (row == null)
|
||||||
@@ -824,6 +881,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
var latestRun = await runRepo.GetLatestByTaskIdAsync(row.Id, ct);
|
var latestRun = await runRepo.GetLatestByTaskIdAsync(row.Id, ct);
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
LatestRunSessionId = latestRun?.SessionId;
|
LatestRunSessionId = latestRun?.SessionId;
|
||||||
|
ApplyOutcome(entity.Result, latestRun?.ErrorMarkdown);
|
||||||
|
|
||||||
// Subscribe only after DB load confirms the task exists
|
// Subscribe only after DB load confirms the task exists
|
||||||
_subscribedTaskId = row.Id;
|
_subscribedTaskId = row.Id;
|
||||||
@@ -1170,34 +1228,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
MergePreviewText = text; MergeIsClean = clean; MergeIsConflict = conflict;
|
MergePreviewText = text; MergeIsClean = clean; MergeIsConflict = conflict;
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
|
||||||
private async System.Threading.Tasks.Task MergeAsync()
|
|
||||||
{
|
|
||||||
if (Task is null || WorktreePath is null || !_worker.IsConnected) return;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var result = await _worker.MergeTaskAsync(Task.Id, SelectedMergeTarget ?? "", false, "Merge task");
|
|
||||||
if (result.Status == "conflict")
|
|
||||||
{
|
|
||||||
if (RequestConflictResolution is not null)
|
|
||||||
{
|
|
||||||
await RequestConflictResolution(Task.Id, SelectedMergeTarget ?? "");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var (text, _, _) = MergePreviewPresenter.Describe(
|
|
||||||
new MergePreviewDto("conflict", result.ConflictFiles, 0));
|
|
||||||
MergePreviewText = text; MergeIsClean = false; MergeIsConflict = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
await RefreshMergePreviewAsync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch { /* broadcast reconciles */ }
|
|
||||||
}
|
|
||||||
|
|
||||||
[RelayCommand(CanExecute = nameof(CanOpenDiff))]
|
[RelayCommand(CanExecute = nameof(CanOpenDiff))]
|
||||||
private async System.Threading.Tasks.Task OpenDiffAsync()
|
private async System.Threading.Tasks.Task OpenDiffAsync()
|
||||||
{
|
{
|
||||||
@@ -1244,7 +1274,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
OpenDiffCommand.NotifyCanExecuteChanged();
|
OpenDiffCommand.NotifyCanExecuteChanged();
|
||||||
OpenWorktreeCommand.NotifyCanExecuteChanged();
|
OpenWorktreeCommand.NotifyCanExecuteChanged();
|
||||||
NotifySessionSections();
|
NotifySessionSections();
|
||||||
OnPropertyChanged(nameof(ShowSingleMerge));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
partial void OnTaskChanged(TaskRowViewModel? value) => NotifySessionSections();
|
partial void OnTaskChanged(TaskRowViewModel? value) => NotifySessionSections();
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
|
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
|
||||||
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
|
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
|
||||||
|
xmlns:loc="using:ClaudeDo.Ui.Localization"
|
||||||
x:Class="ClaudeDo.Ui.Views.Islands.Detail.DescriptionStepsCard"
|
x:Class="ClaudeDo.Ui.Views.Islands.Detail.DescriptionStepsCard"
|
||||||
x:DataType="vm:DetailsIslandViewModel">
|
x:DataType="vm:DetailsIslandViewModel">
|
||||||
|
|
||||||
@@ -21,7 +22,7 @@
|
|||||||
<Button Grid.Column="2"
|
<Button Grid.Column="2"
|
||||||
Classes="icon-btn"
|
Classes="icon-btn"
|
||||||
Margin="0,0,4,0"
|
Margin="0,0,4,0"
|
||||||
ToolTip.Tip="Copy formatted (title + description + open steps)"
|
ToolTip.Tip="{loc:Tr details.copyFormattedTip}"
|
||||||
Click="OnCopyClick">
|
Click="OnCopyClick">
|
||||||
<PathIcon Data="{StaticResource Icon.Copy}" Width="11" Height="11"/>
|
<PathIcon Data="{StaticResource Icon.Copy}" Width="11" Height="11"/>
|
||||||
</Button>
|
</Button>
|
||||||
@@ -30,6 +31,7 @@
|
|||||||
<Button Grid.Column="3"
|
<Button Grid.Column="3"
|
||||||
Classes="btn"
|
Classes="btn"
|
||||||
Padding="8,3"
|
Padding="8,3"
|
||||||
|
ToolTip.Tip="{loc:Tr details.toggleEditPreviewTip}"
|
||||||
Command="{Binding ToggleEditDescriptionCommand}">
|
Command="{Binding ToggleEditDescriptionCommand}">
|
||||||
<Panel>
|
<Panel>
|
||||||
<TextBlock Text="Preview" IsVisible="{Binding IsEditingDescription}"/>
|
<TextBlock Text="Preview" IsVisible="{Binding IsEditingDescription}"/>
|
||||||
@@ -40,7 +42,8 @@
|
|||||||
</Grid>
|
</Grid>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
<!-- Body -->
|
<!-- Body (scrolls inside the card so the card fills its row to the divider) -->
|
||||||
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
<StackPanel Margin="14" Spacing="10">
|
<StackPanel Margin="14" Spacing="10">
|
||||||
|
|
||||||
<!-- Description (always visible) -->
|
<!-- Description (always visible) -->
|
||||||
@@ -159,6 +162,7 @@
|
|||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
</ScrollViewer>
|
||||||
</DockPanel>
|
</DockPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|
||||||
|
|||||||
@@ -72,22 +72,12 @@
|
|||||||
<Grid DockPanel.Dock="Top" ColumnDefinitions="Auto,*,Auto"
|
<Grid DockPanel.Dock="Top" ColumnDefinitions="Auto,*,Auto"
|
||||||
Background="{DynamicResource Surface2Brush}" Height="28">
|
Background="{DynamicResource Surface2Brush}" Height="28">
|
||||||
|
|
||||||
<!-- Traffic-light dots; green toggles console maximize -->
|
<!-- Traffic-light dots (decorative) -->
|
||||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6"
|
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="6"
|
||||||
Margin="12,0" VerticalAlignment="Center">
|
Margin="12,0" VerticalAlignment="Center">
|
||||||
<Ellipse Classes="dot-red" />
|
<Ellipse Classes="dot-red" />
|
||||||
<Ellipse Classes="dot-yellow" />
|
<Ellipse Classes="dot-yellow" />
|
||||||
<Button Classes="dot-btn"
|
<Ellipse Classes="dot-green" />
|
||||||
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>
|
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Right cluster: info header (model · turns · diff) + status chip -->
|
<!-- Right cluster: info header (model · turns · diff) + status chip -->
|
||||||
@@ -183,6 +173,7 @@
|
|||||||
<Button Classes="tab-btn"
|
<Button Classes="tab-btn"
|
||||||
Classes.active="{Binding IsSessionTab}"
|
Classes.active="{Binding IsSessionTab}"
|
||||||
Content="Session"
|
Content="Session"
|
||||||
|
IsVisible="{Binding HasChildOutcomes}"
|
||||||
Command="{Binding SelectTabCommand}"
|
Command="{Binding SelectTabCommand}"
|
||||||
CommandParameter="session" />
|
CommandParameter="session" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
@@ -194,6 +185,26 @@
|
|||||||
<!-- Output: log + review footer, both gated on IsOutputTab -->
|
<!-- Output: log + review footer, both gated on IsOutputTab -->
|
||||||
<DockPanel IsVisible="{Binding IsOutputTab}" LastChildFill="True">
|
<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;
|
<!-- 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. -->
|
only while awaiting review. No border/fill so it reads as part of the log. -->
|
||||||
<Grid DockPanel.Dock="Bottom"
|
<Grid DockPanel.Dock="Bottom"
|
||||||
@@ -222,8 +233,10 @@
|
|||||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="10"
|
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="10"
|
||||||
VerticalAlignment="Top" Margin="12,2,0,0">
|
VerticalAlignment="Top" Margin="12,2,0,0">
|
||||||
<Button Classes="prompt-action accent" Content="[Continue]"
|
<Button Classes="prompt-action accent" Content="[Continue]"
|
||||||
|
ToolTip.Tip="{loc:Tr session.reviewContinueTip}"
|
||||||
Command="{Binding RejectReviewCommand}" />
|
Command="{Binding RejectReviewCommand}" />
|
||||||
<Button Classes="prompt-action" Content="[Reset]"
|
<Button Classes="prompt-action" Content="[Reset]"
|
||||||
|
ToolTip.Tip="{loc:Tr session.reviewResetTip}"
|
||||||
Command="{Binding ResetReviewCommand}" />
|
Command="{Binding ResetReviewCommand}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Grid>
|
</Grid>
|
||||||
@@ -275,19 +288,15 @@
|
|||||||
IsVisible="{Binding ShowMergePreviewMuted}" />
|
IsVisible="{Binding ShowMergePreviewMuted}" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Primary action: Approve flows straight into the merge.
|
<!-- Primary action: Approve flows straight into the merge. -->
|
||||||
Approve is the review-gated path; the plain Merge button covers
|
|
||||||
already-reviewed / kept worktrees. -->
|
|
||||||
<WrapPanel Orientation="Horizontal">
|
<WrapPanel Orientation="Horizontal">
|
||||||
<Button Classes="btn accent" Content="Approve & Merge" Margin="0,0,8,8"
|
<Button Classes="btn accent" Content="Approve & Merge" Margin="0,0,8,8"
|
||||||
Command="{Binding ApproveReviewCommand}"
|
Command="{Binding ApproveReviewCommand}"
|
||||||
IsVisible="{Binding IsWaitingForReview}" />
|
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"
|
<Button Classes="btn" Content="Open Diff" Margin="0,0,8,8"
|
||||||
Command="{Binding OpenDiffCommand}" />
|
Command="{Binding OpenDiffCommand}" />
|
||||||
<Button Classes="btn" Margin="0,0,8,8"
|
<Button Classes="btn" Margin="0,0,8,8"
|
||||||
|
ToolTip.Tip="{loc:Tr agent.openWorktreeTip}"
|
||||||
Command="{Binding OpenWorktreeCommand}">
|
Command="{Binding OpenWorktreeCommand}">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="5">
|
<StackPanel Orientation="Horizontal" Spacing="5">
|
||||||
<TextBlock Text="Worktree" />
|
<TextBlock Text="Worktree" />
|
||||||
@@ -336,13 +345,6 @@
|
|||||||
</ItemsControl>
|
</ItemsControl>
|
||||||
</StackPanel>
|
</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>
|
</StackPanel>
|
||||||
</ScrollViewer>
|
</ScrollViewer>
|
||||||
|
|
||||||
|
|||||||
@@ -43,24 +43,55 @@
|
|||||||
IsVisible="{Binding IsTaskDetailVisible}"
|
IsVisible="{Binding IsTaskDetailVisible}"
|
||||||
Margin="14,12,14,12">
|
Margin="14,12,14,12">
|
||||||
<Grid.RowDefinitions>
|
<Grid.RowDefinitions>
|
||||||
<!-- MinHeight keeps the description visible: the console can never
|
<!-- Auto: the description sizes to its content so the console takes
|
||||||
overlap it, whether maximized (code-behind) or dragged. -->
|
every spare pixel when it's short. Row limits are proportional
|
||||||
<RowDefinition Height="2*" MinHeight="90"/>
|
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="*"/>
|
<RowDefinition Height="*"/>
|
||||||
</Grid.RowDefinitions>
|
</Grid.RowDefinitions>
|
||||||
<ScrollViewer Grid.Row="0" VerticalScrollBarVisibility="Auto">
|
<detail:DescriptionStepsCard x:Name="DescriptionCard" Grid.Row="0"/>
|
||||||
<detail:DescriptionStepsCard VerticalAlignment="Top"/>
|
|
||||||
</ScrollViewer>
|
<!-- Console row also hosts the roadblock card (docked above the console)
|
||||||
<detail:WorkConsole Grid.Row="1" Margin="0,10,0,0"/>
|
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
|
<!-- Resize by dragging the console's top edge — a transparent splitter
|
||||||
over the gap above the console; no standalone separator bar.
|
over the gap above the console; no standalone separator bar.
|
||||||
Stays draggable while maximized. -->
|
Stays draggable while maximized. -->
|
||||||
<GridSplitter Grid.Row="1"
|
<GridSplitter x:Name="DetailSplitter" Grid.Row="1"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
Height="10"
|
Height="10"
|
||||||
HorizontalAlignment="Stretch"
|
HorizontalAlignment="Stretch"
|
||||||
ResizeDirection="Rows"
|
ResizeDirection="Rows"
|
||||||
Background="Transparent"/>
|
Background="Transparent"
|
||||||
|
DragStarted="OnSplitterDragStarted"
|
||||||
|
DragCompleted="OnSplitterDragCompleted"/>
|
||||||
</Grid>
|
</Grid>
|
||||||
|
|
||||||
<!-- Notes mode -->
|
<!-- Notes mode -->
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
using System.ComponentModel;
|
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
using Avalonia.Layout;
|
using Avalonia.Layout;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
|
using Avalonia.Reactive;
|
||||||
using ClaudeDo.Ui.ViewModels.Islands;
|
using ClaudeDo.Ui.ViewModels.Islands;
|
||||||
using ClaudeDo.Ui.Views.Modals;
|
using ClaudeDo.Ui.Views.Modals;
|
||||||
using ClaudeDo.Ui.Views.Planning;
|
using ClaudeDo.Ui.Views.Planning;
|
||||||
@@ -11,27 +12,42 @@ namespace ClaudeDo.Ui.Views.Islands;
|
|||||||
|
|
||||||
public partial class DetailsIslandView : UserControl
|
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()
|
public DetailsIslandView()
|
||||||
{
|
{
|
||||||
InitializeComponent();
|
InitializeComponent();
|
||||||
DataContextChanged += OnDataContextChanged;
|
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)
|
private void OnDataContextChanged(object? sender, EventArgs e)
|
||||||
{
|
{
|
||||||
if (_subscribedVm is not null)
|
if (_vm != null)
|
||||||
_subscribedVm.PropertyChanged -= OnVmPropertyChanged;
|
_vm.PropertyChanged -= OnViewModelPropertyChanged;
|
||||||
_subscribedVm = DataContext as DetailsIslandViewModel;
|
|
||||||
if (_subscribedVm is not null)
|
|
||||||
{
|
|
||||||
_subscribedVm.PropertyChanged += OnVmPropertyChanged;
|
|
||||||
ApplyConsoleMaximized(_subscribedVm.IsConsoleMaximized);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (DataContext is DetailsIslandViewModel vm)
|
if (DataContext is DetailsIslandViewModel vm)
|
||||||
{
|
{
|
||||||
|
_vm = vm;
|
||||||
|
vm.PropertyChanged += OnViewModelPropertyChanged;
|
||||||
|
ApplyResizeStateForCurrentTask();
|
||||||
|
|
||||||
vm.ShowDiffModal = async (diffVm) =>
|
vm.ShowDiffModal = async (diffVm) =>
|
||||||
{
|
{
|
||||||
var owner = TopLevel.GetTopLevel(this) as Window;
|
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)
|
if (e.PropertyName == nameof(DetailsIslandViewModel.Task))
|
||||||
&& sender is DetailsIslandViewModel vm)
|
ApplyResizeStateForCurrentTask();
|
||||||
ApplyConsoleMaximized(vm.IsConsoleMaximized);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Maximized: shrink the description row to its MinHeight (the console fills
|
private void ApplyResizeStateForCurrentTask()
|
||||||
// the rest). Restored: back to the 2:1 default. The GridSplitter keeps both
|
{
|
||||||
// states draggable; MinHeight stops the console from ever covering it.
|
// A task dragged before keeps its pixel height (clamped by the row's
|
||||||
private void ApplyConsoleMaximized(bool maximized)
|
// 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];
|
var descRow = DetailBodyGrid.RowDefinitions[0];
|
||||||
descRow.Height = maximized
|
if (descRow.Height.IsAuto)
|
||||||
? new GridLength(descRow.MinHeight, GridUnitType.Pixel)
|
descRow.Height = new GridLength(DescriptionCard.Bounds.Height, GridUnitType.Pixel);
|
||||||
: new GridLength(2, GridUnitType.Star);
|
}
|
||||||
|
|
||||||
|
// 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)
|
private async System.Threading.Tasks.Task ShowErrorDialogAsync(string message)
|
||||||
|
|||||||
Reference in New Issue
Block a user