Files
ClaudeDo/src/ClaudeDo.Ui/Views/Islands/Detail/WorkConsole.axaml
T

521 lines
29 KiB
XML

<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Islands"
xmlns:loc="using:ClaudeDo.Ui.Localization"
xmlns:controls="using:ClaudeDo.Ui.Views.Controls"
x:DataType="vm:DetailsIslandViewModel"
x:Class="ClaudeDo.Ui.Views.Islands.Detail.WorkConsole">
<UserControl.Styles>
<Style Selector="Button.tab-btn">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="12,8" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeMono}" />
<Setter Property="Foreground" Value="{StaticResource TextMuteBrush}" />
<Setter Property="CornerRadius" Value="0" />
</Style>
<Style Selector="Button.tab-btn:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource Surface2Brush}" />
<Setter Property="TextElement.Foreground" Value="{StaticResource TextDimBrush}" />
</Style>
<Style Selector="Button.tab-btn.active /template/ ContentPresenter">
<Setter Property="Background" Value="{StaticResource Surface3Brush}" />
<Setter Property="TextElement.Foreground" Value="{StaticResource AccentBrush}" />
</Style>
<!-- Traffic-light dot button: no chrome, just the ellipse -->
<Style Selector="Button.dot-btn">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="0" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="Cursor" Value="Hand" />
</Style>
<Style Selector="Button.dot-btn /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
</Style>
<!-- Terminal prompt action: bracketed text, no button chrome -->
<Style Selector="Button.prompt-action">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="2,0" />
<Setter Property="CornerRadius" Value="0" />
<Setter Property="Cursor" Value="Hand" />
<Setter Property="FontFamily" Value="{StaticResource MonoFont}" />
<Setter Property="FontSize" Value="{StaticResource FontSizeMono}" />
<Setter Property="Foreground" Value="{StaticResource TextMuteBrush}" />
</Style>
<Style Selector="Button.prompt-action /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="TextElement.Foreground" Value="{StaticResource TextMuteBrush}" />
</Style>
<Style Selector="Button.prompt-action:pointerover /template/ ContentPresenter">
<Setter Property="Background" Value="Transparent" />
<Setter Property="TextElement.Foreground" Value="{StaticResource TextBrush}" />
</Style>
<Style Selector="Button.prompt-action.accent /template/ ContentPresenter">
<Setter Property="TextElement.Foreground" Value="{StaticResource AccentBrush}" />
</Style>
<Style Selector="Button.prompt-action.accent:pointerover /template/ ContentPresenter">
<Setter Property="TextElement.Foreground" Value="{StaticResource MossBrightBrush}" />
</Style>
<!-- Review prompt input: blends into the terminal — no border/fill highlight in any state -->
<Style Selector="TextBox.review-prompt">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="0" />
</Style>
<Style Selector="TextBox.review-prompt /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="BoxShadow" Value="none" />
</Style>
<Style Selector="TextBox.review-prompt:pointerover /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
</Style>
<Style Selector="TextBox.review-prompt:focus /template/ Border#PART_BorderElement">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderBrush" Value="Transparent" />
<Setter Property="BoxShadow" Value="none" />
</Style>
</UserControl.Styles>
<!-- Outer terminal card — Padding="0" so header/strip span edge-to-edge;
ClipToBounds keeps tab content inside the rounded corners (no bottom clip). -->
<Border Classes="terminal" Padding="0" ClipToBounds="True">
<DockPanel LastChildFill="True">
<!-- ── Title bar ── -->
<Grid DockPanel.Dock="Top" ColumnDefinitions="Auto,*,Auto"
Background="{DynamicResource Surface2Brush}" Height="28">
<!-- 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" />
<Ellipse Classes="dot-green" />
</StackPanel>
<!-- Right cluster: info header (model · turns · diff) + status chip -->
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="12"
Margin="0,0,8,0" VerticalAlignment="Center">
<StackPanel Orientation="Horizontal" Spacing="6" VerticalAlignment="Center">
<TextBlock Classes="meta" Text="{Binding Model}"
Foreground="{DynamicResource TextMuteBrush}" />
<TextBlock Classes="meta" Text="·"
Foreground="{DynamicResource TextFaintBrush}" />
<TextBlock Classes="meta" Text="{Binding TurnsText}"
Foreground="{DynamicResource TextMuteBrush}" />
<TextBlock Classes="meta" Text="·"
Foreground="{DynamicResource TextFaintBrush}" />
<TextBlock Classes="meta" Text="{Binding TokensFormatted}"
Foreground="{DynamicResource TextMuteBrush}"
ToolTip.Tip="{Binding TokensBreakdown}" />
<TextBlock Classes="meta" Text="·"
Foreground="{DynamicResource TextFaintBrush}" />
<TextBlock Classes="diff-add" Text="{Binding DiffAddText}" />
<TextBlock Classes="diff-del" Text="{Binding DiffDelText}" />
</StackPanel>
<Panel VerticalAlignment="Center">
<Border Classes="live-chip pulsing"
IsVisible="{Binding IsRunning}">
<StackPanel Orientation="Horizontal" Spacing="5" VerticalAlignment="Center">
<Ellipse VerticalAlignment="Center" />
<TextBlock Text="{loc:Tr session.chipLive}" VerticalAlignment="Center" />
</StackPanel>
</Border>
<Border Classes="live-chip done"
IsVisible="{Binding IsDone}">
<StackPanel Orientation="Horizontal" Spacing="5" VerticalAlignment="Center">
<Ellipse VerticalAlignment="Center" Fill="{DynamicResource MossBrush}" />
<TextBlock Text="{loc:Tr session.chipDone}" VerticalAlignment="Center"
Foreground="{DynamicResource MossBrush}" />
</StackPanel>
</Border>
<Border Classes="live-chip failed"
IsVisible="{Binding IsFailed}">
<StackPanel Orientation="Horizontal" Spacing="5" VerticalAlignment="Center">
<Ellipse VerticalAlignment="Center" Fill="{DynamicResource BloodBrush}" />
<TextBlock Text="{loc:Tr session.chipFailed}" VerticalAlignment="Center"
Foreground="{DynamicResource BloodBrush}" />
</StackPanel>
</Border>
</Panel>
</StackPanel>
</Grid>
<!-- ── Roadblock band ── -->
<Border DockPanel.Dock="Top"
IsVisible="{Binding ShowRoadblock}"
Background="{DynamicResource ErrorTintBrush}"
BorderBrush="{DynamicResource BloodBrush}"
BorderThickness="0,1"
Padding="14,8">
<StackPanel Spacing="6">
<StackPanel Orientation="Horizontal" Spacing="8">
<PathIcon Data="{Binding Source='mdi:AlertOutline', Converter={StaticResource IconKey}}"
Foreground="{DynamicResource BloodBrush}"
Width="14" Height="14" VerticalAlignment="Center" />
<TextBlock Classes="meta" Text="{Binding RoadblockMessage}"
Foreground="{DynamicResource BloodBrush}"
TextWrapping="Wrap" VerticalAlignment="Center" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="8">
<Button Classes="btn accent" Content="{loc:Tr details.workConsole.continue}"
Command="{Binding ContinueCommand}"
IsVisible="{Binding ShowContinue}"
ToolTip.Tip="{loc:Tr session.continueNoSessionTip}"
ToolTip.ShowOnDisabled="True" />
<Button Classes="btn" Content="{loc:Tr details.workConsole.resetAndRetry}"
Command="{Binding ResetAndRetryCommand}"
IsVisible="{Binding ShowResetAndRetry}"
ToolTip.Tip="{loc:Tr session.resetAndRetryInteractiveTip}"
ToolTip.ShowOnDisabled="True" />
</StackPanel>
</StackPanel>
</Border>
<!-- ── Tab strip ── -->
<Border DockPanel.Dock="Top"
Background="{DynamicResource Surface2Brush}"
BorderBrush="{DynamicResource LineBrush}"
BorderThickness="0,0,0,1">
<StackPanel Orientation="Horizontal">
<Button Classes="tab-btn"
Classes.active="{Binding IsOutputTab}"
Content="{loc:Tr details.workConsole.tabOutput}"
Command="{Binding SelectTabCommand}"
CommandParameter="output" />
<Button Classes="tab-btn"
Classes.active="{Binding IsGitTab}"
Command="{Binding SelectTabCommand}"
CommandParameter="git">
<StackPanel Orientation="Horizontal" Spacing="6">
<TextBlock Text="{loc:Tr details.workConsole.tabGit}" VerticalAlignment="Center" />
<!-- Review-pending dot: where to act when a task awaits review -->
<Ellipse Width="6" Height="6" VerticalAlignment="Center"
Fill="{DynamicResource AccentBrush}"
IsVisible="{Binding IsWaitingForReview}" />
</StackPanel>
</Button>
<Button Classes="tab-btn"
Classes.active="{Binding IsSessionTab}"
Content="{loc:Tr details.workConsole.tabSession}"
IsVisible="{Binding HasChildOutcomes}"
Command="{Binding SelectTabCommand}"
CommandParameter="session" />
</StackPanel>
</Border>
<!-- ── Tab body (bottom inset keeps content clear of the rounded corner) ── -->
<Grid Margin="0,0,0,8">
<!-- 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="{loc:Tr details.workConsole.outcomeLabel}" />
<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"
IsVisible="{Binding IsWaitingForReview}"
ColumnDefinitions="Auto,*,Auto"
Margin="12,2,12,8">
<TextBlock Grid.Column="0" Text="&#x276F;"
FontFamily="{StaticResource MonoFont}"
FontSize="{StaticResource FontSizeMono}"
Foreground="{DynamicResource AccentBrush}"
VerticalAlignment="Top" Margin="0,2,8,0" />
<TextBox Grid.Column="1"
Name="ReviewInput"
Classes="review-prompt"
KeyDown="OnReviewInputKeyDown"
Text="{Binding ReviewFeedback, Mode=TwoWay}"
AcceptsReturn="True"
TextWrapping="Wrap"
MaxHeight="160"
PlaceholderText="{loc:Tr session.reviewFeedbackPlaceholder}"
VerticalContentAlignment="Top"
ScrollViewer.VerticalScrollBarVisibility="Auto"
FontFamily="{StaticResource MonoFont}"
FontSize="{StaticResource FontSizeMono}" />
<StackPanel Grid.Column="2" Spacing="4" Margin="12,2,0,0">
<Button Classes="prompt-action accent" Content="[Resume]"
HorizontalAlignment="Right"
ToolTip.Tip="{loc:Tr session.reviewContinueTip}"
Command="{Binding RejectReviewCommand}" />
<controls:OperationIndicator Status="{Binding RejectOp}" HorizontalAlignment="Right" />
</StackPanel>
</Grid>
<!-- Inset lives on the content, NOT on the ScrollViewer: Avalonia 12 leaves
ScrollViewer.Padding out of the Extent, so at max scroll the last lines
sit below the viewport and stay unreachable. -->
<ScrollViewer Name="LogScroll"
VerticalScrollBarVisibility="Visible"
AllowAutoHide="False">
<ItemsControl ItemsSource="{Binding Log}" Margin="12,8,12,4">
<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="{Binding Kind, Converter={StaticResource LogKindForeground}}"
TextWrapping="Wrap" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</ScrollViewer>
</DockPanel>
<!-- Git: the review + merge cockpit -->
<ScrollViewer IsVisible="{Binding IsGitTab}">
<StackPanel Spacing="14" Margin="14,10">
<!-- Merge controls — shown whenever there's a worktree / unit to merge.
Header reads REVIEW while a decision is pending, otherwise MERGE. -->
<StackPanel Spacing="14" IsVisible="{Binding Merge.ShowMergeSection}">
<TextBlock Classes="section-label" Text="{loc:Tr details.workConsole.reviewLabel}"
IsVisible="{Binding IsWaitingForReview}" />
<TextBlock Classes="section-label" Text="{loc:Tr details.workConsole.mergeLabel}"
IsVisible="{Binding !IsWaitingForReview}" />
<!-- Change summary (review only) -->
<StackPanel Orientation="Horizontal" Spacing="6"
IsVisible="{Binding IsWaitingForReview}">
<TextBlock Classes="diff-add" Text="{Binding DiffAddText}" />
<TextBlock Classes="diff-del" Text="{Binding DiffDelText}" />
</StackPanel>
<!-- Company repo: no local target — approve pushes and opens a draft PR. -->
<StackPanel Spacing="6" IsVisible="{Binding Merge.IsCompanyRepo}">
<TextBlock Classes="meta" Text="{loc:Tr details.workConsole.companyTargetHint}" TextWrapping="Wrap" />
<Button Classes="btn" HorizontalAlignment="Left"
Content="{loc:Tr details.workConsole.openPr}"
ToolTip.Tip="{Binding Merge.PullRequestUrl}"
Command="{Binding Merge.OpenPullRequestCommand}"
IsVisible="{Binding Merge.HasPullRequest}" />
</StackPanel>
<!-- Target branch + pre-flight mergeability -->
<StackPanel Spacing="4" IsVisible="{Binding Merge.ShowApproveMerge}">
<TextBlock Classes="field-label" Text="{loc:Tr details.workConsole.targetBranch}" />
<ComboBox ItemsSource="{Binding Merge.MergeTargetBranches}"
SelectedItem="{Binding Merge.SelectedMergeTarget, Mode=TwoWay}"
HorizontalAlignment="Stretch" />
<TextBlock Classes="meta" Text="{loc:Tr details.workConsole.targetDirty}"
TextWrapping="Wrap"
Foreground="{DynamicResource AmberBrush}"
IsVisible="{Binding Merge.TargetDirty}" />
</StackPanel>
<!-- The preview is against the local target, meaningless when the PR targets origin. -->
<StackPanel Spacing="0" IsVisible="{Binding Merge.ShowApproveMerge}">
<TextBlock Classes="meta" Text="{Binding Merge.MergePreviewText}" TextWrapping="Wrap"
Foreground="{DynamicResource MossBrush}"
IsVisible="{Binding Merge.MergeIsClean}" />
<TextBlock Classes="meta" Text="{Binding Merge.MergePreviewText}" TextWrapping="Wrap"
Foreground="{DynamicResource BloodBrush}"
IsVisible="{Binding Merge.MergeIsConflict}" />
<TextBlock Classes="meta" Text="{Binding Merge.MergePreviewText}" TextWrapping="Wrap"
Foreground="{DynamicResource TextMuteBrush}"
IsVisible="{Binding Merge.ShowMergePreviewMuted}" />
<controls:OperationIndicator Status="{Binding Merge.PreviewOp}" />
</StackPanel>
<!-- Inspect: diff / worktree / combined diff -->
<WrapPanel Orientation="Horizontal">
<Button Classes="btn" Content="{loc:Tr details.workConsole.openDiff}" Margin="0,0,8,8"
ToolTip.Tip="{loc:Tr agent.openDiffTip}"
ToolTip.ShowOnDisabled="True"
Command="{Binding Merge.OpenDiffCommand}" />
<Button Classes="btn" Margin="0,0,8,8"
ToolTip.Tip="{loc:Tr agent.openWorktreeTip}"
ToolTip.ShowOnDisabled="True"
Command="{Binding Merge.OpenWorktreeCommand}">
<StackPanel Orientation="Horizontal" Spacing="5">
<TextBlock Text="{loc:Tr details.workConsole.worktree}" />
<PathIcon Data="{Binding Source='mdi:OpenInNew', Converter={StaticResource IconKey}}" Width="11" Height="11" />
</StackPanel>
</Button>
<Button Classes="btn" Content="{loc:Tr details.workConsole.reviewCombinedDiff}" Margin="0,0,8,8"
ToolTip.Tip="{loc:Tr agent.reviewCombinedDiffTip}"
ToolTip.ShowOnDisabled="True"
Command="{Binding Merge.ReviewCombinedDiffCommand}" />
</WrapPanel>
</StackPanel>
<!-- Review decision — the merge verbs. Feedback + Resume session moved to the
Output tab. Present while awaiting review, even for sandbox runs. -->
<StackPanel Spacing="10" IsVisible="{Binding IsWaitingForReview}">
<Border Height="1" Background="{DynamicResource LineBrush}"
IsVisible="{Binding Merge.ShowMergeSection}" />
<!-- Gate: you must open the diff before Approve & Merge unlocks. -->
<StackPanel Spacing="2" IsVisible="{Binding ShowReviewDiffHint}">
<StackPanel Orientation="Horizontal" Spacing="6">
<PathIcon Data="{Binding Source='mdi:OpenInNew', Converter={StaticResource IconKey}}" Width="11" Height="11"
Foreground="{DynamicResource AmberBrush}" VerticalAlignment="Center" />
<TextBlock Classes="meta" VerticalAlignment="Center"
Foreground="{DynamicResource AmberBrush}"
Text="{loc:Tr details.workConsole.openDiffHint}" />
</StackPanel>
<TextBlock Classes="meta" Margin="17,0,0,0"
Text="{loc:Tr session.approveMergesUnitHint}" />
</StackPanel>
<!-- Gate: a unit merge is draining this task's subtasks, so Cancel is locked
until it finishes (mirrors the worker-side guard). -->
<StackPanel Orientation="Horizontal" Spacing="6"
IsVisible="{Binding ShowMergeDrainHint}">
<PathIcon Data="{Binding Source='mdi:AlertOutline', Converter={StaticResource IconKey}}" Width="11" Height="11"
Foreground="{DynamicResource AmberBrush}" VerticalAlignment="Center" />
<TextBlock Classes="meta" VerticalAlignment="Center"
Foreground="{DynamicResource AmberBrush}"
Text="{loc:Tr session.mergeDrainHint}" />
</StackPanel>
<WrapPanel Orientation="Horizontal">
<StackPanel Orientation="Horizontal" Spacing="{StaticResource SpaceSm}" Margin="0,0,8,8">
<Button Classes="btn accent" Command="{Binding ApproveReviewCommand}">
<Panel>
<TextBlock Text="{loc:Tr details.workConsole.approveMerge}" IsVisible="{Binding Merge.ShowApproveMerge}" />
<TextBlock Text="{loc:Tr details.workConsole.createDraftPr}" IsVisible="{Binding Merge.ShowCreatePr}" />
<TextBlock Text="{loc:Tr details.workConsole.updatePr}" IsVisible="{Binding Merge.ShowUpdatePr}" />
</Panel>
</Button>
<controls:OperationIndicator Status="{Binding ApproveOp}" />
</StackPanel>
<StackPanel Orientation="Horizontal" Spacing="{StaticResource SpaceSm}" Margin="0,0,8,8">
<Button Classes="btn" Content="{loc:Tr details.workConsole.park}"
ToolTip.Tip="{loc:Tr details.workConsole.parkTip}"
Command="{Binding ParkReviewCommand}" />
<controls:OperationIndicator Status="{Binding ParkOp}" />
</StackPanel>
<Button Classes="btn" Content="{loc:Tr details.workConsole.cancel}" Margin="0,0,8,8"
ToolTip.Tip="{loc:Tr session.mergeDrainHint}"
ToolTip.ShowOnDisabled="True"
Command="{Binding CancelReviewCommand}" />
</WrapPanel>
<Button Classes="prompt-action" Content="{loc:Tr details.workConsole.resetDiscardBranch}"
ToolTip.Tip="{loc:Tr session.reviewResetTip}"
Command="{Binding ResetReviewCommand}" />
</StackPanel>
<!-- Send an Idle task to the queue for Claude to pick up on the next run. -->
<StackPanel Spacing="8">
<Border Height="1" Background="{DynamicResource LineBrush}" />
<StackPanel Orientation="Horizontal" Spacing="{StaticResource SpaceSm}">
<Button Classes="btn" Content="{loc:Tr details.sendToQueue}"
ToolTip.Tip="{loc:Tr details.sendToQueueTip}"
ToolTip.ShowOnDisabled="True"
Command="{Binding EnqueueCommand}" />
<!-- Parked (Idle + Active worktree): clean reset, discards worktree and branch. -->
<Button Classes="btn" Content="{loc:Tr details.workConsole.resetDiscardBranch}"
ToolTip.Tip="{loc:Tr session.reviewResetTip}"
IsVisible="{Binding Task.IsParked, FallbackValue=False}"
Command="{Binding ResetReviewCommand}" />
</StackPanel>
</StackPanel>
<!-- Submit an interactive (ConPTY) session for review: an Idle/Failed task that
still has a worktree. Commits the worktree, then moves it to WaitingForReview. -->
<StackPanel Spacing="8" IsVisible="{Binding CanSubmitForReview}">
<Border Height="1" Background="{DynamicResource LineBrush}" />
<TextBlock Classes="meta" TextWrapping="Wrap"
Foreground="{DynamicResource TextMuteBrush}"
Text="{loc:Tr details.workConsole.submitForReviewHint}" />
<StackPanel Orientation="Horizontal" Spacing="{StaticResource SpaceSm}">
<Button Classes="btn accent" Content="{loc:Tr details.workConsole.submitForReview}" HorizontalAlignment="Left"
Command="{Binding SubmitForReviewCommand}" />
<controls:OperationIndicator Status="{Binding SubmitOp}" />
</StackPanel>
</StackPanel>
</StackPanel>
</ScrollViewer>
<!-- Session: subtask outcomes (review lives in Output, merge in Git) -->
<ScrollViewer IsVisible="{Binding IsSessionTab}">
<StackPanel Spacing="14" Margin="14,10">
<!-- Attention band: a child failed, was cancelled, still needs its own
review, or reported roadblocks. The parent stays waiting until resolved. -->
<Border IsVisible="{Binding HasChildrenNeedingAttention}"
Background="{DynamicResource ErrorTintBrush}"
BorderBrush="{DynamicResource BloodBrush}"
BorderThickness="1" CornerRadius="8" Padding="10,8">
<StackPanel Orientation="Horizontal" Spacing="8">
<PathIcon Data="{Binding Source='mdi:AlertOutline', Converter={StaticResource IconKey}}"
Foreground="{DynamicResource BloodBrush}"
Width="14" Height="14" VerticalAlignment="Center" />
<TextBlock Classes="meta" Text="{Binding ChildrenAttentionText}"
Foreground="{DynamicResource BloodBrush}"
VerticalAlignment="Center" />
</StackPanel>
</Border>
<!-- Child outcomes -->
<StackPanel Spacing="6" IsVisible="{Binding HasChildOutcomes}">
<TextBlock Classes="section-label" Text="{loc:Tr details.workConsole.outcomesLabel}" />
<ItemsControl ItemsSource="{Binding ChildOutcomes}">
<ItemsControl.ItemTemplate>
<DataTemplate x:DataType="vm:ChildOutcomeRowViewModel">
<Grid ColumnDefinitions="*,Auto,Auto" Margin="0,2">
<TextBlock Grid.Column="0" Text="{Binding Title}"
TextTrimming="CharacterEllipsis"
VerticalAlignment="Center" />
<TextBlock Grid.Column="1" Text="{Binding RoadblockText}"
IsVisible="{Binding HasRoadblock}"
Foreground="#E0A030"
Margin="8,0" VerticalAlignment="Center" />
<TextBlock Grid.Column="2" Text="{Binding StatusLabel}"
Opacity="0.75" VerticalAlignment="Center" />
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</StackPanel>
</ScrollViewer>
</Grid>
</DockPanel>
</Border>
</UserControl>