feat(ui): dirty Ziel-Verzeichnis beim Oeffnen des Merge-Dialogs melden statt nach dem Fehlschlag

MergeTargetsDto/MergeTargets bekommen TargetDirty (via HasChangesAsync,
includeUntracked: false). Das Merge-Modal zeigt Warnung + Stash-Retry
sofort in InitializeAsync statt erst nach dem dirty_target-Fehlschlag;
der REVIEW-Block in der Detail-Pane zeigt denselben Hinweis neben
Approve. Der dirty_target-Rueckgabepfad bleibt als Absicherung bestehen.

Behebt Befund F6 aus docs/test-session-2026-09-16.md.
This commit is contained in:
Mika Kuns
2026-09-16 15:43:39 +02:00
committed by mika kuns
parent 86cc309ba9
commit 09bcd696dd
12 changed files with 92 additions and 6 deletions
@@ -726,6 +726,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
Merge.MergeTargetBranches.Clear();
foreach (var b in targets.LocalBranches) Merge.MergeTargetBranches.Add(b);
Merge.SelectedMergeTarget = targets.DefaultBranch;
Merge.TargetDirty = targets.TargetDirty;
}
}
await Merge.RefreshMergePreviewAsync();
@@ -776,6 +777,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
foreach (var b in targets.LocalBranches)
Merge.MergeTargetBranches.Add(b);
Merge.SelectedMergeTarget = targets.DefaultBranch;
Merge.TargetDirty = targets.TargetDirty;
}
}
}
@@ -822,6 +824,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
foreach (var b in targets.LocalBranches)
Merge.MergeTargetBranches.Add(b);
Merge.SelectedMergeTarget = targets.DefaultBranch;
Merge.TargetDirty = targets.TargetDirty;
}
}
}
@@ -28,6 +28,10 @@ public sealed partial class MergeSectionViewModel : ViewModelBase
[ObservableProperty] private ObservableCollection<string> _mergeTargetBranches = new();
[ObservableProperty] private string? _selectedMergeTarget;
// Mirrors MergeTargetsDto.TargetDirty — surfaced next to Approve so a dirty target working
// dir is visible before anyone clicks, not just after the merge modal refuses it.
[ObservableProperty] private bool _targetDirty;
// No CanExecute to gate here — RefreshMergePreviewAsync is triggered from the Bind path,
// not a [RelayCommand], so PreviewOp only drives the indicator, never locks a button.
public OperationStatus PreviewOp { get; } = new();
@@ -141,6 +145,7 @@ public sealed partial class MergeSectionViewModel : ViewModelBase
MergePreviewText = "";
MergeIsClean = false;
MergeIsConflict = false;
TargetDirty = false;
SyncWorktree(null, null, null, null, null);
SyncTaskContext(null, null, false);
SyncChildOutcomes(false, 0);
@@ -76,6 +76,14 @@ public sealed partial class MergeModalViewModel : ViewModelBase
SelectedBranch = Branches.Contains(targets.DefaultBranch)
? targets.DefaultBranch
: Branches.FirstOrDefault();
// Known dirty before anyone clicks Merge — offer the stash retry right away instead
// of making the user hit the "dirty_target" refusal first.
if (targets.TargetDirty)
{
WarningMessage = Loc.T("vm.merge.targetDirty");
ShowStashRetry = true;
}
}
catch (Exception ex)
{
@@ -327,6 +327,10 @@
<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>
<StackPanel Spacing="0">
@@ -70,6 +70,11 @@
TextWrapping="Wrap"
IsVisible="{Binding ErrorMessage, Converter={x:Static ObjectConverters.IsNotNull}}" />
<TextBlock Text="{Binding WarningMessage}"
Foreground="{DynamicResource AmberBrush}"
TextWrapping="Wrap"
IsVisible="{Binding WarningMessage, Converter={x:Static ObjectConverters.IsNotNull}}" />
<Button Classes="btn" HorizontalAlignment="Left"
Content="{loc:Tr modals.merge.stashAndMerge}"
Command="{Binding StashAndMergeCommand}"