feat(ui): diff modal with file sidebar and tinted hunks
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -108,7 +108,8 @@ sealed class Program
|
|||||||
sc.AddSingleton<DetailsIslandViewModel>(sp =>
|
sc.AddSingleton<DetailsIslandViewModel>(sp =>
|
||||||
new DetailsIslandViewModel(
|
new DetailsIslandViewModel(
|
||||||
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
|
sp.GetRequiredService<IDbContextFactory<ClaudeDoDbContext>>(),
|
||||||
sp.GetRequiredService<WorkerClient>()));
|
sp.GetRequiredService<WorkerClient>(),
|
||||||
|
sp));
|
||||||
sc.AddSingleton<IslandsShellViewModel>();
|
sc.AddSingleton<IslandsShellViewModel>();
|
||||||
|
|
||||||
return sc.BuildServiceProvider();
|
return sc.BuildServiceProvider();
|
||||||
|
|||||||
@@ -27,6 +27,14 @@ public sealed class GitService
|
|||||||
throw new InvalidOperationException($"git worktree add failed (exit {exitCode}): {stderr}");
|
throw new InvalidOperationException($"git worktree add failed (exit {exitCode}): {stderr}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetStatusPorcelainAsync(string workingDirectory, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var (exitCode, stdout, stderr) = await RunGitAsync(workingDirectory, ["status", "--porcelain"], ct);
|
||||||
|
if (exitCode != 0)
|
||||||
|
throw new InvalidOperationException($"git status --porcelain failed (exit {exitCode}): {stderr}");
|
||||||
|
return stdout;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<bool> HasChangesAsync(string worktreePath, CancellationToken ct = default)
|
public async Task<bool> HasChangesAsync(string worktreePath, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var (exitCode, stdout, stderr) = await RunGitAsync(worktreePath, ["status", "--porcelain"], ct);
|
var (exitCode, stdout, stderr) = await RunGitAsync(worktreePath, ["status", "--porcelain"], ct);
|
||||||
@@ -50,6 +58,21 @@ public sealed class GitService
|
|||||||
throw new InvalidOperationException($"git commit failed (exit {exitCode}): {stderr}");
|
throw new InvalidOperationException($"git commit failed (exit {exitCode}): {stderr}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task<string> GetDiffAsync(string worktreePath, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
var (exitCode, stdout, stderr) = await RunGitAsync(worktreePath,
|
||||||
|
["diff", "HEAD"], ct);
|
||||||
|
if (exitCode != 0)
|
||||||
|
throw new InvalidOperationException($"git diff HEAD failed (exit {exitCode}): {stderr}");
|
||||||
|
// If nothing staged vs HEAD, try the index (untracked is never in diff)
|
||||||
|
if (string.IsNullOrWhiteSpace(stdout))
|
||||||
|
{
|
||||||
|
var (e2, s2, _) = await RunGitAsync(worktreePath, ["diff", "--cached"], ct);
|
||||||
|
if (e2 == 0) return s2;
|
||||||
|
}
|
||||||
|
return stdout;
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<string> DiffStatAsync(string worktreePath, string baseCommit, string headCommit, CancellationToken ct = default)
|
public async Task<string> DiffStatAsync(string worktreePath, string baseCommit, string headCommit, CancellationToken ct = default)
|
||||||
{
|
{
|
||||||
var (exitCode, stdout, stderr) = await RunGitAsync(worktreePath,
|
var (exitCode, stdout, stderr) = await RunGitAsync(worktreePath,
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ using CommunityToolkit.Mvvm.Input;
|
|||||||
using ClaudeDo.Data;
|
using ClaudeDo.Data;
|
||||||
using ClaudeDo.Data.Repositories;
|
using ClaudeDo.Data.Repositories;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Modals;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.ViewModels.Islands;
|
namespace ClaudeDo.Ui.ViewModels.Islands;
|
||||||
|
|
||||||
@@ -12,6 +14,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
{
|
{
|
||||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||||
private readonly WorkerClient _worker;
|
private readonly WorkerClient _worker;
|
||||||
|
private readonly IServiceProvider _services;
|
||||||
|
|
||||||
// Current task row (set by IslandsShellViewModel via Bind)
|
// Current task row (set by IslandsShellViewModel via Bind)
|
||||||
[ObservableProperty] private TaskRowViewModel? _task;
|
[ObservableProperty] private TaskRowViewModel? _task;
|
||||||
@@ -35,10 +38,17 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
// The task ID we are currently subscribed to for live log messages
|
// The task ID we are currently subscribed to for live log messages
|
||||||
private string? _subscribedTaskId;
|
private string? _subscribedTaskId;
|
||||||
|
|
||||||
public DetailsIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, WorkerClient worker)
|
// Set by the view so OpenDiffCommand can show the modal as a dialog
|
||||||
|
public Func<DiffModalViewModel, System.Threading.Tasks.Task>? ShowDiffModal { get; set; }
|
||||||
|
|
||||||
|
// Set by the view so OpenWorktreeCommand can show the modal as a dialog
|
||||||
|
public Func<WorktreeModalViewModel, System.Threading.Tasks.Task>? ShowWorktreeModal { get; set; }
|
||||||
|
|
||||||
|
public DetailsIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, WorkerClient worker, IServiceProvider services)
|
||||||
{
|
{
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
_worker = worker;
|
_worker = worker;
|
||||||
|
_services = services;
|
||||||
|
|
||||||
// Subscribe once; filter by current task id inside the handler
|
// Subscribe once; filter by current task id inside the handler
|
||||||
_worker.TaskMessageEvent += OnTaskMessage;
|
_worker.TaskMessageEvent += OnTaskMessage;
|
||||||
@@ -99,6 +109,38 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
Subtasks.Add(new SubtaskRowViewModel { Id = s.Id, Title = s.Title, Done = s.Completed });
|
Subtasks.Add(new SubtaskRowViewModel { Id = s.Id, Title = s.Title, Done = s.Completed });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanOpenDiff))]
|
||||||
|
private async System.Threading.Tasks.Task OpenDiffAsync()
|
||||||
|
{
|
||||||
|
if (WorktreePath == null || ShowDiffModal == null) return;
|
||||||
|
var diffVm = new DiffModalViewModel(_services.GetRequiredService<ClaudeDo.Data.Git.GitService>())
|
||||||
|
{
|
||||||
|
WorktreePath = WorktreePath,
|
||||||
|
};
|
||||||
|
await diffVm.LoadAsync();
|
||||||
|
await ShowDiffModal(diffVm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanOpenDiff() => WorktreePath != null;
|
||||||
|
|
||||||
|
[RelayCommand(CanExecute = nameof(CanOpenWorktree))]
|
||||||
|
private async System.Threading.Tasks.Task OpenWorktreeAsync()
|
||||||
|
{
|
||||||
|
if (WorktreePath == null || ShowWorktreeModal == null) return;
|
||||||
|
var vm = _services.GetRequiredService<WorktreeModalViewModel>();
|
||||||
|
vm.WorktreePath = WorktreePath;
|
||||||
|
await vm.LoadAsync();
|
||||||
|
await ShowWorktreeModal(vm);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool CanOpenWorktree() => WorktreePath != null;
|
||||||
|
|
||||||
|
partial void OnWorktreePathChanged(string? value)
|
||||||
|
{
|
||||||
|
OpenDiffCommand.NotifyCanExecuteChanged();
|
||||||
|
OpenWorktreeCommand.NotifyCanExecuteChanged();
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async System.Threading.Tasks.Task SendPromptAsync()
|
private async System.Threading.Tasks.Task SendPromptAsync()
|
||||||
{
|
{
|
||||||
|
|||||||
154
src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs
Normal file
154
src/ClaudeDo.Ui/ViewModels/Modals/DiffModalViewModel.cs
Normal file
@@ -0,0 +1,154 @@
|
|||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
using ClaudeDo.Data.Git;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.ViewModels.Modals;
|
||||||
|
|
||||||
|
public enum DiffLineKind { Add, Del, Ctx }
|
||||||
|
|
||||||
|
public sealed class DiffLineViewModel
|
||||||
|
{
|
||||||
|
public required DiffLineKind Kind { get; init; }
|
||||||
|
public int? OldNo { get; init; }
|
||||||
|
public int? NewNo { get; init; }
|
||||||
|
public required string Text { get; init; }
|
||||||
|
public string ClassName => Kind switch
|
||||||
|
{
|
||||||
|
DiffLineKind.Add => "add",
|
||||||
|
DiffLineKind.Del => "del",
|
||||||
|
_ => "ctx",
|
||||||
|
};
|
||||||
|
|
||||||
|
public string Sign => Kind switch
|
||||||
|
{
|
||||||
|
DiffLineKind.Add => "+",
|
||||||
|
DiffLineKind.Del => "-",
|
||||||
|
_ => " ",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed class DiffFileViewModel
|
||||||
|
{
|
||||||
|
public required string Path { get; init; }
|
||||||
|
public int Additions { get; init; }
|
||||||
|
public int Deletions { get; init; }
|
||||||
|
public ObservableCollection<DiffLineViewModel> Lines { get; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
public sealed partial class DiffModalViewModel : ViewModelBase
|
||||||
|
{
|
||||||
|
private readonly GitService _git;
|
||||||
|
|
||||||
|
public required string WorktreePath { get; init; }
|
||||||
|
|
||||||
|
public ObservableCollection<DiffFileViewModel> Files { get; } = new();
|
||||||
|
|
||||||
|
[ObservableProperty] private DiffFileViewModel? _selectedFile;
|
||||||
|
|
||||||
|
// Injected action to close the owning Window
|
||||||
|
public Action? CloseAction { get; set; }
|
||||||
|
|
||||||
|
public DiffModalViewModel(GitService git)
|
||||||
|
{
|
||||||
|
_git = git;
|
||||||
|
}
|
||||||
|
|
||||||
|
[RelayCommand]
|
||||||
|
private void Close() => CloseAction?.Invoke();
|
||||||
|
|
||||||
|
public async Task LoadAsync(CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
Files.Clear();
|
||||||
|
|
||||||
|
string raw;
|
||||||
|
try { raw = await _git.GetDiffAsync(WorktreePath, ct); }
|
||||||
|
catch { return; }
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(raw)) return;
|
||||||
|
|
||||||
|
// Parse unified diff — state machine over lines
|
||||||
|
DiffFileViewModel? current = null;
|
||||||
|
int oldLine = 0, newLine = 0;
|
||||||
|
|
||||||
|
foreach (var line in raw.Split('\n'))
|
||||||
|
{
|
||||||
|
if (line.StartsWith("diff --git ", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
// e.g. "diff --git a/src/Foo.cs b/src/Foo.cs"
|
||||||
|
var parts = line.Split(' ');
|
||||||
|
var path = parts.Length >= 4 ? parts[3][2..] : line;
|
||||||
|
current = new DiffFileViewModel { Path = path };
|
||||||
|
Files.Add(current);
|
||||||
|
oldLine = 0; newLine = 0;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current == null) continue;
|
||||||
|
|
||||||
|
if (line.StartsWith("@@ ", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
// e.g. "@@ -10,7 +10,9 @@"
|
||||||
|
ParseHunkHeader(line, out oldLine, out newLine);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip diff metadata lines
|
||||||
|
if (line.StartsWith("--- ", StringComparison.Ordinal) ||
|
||||||
|
line.StartsWith("+++ ", StringComparison.Ordinal) ||
|
||||||
|
line.StartsWith("index ", StringComparison.Ordinal) ||
|
||||||
|
line.StartsWith("new file", StringComparison.Ordinal) ||
|
||||||
|
line.StartsWith("deleted file", StringComparison.Ordinal) ||
|
||||||
|
line.StartsWith("Binary ", StringComparison.Ordinal))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (line.StartsWith('+'))
|
||||||
|
{
|
||||||
|
current.Lines.Add(new DiffLineViewModel
|
||||||
|
{
|
||||||
|
Kind = DiffLineKind.Add,
|
||||||
|
NewNo = newLine++,
|
||||||
|
Text = line.Length > 1 ? line[1..] : "",
|
||||||
|
});
|
||||||
|
// Count additions on the file VM
|
||||||
|
}
|
||||||
|
else if (line.StartsWith('-'))
|
||||||
|
{
|
||||||
|
current.Lines.Add(new DiffLineViewModel
|
||||||
|
{
|
||||||
|
Kind = DiffLineKind.Del,
|
||||||
|
OldNo = oldLine++,
|
||||||
|
Text = line.Length > 1 ? line[1..] : "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else if (line.StartsWith(' '))
|
||||||
|
{
|
||||||
|
current.Lines.Add(new DiffLineViewModel
|
||||||
|
{
|
||||||
|
Kind = DiffLineKind.Ctx,
|
||||||
|
OldNo = oldLine++,
|
||||||
|
NewNo = newLine++,
|
||||||
|
Text = line.Length > 1 ? line[1..] : "",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SelectedFile = Files.Count > 0 ? Files[0] : null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ParseHunkHeader(string header, out int oldStart, out int newStart)
|
||||||
|
{
|
||||||
|
oldStart = 1; newStart = 1;
|
||||||
|
// Format: @@ -<old>,<count> +<new>,<count> @@
|
||||||
|
var at = header.IndexOf("@@", 3, StringComparison.Ordinal);
|
||||||
|
var inner = at > 0 ? header[3..at].Trim() : header;
|
||||||
|
var segs = inner.Split(' ');
|
||||||
|
foreach (var seg in segs)
|
||||||
|
{
|
||||||
|
if (seg.StartsWith('-') && int.TryParse(seg[1..].Split(',')[0], out var o))
|
||||||
|
oldStart = o;
|
||||||
|
else if (seg.StartsWith('+') && int.TryParse(seg[1..].Split(',')[0], out var n))
|
||||||
|
newStart = n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,8 +29,8 @@
|
|||||||
IsVisible="{Binding BranchLine, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
IsVisible="{Binding BranchLine, Converter={x:Static ObjectConverters.IsNotNull}}"/>
|
||||||
<!-- Button row -->
|
<!-- Button row -->
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,6,0,0">
|
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,6,0,0">
|
||||||
<Button Classes="icon-btn" Content="Open diff"/>
|
<Button Classes="icon-btn" Content="Open diff" Command="{Binding OpenDiffCommand}"/>
|
||||||
<Button Classes="icon-btn" Content="Worktree"/>
|
<Button Classes="icon-btn" Content="Worktree" Command="{Binding OpenWorktreeCommand}"/>
|
||||||
<Button Classes="icon-btn" Content="Stop" Command="{Binding StopCommand}"/>
|
<Button Classes="icon-btn" Content="Stop" Command="{Binding StopCommand}"/>
|
||||||
<Button Classes="icon-btn" Content="Approve & merge" Command="{Binding ApproveMergeCommand}"/>
|
<Button Classes="icon-btn" Content="Approve & merge" Command="{Binding ApproveMergeCommand}"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|||||||
@@ -1,8 +1,38 @@
|
|||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Islands;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Modals;
|
||||||
|
using ClaudeDo.Ui.Views.Modals;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Views.Islands;
|
namespace ClaudeDo.Ui.Views.Islands;
|
||||||
|
|
||||||
public partial class AgentStripView : UserControl
|
public partial class AgentStripView : UserControl
|
||||||
{
|
{
|
||||||
public AgentStripView() { InitializeComponent(); }
|
public AgentStripView()
|
||||||
|
{
|
||||||
|
InitializeComponent();
|
||||||
|
DataContextChanged += OnDataContextChanged;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnDataContextChanged(object? sender, EventArgs e)
|
||||||
|
{
|
||||||
|
if (DataContext is DetailsIslandViewModel vm)
|
||||||
|
{
|
||||||
|
vm.ShowDiffModal = async (diffVm) =>
|
||||||
|
{
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
if (owner == null) return;
|
||||||
|
var modal = new DiffModalView { DataContext = diffVm };
|
||||||
|
await modal.ShowDialog(owner);
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.ShowWorktreeModal = async (worktreeVm) =>
|
||||||
|
{
|
||||||
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
|
if (owner == null) return;
|
||||||
|
var modal = new WorktreeModalView { DataContext = worktreeVm };
|
||||||
|
worktreeVm.CloseCommand.Subscribe(_ => modal.Close());
|
||||||
|
await modal.ShowDialog(owner);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
174
src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml
Normal file
174
src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml
Normal file
@@ -0,0 +1,174 @@
|
|||||||
|
<Window xmlns="https://github.com/avaloniaui"
|
||||||
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||||
|
xmlns:vm="using:ClaudeDo.Ui.ViewModels.Modals"
|
||||||
|
x:Class="ClaudeDo.Ui.Views.Modals.DiffModalView"
|
||||||
|
x:DataType="vm:DiffModalViewModel"
|
||||||
|
Title="Diff"
|
||||||
|
Width="1200" Height="800"
|
||||||
|
SystemDecorations="None"
|
||||||
|
ExtendClientAreaToDecorationsHint="True"
|
||||||
|
WindowStartupLocation="CenterOwner"
|
||||||
|
Background="Transparent"
|
||||||
|
TransparencyLevelHint="AcrylicBlur">
|
||||||
|
|
||||||
|
<Window.KeyBindings>
|
||||||
|
<KeyBinding Gesture="Escape" Command="{Binding CloseCommand}"/>
|
||||||
|
</Window.KeyBindings>
|
||||||
|
|
||||||
|
<Window.Styles>
|
||||||
|
<!-- diff line row tints via Tag selector (compiled-binding-friendly) -->
|
||||||
|
<Style Selector="Border.diff-line[Tag=add]">
|
||||||
|
<Setter Property="Background" Value="#1A4A6B4A"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.diff-line[Tag=del]">
|
||||||
|
<Setter Property="Background" Value="#1AC87060"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.diff-line[Tag=ctx]">
|
||||||
|
<Setter Property="Background" Value="Transparent"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.diff-line[Tag=add] TextBlock.diff-sign">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource MossBrightBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.diff-line[Tag=del] TextBlock.diff-sign">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource BloodBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.diff-line[Tag=ctx] TextBlock.diff-sign">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextFaintBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.diff-line[Tag=add] TextBlock.diff-text">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource MossBrightBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.diff-line[Tag=del] TextBlock.diff-text">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource BloodBrush}"/>
|
||||||
|
</Style>
|
||||||
|
<Style Selector="Border.diff-line[Tag=ctx] TextBlock.diff-text">
|
||||||
|
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}"/>
|
||||||
|
</Style>
|
||||||
|
</Window.Styles>
|
||||||
|
|
||||||
|
<!-- Outer container -->
|
||||||
|
<Border CornerRadius="{StaticResource ModalCornerRadius}"
|
||||||
|
BoxShadow="{StaticResource ModalShadow}"
|
||||||
|
Background="{StaticResource SurfaceBrush}"
|
||||||
|
BorderBrush="{StaticResource LineBrush}"
|
||||||
|
BorderThickness="1"
|
||||||
|
ClipToBounds="True">
|
||||||
|
<Grid RowDefinitions="36,*">
|
||||||
|
|
||||||
|
<!-- Title bar / drag handle -->
|
||||||
|
<Border Grid.Row="0"
|
||||||
|
x:Name="TitleBar"
|
||||||
|
Background="{StaticResource Surface2Brush}"
|
||||||
|
BorderBrush="{StaticResource LineBrush}"
|
||||||
|
BorderThickness="0,0,0,1"
|
||||||
|
PointerPressed="TitleBar_PointerPressed">
|
||||||
|
<Grid ColumnDefinitions="*,Auto" Margin="14,0">
|
||||||
|
<TextBlock Text="Diff" VerticalAlignment="Center"
|
||||||
|
FontFamily="{StaticResource MonoFamily}"
|
||||||
|
FontSize="12"
|
||||||
|
Foreground="{StaticResource TextDimBrush}"/>
|
||||||
|
<Button Grid.Column="1"
|
||||||
|
Classes="icon-btn"
|
||||||
|
Content="✕"
|
||||||
|
FontSize="12"
|
||||||
|
Command="{Binding CloseCommand}"
|
||||||
|
VerticalAlignment="Center"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Body: sidebar + diff content -->
|
||||||
|
<Grid Grid.Row="1" ColumnDefinitions="240,*">
|
||||||
|
|
||||||
|
<!-- File sidebar -->
|
||||||
|
<Border Grid.Column="0"
|
||||||
|
BorderBrush="{StaticResource LineBrush}"
|
||||||
|
BorderThickness="0,0,1,0"
|
||||||
|
Background="{StaticResource DeepBrush}">
|
||||||
|
<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">
|
||||||
|
<TextBlock Text="{Binding Path}"
|
||||||
|
FontFamily="{StaticResource MonoFamily}"
|
||||||
|
FontSize="11"
|
||||||
|
Foreground="{StaticResource TextDimBrush}"
|
||||||
|
TextTrimming="LeadingEllipsis"/>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
|
<Border Classes="chip" Padding="5,2">
|
||||||
|
<TextBlock Foreground="{StaticResource MossBrightBrush}"
|
||||||
|
FontFamily="{StaticResource MonoFamily}"
|
||||||
|
FontSize="10"
|
||||||
|
Text="{Binding Additions, StringFormat='+{0}'}"/>
|
||||||
|
</Border>
|
||||||
|
<Border Classes="chip" Padding="5,2">
|
||||||
|
<TextBlock Foreground="{StaticResource BloodBrush}"
|
||||||
|
FontFamily="{StaticResource MonoFamily}"
|
||||||
|
FontSize="10"
|
||||||
|
Text="{Binding Deletions, StringFormat='\u2212{0}'}"/>
|
||||||
|
</Border>
|
||||||
|
</StackPanel>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ListBox.ItemTemplate>
|
||||||
|
</ListBox>
|
||||||
|
</Border>
|
||||||
|
|
||||||
|
<!-- Diff content -->
|
||||||
|
<ScrollViewer Grid.Column="1"
|
||||||
|
HorizontalScrollBarVisibility="Auto"
|
||||||
|
VerticalScrollBarVisibility="Auto"
|
||||||
|
Background="{StaticResource VoidBrush}">
|
||||||
|
<ItemsControl ItemsSource="{Binding SelectedFile.Lines}">
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="vm:DiffLineViewModel">
|
||||||
|
<Border Classes="diff-line"
|
||||||
|
Tag="{Binding ClassName}"
|
||||||
|
Padding="4,1">
|
||||||
|
<Grid ColumnDefinitions="48,48,16,*">
|
||||||
|
<!-- Old line number -->
|
||||||
|
<TextBlock Grid.Column="0"
|
||||||
|
Text="{Binding OldNo}"
|
||||||
|
Classes="diff-lineno"
|
||||||
|
FontFamily="{StaticResource MonoFamily}"
|
||||||
|
FontSize="11"
|
||||||
|
Foreground="{StaticResource TextFaintBrush}"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<!-- New line number -->
|
||||||
|
<TextBlock Grid.Column="1"
|
||||||
|
Text="{Binding NewNo}"
|
||||||
|
Classes="diff-lineno"
|
||||||
|
FontFamily="{StaticResource MonoFamily}"
|
||||||
|
FontSize="11"
|
||||||
|
Foreground="{StaticResource TextFaintBrush}"
|
||||||
|
HorizontalAlignment="Right"
|
||||||
|
Margin="0,0,8,0"/>
|
||||||
|
<!-- Sign -->
|
||||||
|
<TextBlock Grid.Column="2"
|
||||||
|
Classes="diff-sign"
|
||||||
|
Text="{Binding Sign}"
|
||||||
|
FontFamily="{StaticResource MonoFamily}"
|
||||||
|
FontSize="11"/>
|
||||||
|
<!-- Line text -->
|
||||||
|
<TextBlock Grid.Column="3"
|
||||||
|
Classes="diff-text"
|
||||||
|
Text="{Binding Text}"
|
||||||
|
FontFamily="{StaticResource MonoFamily}"
|
||||||
|
FontSize="11"
|
||||||
|
TextWrapping="NoWrap"/>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
</ScrollViewer>
|
||||||
|
</Grid>
|
||||||
|
</Grid>
|
||||||
|
</Border>
|
||||||
|
</Window>
|
||||||
26
src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml.cs
Normal file
26
src/ClaudeDo.Ui/Views/Modals/DiffModalView.axaml.cs
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
using Avalonia.Controls;
|
||||||
|
using Avalonia.Input;
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
|
{
|
||||||
|
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||||
|
BeginMoveDrag(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user