refactor(diff): single DiffViewer replaces DiffModal + WorktreeModal + PlanningDiff

This commit is contained in:
Mika Kuns
2026-06-23 09:30:37 +02:00
parent 4022bd7197
commit 167d2fec6a
28 changed files with 923 additions and 1120 deletions

View File

@@ -8,7 +8,6 @@ using Avalonia.Platform.Storage;
using Avalonia.Reactive;
using ClaudeDo.Ui.ViewModels.Islands;
using ClaudeDo.Ui.Views.Modals;
using ClaudeDo.Ui.Views.Planning;
namespace ClaudeDo.Ui.Views.Islands;
@@ -129,11 +128,11 @@ public partial class DetailsIslandView : UserControl
vm.PropertyChanged += OnViewModelPropertyChanged;
ApplyResizeStateForCurrentTask();
vm.Merge.ShowDiffModal = async (diffVm) =>
vm.Merge.ShowDiffViewer = async (diffVm) =>
{
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner == null) return;
var modal = new DiffModalView { DataContext = diffVm };
var modal = new DiffViewerView { DataContext = diffVm };
await modal.ShowDialog(owner);
};
@@ -145,14 +144,6 @@ public partial class DetailsIslandView : UserControl
await modal.ShowDialog(owner);
};
vm.Merge.ShowPlanningDiffModal = async (planningDiffVm) =>
{
var owner = TopLevel.GetTopLevel(this) as Window;
if (owner == null) return;
var modal = new PlanningDiffView { DataContext = planningDiffVm };
await modal.ShowDialog(owner);
};
vm.ConfirmAsync = ShowConfirmAsync;
vm.ShowErrorAsync = ShowErrorDialogAsync;
}

View File

@@ -8,7 +8,6 @@ using ClaudeDo.Ui.ViewModels;
using ClaudeDo.Ui.ViewModels.Islands;
using ClaudeDo.Ui.ViewModels.Modals;
using ClaudeDo.Ui.Views.Modals;
using ClaudeDo.Ui.Views.Planning;
namespace ClaudeDo.Ui.Views;

View File

@@ -1,125 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
xmlns:loc="using:ClaudeDo.Ui.Localization"
x:Class="ClaudeDo.Ui.Views.Modals.DiffModalView"
x:DataType="vm:DiffModalViewModel"
Title="{loc:Tr modals.diff.windowTitle}"
Width="1200" Height="800" MinWidth="700" MinHeight="450"
CanResize="True"
WindowDecorations="BorderOnly"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaTitleBarHeightHint="-1"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource SurfaceBrush}">
<Window.KeyBindings>
<KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/>
</Window.KeyBindings>
<ctl:ModalShell Title="{loc:Tr modals.diff.title}" CloseCommand="{Binding CloseCommand}">
<ctl:ModalShell.Footer>
<StackPanel Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Classes="btn" Content="{loc:Tr modals.diff.merge}" Command="{Binding MergeCommand}"/>
</StackPanel>
</ctl:ModalShell.Footer>
<!-- Body: two islands — file list | diff content -->
<Grid ColumnDefinitions="280,12,*" Margin="16">
<!-- Files island -->
<Border Grid.Column="0" Classes="island">
<DockPanel>
<Border DockPanel.Dock="Top" Classes="island-header">
<TextBlock Classes="eyebrow" Text="{loc:Tr modals.diff.filesHeader}"/>
</Border>
<ListBox ItemsSource="{Binding Files}"
SelectedItem="{Binding SelectedFile, Mode=TwoWay}"
Background="Transparent"
BorderThickness="0"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:DiffFileViewModel">
<Border Padding="10,8" Background="Transparent">
<StackPanel Spacing="4">
<Grid ColumnDefinitions="Auto,*">
<Border Grid.Column="0" Tag="{Binding StatusCode}"
CornerRadius="3" Padding="4,0" Margin="0,0,8,0"
VerticalAlignment="Center">
<TextBlock Text="{Binding StatusCode}"
FontFamily="{DynamicResource MonoFont}"
FontSize="{StaticResource FontSizeEyebrow}"
Foreground="{DynamicResource TextBrush}"/>
</Border>
<TextBlock Grid.Column="1" Classes="path-mono" Text="{Binding Path}"
VerticalAlignment="Center"
TextTrimming="PrefixCharacterEllipsis"/>
</Grid>
<StackPanel Orientation="Horizontal" Spacing="6"
IsVisible="{Binding !IsBinary}">
<Border Classes="chip" Padding="5,2">
<TextBlock Foreground="{DynamicResource MossBrightBrush}"
Text="{Binding Additions, StringFormat='+{0}'}"/>
</Border>
<Border Classes="chip" Padding="5,2">
<TextBlock Foreground="{DynamicResource BloodBrush}"
Text="{Binding Deletions, StringFormat='{0}'}"/>
</Border>
</StackPanel>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</DockPanel>
</Border>
<!-- Diff content island -->
<Border Grid.Column="2" Classes="island">
<DockPanel>
<Border DockPanel.Dock="Top" Classes="island-header"
IsVisible="{Binding SelectedFile, Converter={x:Static ObjectConverters.IsNotNull}}">
<Grid ColumnDefinitions="Auto,*">
<Border Grid.Column="0" Tag="{Binding SelectedFile.StatusCode}"
CornerRadius="3" Padding="4,0" Margin="0,0,8,0"
VerticalAlignment="Center">
<TextBlock Text="{Binding SelectedFile.StatusCode}"
FontFamily="{DynamicResource MonoFont}"
FontSize="{StaticResource FontSizeEyebrow}"
Foreground="{DynamicResource TextBrush}"/>
</Border>
<TextBlock Grid.Column="1" Classes="path-mono" Text="{Binding SelectedFile.Path}"
VerticalAlignment="Center"
TextTrimming="PrefixCharacterEllipsis"/>
</Grid>
</Border>
<Grid Background="{DynamicResource VoidBrush}">
<!-- Load / no-changes message -->
<TextBlock Classes="body" Text="{Binding StatusMessage}"
IsVisible="{Binding StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<!-- Binary file -->
<TextBlock Classes="body" Text="{loc:Tr modals.diff.binary}"
Foreground="{DynamicResource TextMuteBrush}"
IsVisible="{Binding SelectedFile.IsBinary}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<!-- Empty / no-content file -->
<TextBlock Classes="body" Text="{loc:Tr modals.diff.empty}"
Foreground="{DynamicResource TextMuteBrush}"
IsVisible="{Binding SelectedFile.IsEmptyContent}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<!-- Diff content -->
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
IsVisible="{Binding SelectedFile.HasLines}">
<ctl:DiffLinesView Lines="{Binding SelectedFile.Lines}"/>
</ScrollViewer>
</Grid>
</DockPanel>
</Border>
</Grid>
</ctl:ModalShell>
</Window>

View File

@@ -1,45 +0,0 @@
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Styling;
using ClaudeDo.Ui.ViewModels.Modals;
namespace ClaudeDo.Ui.Views.Modals;
public partial class DiffModalView : Window
{
public DiffModalView()
{
InitializeComponent();
}
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
if (DataContext is DiffModalViewModel vm)
vm.CloseAction = Close;
}
protected override async void OnOpened(EventArgs e)
{
base.OnOpened(e);
Opacity = 0;
RenderTransform = new ScaleTransform(0.98, 0.98);
var anim = new Avalonia.Animation.Animation
{
Duration = TimeSpan.FromMilliseconds(180),
Easing = new CubicEaseOut(),
FillMode = FillMode.Forward,
Children =
{
new KeyFrame { Cue = new Cue(0), Setters = { new Setter(OpacityProperty, 0d) } },
new KeyFrame { Cue = new Cue(1), Setters = { new Setter(OpacityProperty, 1d) } },
}
};
await anim.RunAsync(this);
Opacity = 1;
RenderTransform = new ScaleTransform(1.0, 1.0);
}
}

View File

@@ -0,0 +1,171 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
xmlns:loc="using:ClaudeDo.Ui.Localization"
x:Class="ClaudeDo.Ui.Views.Modals.DiffViewerView"
x:DataType="vm:DiffViewerViewModel"
Title="{loc:Tr modals.diff.windowTitle}"
Width="1200" Height="800" MinWidth="700" MinHeight="450"
CanResize="True"
WindowDecorations="BorderOnly"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaTitleBarHeightHint="-1"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource SurfaceBrush}">
<Window.KeyBindings>
<KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/>
</Window.KeyBindings>
<ctl:ModalShell Title="{loc:Tr modals.diff.title}" CloseCommand="{Binding CloseCommand}">
<ctl:ModalShell.Footer>
<StackPanel Orientation="Horizontal" Spacing="8"
HorizontalAlignment="Right" VerticalAlignment="Center">
<Button Classes="btn" Content="{loc:Tr modals.diff.merge}"
Command="{Binding MergeCommand}"
IsVisible="{Binding ShowMerge}"/>
</StackPanel>
</ctl:ModalShell.Footer>
<DockPanel>
<!-- Planning toolbar: combined-mode toggle + warning/loading -->
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8" Margin="16,8,16,0"
IsVisible="{Binding IsPlanning}">
<ToggleButton Content="{loc:Tr planning.diff.previewCombined}" IsChecked="{Binding IsCombinedMode}"/>
<TextBlock Text="{Binding CombinedWarning}"
Foreground="{DynamicResource BloodBrush}"
VerticalAlignment="Center"
IsVisible="{Binding CombinedWarning, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<TextBlock Text="{loc:Tr planning.diff.loading}"
VerticalAlignment="Center"
IsVisible="{Binding IsLoadingCombined}"/>
</StackPanel>
<!-- Body: nav | splitter | diff -->
<Grid ColumnDefinitions="280,4,*" Margin="16">
<!-- Left nav: file tree (Files) OR subtask list (Planning) -->
<Border Grid.Column="0" Classes="island">
<DockPanel>
<Border DockPanel.Dock="Top" Classes="island-header" IsVisible="{Binding !IsPlanning}">
<TextBlock Classes="eyebrow" Text="{loc:Tr modals.diff.filesHeader}"/>
</Border>
<Panel>
<!-- Files: folder tree -->
<TreeView x:Name="FileTree"
ItemsSource="{Binding FileTree}"
SelectedItem="{Binding SelectedNode, Mode=TwoWay}"
Background="Transparent"
BorderThickness="0"
IsVisible="{Binding !IsPlanning}">
<TreeView.Styles>
<Style Selector="TreeViewItem" x:DataType="vm:DiffTreeNodeViewModel">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
</Style>
</TreeView.Styles>
<TreeView.ItemTemplate>
<TreeDataTemplate DataType="vm:DiffTreeNodeViewModel" ItemsSource="{Binding Children}">
<Border Background="Transparent" Tapped="OnNodeTapped" Cursor="Hand" Padding="0,2">
<Grid ColumnDefinitions="Auto,*,Auto">
<Border Grid.Column="0" Tag="{Binding StatusCode}" CornerRadius="3" Padding="4,0" Margin="0,0,6,0"
VerticalAlignment="Center"
IsVisible="{Binding StatusCode, Converter={x:Static StringConverters.IsNotNullOrEmpty}}">
<TextBlock Text="{Binding StatusCode}"
FontFamily="{DynamicResource MonoFont}" FontSize="{StaticResource FontSizeEyebrow}"
Foreground="{DynamicResource TextBrush}"/>
</Border>
<TextBlock Grid.Column="1" Classes="meta" Text="{Binding Name}"
VerticalAlignment="Center" TextTrimming="CharacterEllipsis"/>
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="4" VerticalAlignment="Center"
IsVisible="{Binding ShowStats}">
<TextBlock Foreground="{DynamicResource MossBrightBrush}" FontSize="{StaticResource FontSizeEyebrow}"
Text="{Binding Additions, StringFormat='+{0}'}"/>
<TextBlock Foreground="{DynamicResource BloodBrush}" FontSize="{StaticResource FontSizeEyebrow}"
Text="{Binding Deletions, StringFormat='{0}'}"/>
</StackPanel>
</Grid>
</Border>
</TreeDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<!-- Planning: subtask list -->
<ListBox ItemsSource="{Binding Subtasks}"
SelectedItem="{Binding SelectedSubtask}"
IsEnabled="{Binding !IsCombinedMode}"
IsVisible="{Binding IsPlanning}"
Background="Transparent"
BorderThickness="0"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:SubtaskDiffRow">
<Border Padding="10,8" Background="Transparent">
<StackPanel Spacing="2">
<TextBlock Classes="title" Text="{Binding Title}" TextTrimming="CharacterEllipsis"/>
<TextBlock Classes="meta" Text="{Binding DiffStat}"/>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Panel>
</DockPanel>
</Border>
<GridSplitter Grid.Column="1" ResizeDirection="Columns" Background="{DynamicResource LineBrush}"/>
<!-- Right: diff content (Files per-file pane OR Planning flat diff) -->
<Border Grid.Column="2" Classes="island">
<Panel>
<!-- Files mode: per-file pane with header + placeholders -->
<DockPanel IsVisible="{Binding !IsPlanning}">
<Border DockPanel.Dock="Top" Classes="island-header"
IsVisible="{Binding SelectedFile, Converter={x:Static ObjectConverters.IsNotNull}}">
<Grid ColumnDefinitions="Auto,*">
<Border Grid.Column="0" Tag="{Binding SelectedFile.StatusCode}"
CornerRadius="3" Padding="4,0" Margin="0,0,8,0" VerticalAlignment="Center">
<TextBlock Text="{Binding SelectedFile.StatusCode}"
FontFamily="{DynamicResource MonoFont}" FontSize="{StaticResource FontSizeEyebrow}"
Foreground="{DynamicResource TextBrush}"/>
</Border>
<TextBlock Grid.Column="1" Classes="path-mono" Text="{Binding SelectedFile.Path}"
VerticalAlignment="Center" TextTrimming="PrefixCharacterEllipsis"/>
</Grid>
</Border>
<Grid Background="{DynamicResource VoidBrush}">
<TextBlock Classes="body" Text="{Binding StatusMessage}"
IsVisible="{Binding StatusMessage, Converter={x:Static StringConverters.IsNotNullOrEmpty}}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBlock Classes="body" Text="{loc:Tr modals.diff.binary}"
Foreground="{DynamicResource TextMuteBrush}"
IsVisible="{Binding SelectedFile.IsBinary}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<TextBlock Classes="body" Text="{loc:Tr modals.diff.empty}"
Foreground="{DynamicResource TextMuteBrush}"
IsVisible="{Binding SelectedFile.IsEmptyContent}"
HorizontalAlignment="Center" VerticalAlignment="Center"/>
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto"
IsVisible="{Binding SelectedFile.HasLines}">
<ctl:DiffLinesView Lines="{Binding SelectedFile.Lines}"/>
</ScrollViewer>
</Grid>
</DockPanel>
<!-- Planning mode: flat aggregate/combined diff -->
<Grid Background="{DynamicResource VoidBrush}" IsVisible="{Binding IsPlanning}">
<ScrollViewer HorizontalScrollBarVisibility="Auto" VerticalScrollBarVisibility="Auto">
<ctl:DiffLinesView Lines="{Binding DiffLines}"/>
</ScrollViewer>
</Grid>
</Panel>
</Border>
</Grid>
</DockPanel>
</ctl:ModalShell>
</Window>

View File

@@ -0,0 +1,41 @@
using Avalonia.Controls;
using Avalonia.Input;
using ClaudeDo.Ui.ViewModels.Modals;
namespace ClaudeDo.Ui.Views.Modals;
public partial class DiffViewerView : Window
{
public DiffViewerView()
{
InitializeComponent();
}
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
if (DataContext is DiffViewerViewModel vm)
vm.CloseAction = Close;
// SelectedItem TwoWay binding can miss on Avalonia 12 TreeView — back it
// up with SelectionChanged.
var tree = this.FindControl<TreeView>("FileTree");
if (tree is not null)
tree.SelectionChanged += OnFileTreeSelectionChanged;
}
private void OnFileTreeSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (DataContext is DiffViewerViewModel vm && sender is TreeView tree)
vm.SelectedNode = tree.SelectedItem as DiffTreeNodeViewModel;
}
private void OnNodeTapped(object? sender, TappedEventArgs e)
{
if (sender is not Control c) return;
if (c.DataContext is not DiffTreeNodeViewModel node) return;
if (!node.IsDirectory) return;
node.IsExpanded = !node.IsExpanded;
e.Handled = true;
}
}

View File

@@ -1,96 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
xmlns:converters="using:ClaudeDo.Ui.Converters"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
xmlns:loc="using:ClaudeDo.Ui.Localization"
x:Class="ClaudeDo.Ui.Views.Modals.WorktreeModalView"
x:DataType="vm:WorktreeModalViewModel"
Title="{loc:Tr modals.worktree.title}"
Width="1100" Height="720"
MinWidth="640" MinHeight="400"
WindowStartupLocation="CenterOwner"
WindowDecorations="BorderOnly"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaTitleBarHeightHint="-1"
Background="Transparent"
CanResize="True"
TransparencyLevelHint="AcrylicBlur">
<Window.KeyBindings>
<KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/>
</Window.KeyBindings>
<Border Classes="island" Margin="12">
<DockPanel>
<!-- Title strip -->
<Border DockPanel.Dock="Top" Height="36"
Background="Transparent"
PointerPressed="OnTitleBarPressed"
PointerMoved="OnTitleBarMoved"
PointerReleased="OnTitleBarReleased">
<Grid ColumnDefinitions="*,Auto" Margin="14,0">
<TextBlock Grid.Column="0" Text="{loc:Tr modals.worktree.title}" VerticalAlignment="Center"
FontFamily="{DynamicResource MonoFont}" FontSize="{StaticResource FontSizeBody}"
Foreground="{DynamicResource TextMuteBrush}"/>
<Button Grid.Column="1" Classes="icon-btn" Content="✕"
Command="{Binding CloseCommand}" VerticalAlignment="Center"/>
</Grid>
</Border>
<!-- Path strip -->
<Border DockPanel.Dock="Top" Padding="14,0,14,8">
<TextBlock Classes="path-mono" Text="{Binding WorktreePath}"/>
</Border>
<!-- Split: file tree | splitter | diff pane -->
<Grid ColumnDefinitions="260,4,*">
<!-- Left: file tree -->
<TreeView x:Name="FileTree"
Grid.Column="0"
ItemsSource="{Binding Root}"
SelectedItem="{Binding SelectedNode, Mode=TwoWay}"
Background="Transparent"
Margin="8,0,4,8">
<TreeView.Styles>
<Style Selector="TreeViewItem" x:DataType="vm:WorktreeNodeViewModel">
<Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
</Style>
</TreeView.Styles>
<TreeView.ItemTemplate>
<TreeDataTemplate DataType="vm:WorktreeNodeViewModel"
ItemsSource="{Binding Children}">
<Border Background="Transparent" Tapped="OnNodeTapped" Cursor="Hand">
<StackPanel Orientation="Horizontal" Spacing="8">
<TextBlock Classes="meta" Text="{Binding Name}"/>
<Border Tag="{Binding Status}" CornerRadius="3" Padding="4,0"
VerticalAlignment="Center"
IsVisible="{Binding Status, Converter={x:Static ObjectConverters.IsNotNull}}">
<TextBlock Text="{Binding Status}"
FontFamily="{DynamicResource MonoFont}" FontSize="{StaticResource FontSizeEyebrow}"
Foreground="{DynamicResource TextBrush}"/>
</Border>
</StackPanel>
</Border>
</TreeDataTemplate>
</TreeView.ItemTemplate>
</TreeView>
<!-- Splitter -->
<GridSplitter Grid.Column="1" ResizeDirection="Columns" Background="{DynamicResource LineBrush}"/>
<!-- Right: diff content -->
<ScrollViewer Grid.Column="2" Padding="8"
HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto"
Margin="4,0,8,8">
<ctl:DiffLinesView Lines="{Binding SelectedFileDiffLines}"/>
</ScrollViewer>
</Grid>
</DockPanel>
</Border>
</Window>

View File

@@ -1,96 +0,0 @@
using Avalonia;
using Avalonia.Animation;
using Avalonia.Animation.Easings;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Media;
using Avalonia.Styling;
using Avalonia.VisualTree;
using ClaudeDo.Ui.ViewModels.Modals;
namespace ClaudeDo.Ui.Views.Modals;
public partial class WorktreeModalView : Window
{
public WorktreeModalView()
{
InitializeComponent();
}
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
if (DataContext is WorktreeModalViewModel vm)
vm.CloseAction = Close;
// Wire TreeView selection — SelectedItem TwoWay binding may not fire
// reliably in Avalonia 12 for TreeView; use SelectionChanged as backup.
var tree = this.FindControl<TreeView>("FileTree");
if (tree is not null)
tree.SelectionChanged += OnFileTreeSelectionChanged;
}
private void OnFileTreeSelectionChanged(object? sender, SelectionChangedEventArgs e)
{
if (DataContext is WorktreeModalViewModel vm && sender is TreeView tree)
vm.SelectedNode = tree.SelectedItem as WorktreeNodeViewModel;
}
protected override async void OnOpened(EventArgs e)
{
base.OnOpened(e);
Opacity = 0;
RenderTransform = new ScaleTransform(0.98, 0.98);
var anim = new Avalonia.Animation.Animation
{
Duration = TimeSpan.FromMilliseconds(180),
Easing = new CubicEaseOut(),
FillMode = FillMode.Forward,
Children =
{
new KeyFrame { Cue = new Cue(0), Setters = { new Setter(OpacityProperty, 0d) } },
new KeyFrame { Cue = new Cue(1), Setters = { new Setter(OpacityProperty, 1d) } },
}
};
await anim.RunAsync(this);
Opacity = 1;
RenderTransform = new ScaleTransform(1.0, 1.0);
}
private void OnNodeTapped(object? sender, Avalonia.Input.TappedEventArgs e)
{
if (sender is not Control c) return;
if (c.DataContext is not WorktreeNodeViewModel node) return;
if (!node.IsDirectory) return;
node.IsExpanded = !node.IsExpanded;
e.Handled = true;
}
private PixelPoint _dragStartScreen;
private PixelPoint _dragStartPos;
private bool _dragging;
private void OnTitleBarPressed(object? sender, PointerPressedEventArgs e)
{
if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return;
_dragStartScreen = this.PointToScreen(e.GetPosition(this));
_dragStartPos = Position;
_dragging = true;
e.Pointer.Capture(sender as IInputElement);
}
private void OnTitleBarMoved(object? sender, PointerEventArgs e)
{
if (!_dragging || !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed) return;
var cur = this.PointToScreen(e.GetPosition(this));
Position = new PixelPoint(
_dragStartPos.X + (cur.X - _dragStartScreen.X),
_dragStartPos.Y + (cur.Y - _dragStartScreen.Y));
}
private void OnTitleBarReleased(object? sender, PointerReleasedEventArgs e)
{
_dragging = false;
e.Pointer.Capture(null);
}
}

View File

@@ -1,77 +0,0 @@
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Planning"
xmlns:ctl="using:ClaudeDo.Ui.Views.Controls"
xmlns:loc="using:ClaudeDo.Ui.Localization"
x:Class="ClaudeDo.Ui.Views.Planning.PlanningDiffView"
x:DataType="vm:PlanningDiffViewModel"
Title="{loc:Tr planning.diff.windowTitle}"
Width="1100" Height="700" MinWidth="700" MinHeight="450"
CanResize="True"
WindowDecorations="BorderOnly"
ExtendClientAreaToDecorationsHint="True"
ExtendClientAreaTitleBarHeightHint="-1"
WindowStartupLocation="CenterOwner"
Background="{DynamicResource SurfaceBrush}">
<Window.KeyBindings>
<KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/>
</Window.KeyBindings>
<ctl:ModalShell Title="{loc:Tr planning.diff.modalTitle}" CloseCommand="{Binding CloseCommand}">
<!-- Toolbar row -->
<DockPanel>
<StackPanel DockPanel.Dock="Top"
Orientation="Horizontal"
Spacing="8"
Margin="8,6">
<ToggleButton Content="{loc:Tr planning.diff.previewCombined}" IsChecked="{Binding IsCombinedMode}"/>
<TextBlock Text="{Binding CombinedWarning}"
Foreground="{DynamicResource BloodBrush}"
VerticalAlignment="Center"
IsVisible="{Binding CombinedWarning, Converter={x:Static ObjectConverters.IsNotNull}}"/>
<TextBlock Text="{loc:Tr planning.diff.loading}"
VerticalAlignment="Center"
IsVisible="{Binding IsLoadingCombined}"/>
</StackPanel>
<!-- Two-pane body -->
<Grid ColumnDefinitions="240,*">
<!-- Subtask list (left pane) -->
<Border Grid.Column="0"
Classes="sidebar-pane">
<ListBox ItemsSource="{Binding Subtasks}"
SelectedItem="{Binding SelectedSubtask}"
IsEnabled="{Binding !IsCombinedMode}"
Background="Transparent"
BorderThickness="0"
ScrollViewer.HorizontalScrollBarVisibility="Disabled">
<ListBox.ItemTemplate>
<DataTemplate x:DataType="vm:SubtaskDiffRow">
<Border Padding="10,8" Background="Transparent">
<StackPanel Spacing="2">
<TextBlock Classes="title" Text="{Binding Title}"
TextTrimming="CharacterEllipsis"/>
<TextBlock Classes="meta" Text="{Binding DiffStat}"/>
</StackPanel>
</Border>
</DataTemplate>
</ListBox.ItemTemplate>
</ListBox>
</Border>
<!-- Diff content (right pane) -->
<Grid Grid.Column="1" Background="{DynamicResource VoidBrush}">
<ScrollViewer HorizontalScrollBarVisibility="Auto"
VerticalScrollBarVisibility="Auto">
<ctl:DiffLinesView Lines="{Binding DiffLines}"/>
</ScrollViewer>
</Grid>
</Grid>
</DockPanel>
</ctl:ModalShell>
</Window>

View File

@@ -1,19 +0,0 @@
using Avalonia.Controls;
using ClaudeDo.Ui.ViewModels.Planning;
namespace ClaudeDo.Ui.Views.Planning;
public partial class PlanningDiffView : Window
{
public PlanningDiffView()
{
InitializeComponent();
}
protected override void OnDataContextChanged(EventArgs e)
{
base.OnDataContextChanged(e);
if (DataContext is PlanningDiffViewModel vm)
vm.CloseAction = Close;
}
}

View File

@@ -73,7 +73,7 @@ public sealed class WindowDialogService : IDialogService
};
vm.ShowDiffAction = diffVm =>
{
var diffDlg = new WorktreeModalView { DataContext = diffVm };
var diffDlg = new DiffViewerView { DataContext = diffVm };
diffVm.CloseAction = () => diffDlg.Close();
_ = diffVm.LoadAsync();
_ = diffDlg.ShowDialog(_owner);