feat(ui): worktree modal with tree view and M/A badges
Adds WorktreeModalView/ViewModel showing git status --porcelain as a recursive file tree with M/A/D/? status badges. Wires the Worktree button in AgentStripView to OpenWorktreeCommand on DetailsIslandViewModel. Adds GetStatusPorcelainAsync to GitService. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -260,6 +260,35 @@
|
||||
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
|
||||
</Style>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- WORKTREE MODAL STATUS BADGES -->
|
||||
<!-- Tag="M" → peat, "A" → moss, "D" → blood, "?" → faint -->
|
||||
<!-- ============================================================ -->
|
||||
<Style Selector="Border[Tag=M]">
|
||||
<Setter Property="Background" Value="#26A06040"/>
|
||||
</Style>
|
||||
<Style Selector="Border[Tag=M] > TextBlock">
|
||||
<Setter Property="Foreground" Value="{StaticResource PeatBrush}"/>
|
||||
</Style>
|
||||
<Style Selector="Border[Tag=A]">
|
||||
<Setter Property="Background" Value="#267C9166"/>
|
||||
</Style>
|
||||
<Style Selector="Border[Tag=A] > TextBlock">
|
||||
<Setter Property="Foreground" Value="{StaticResource MossBrush}"/>
|
||||
</Style>
|
||||
<Style Selector="Border[Tag=D]">
|
||||
<Setter Property="Background" Value="#26C87060"/>
|
||||
</Style>
|
||||
<Style Selector="Border[Tag=D] > TextBlock">
|
||||
<Setter Property="Foreground" Value="{StaticResource BloodBrush}"/>
|
||||
</Style>
|
||||
<Style Selector="Border[Tag=?]">
|
||||
<Setter Property="Background" Value="#1A888888"/>
|
||||
</Style>
|
||||
<Style Selector="Border[Tag=?] > TextBlock">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextFaintBrush}"/>
|
||||
</Style>
|
||||
|
||||
<!-- ============================================================ -->
|
||||
<!-- LIST NAV ITEM -->
|
||||
<!-- ============================================================ -->
|
||||
|
||||
86
src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs
Normal file
86
src/ClaudeDo.Ui/ViewModels/Modals/WorktreeModalViewModel.cs
Normal file
@@ -0,0 +1,86 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
using ClaudeDo.Data.Git;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels.Modals;
|
||||
|
||||
public sealed partial class WorktreeNodeViewModel : ViewModelBase
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public string? Status { get; init; }
|
||||
public bool IsDirectory { get; init; }
|
||||
public ObservableCollection<WorktreeNodeViewModel> Children { get; } = new();
|
||||
}
|
||||
|
||||
public sealed partial class WorktreeModalViewModel : ViewModelBase
|
||||
{
|
||||
private readonly GitService _git;
|
||||
|
||||
public ObservableCollection<WorktreeNodeViewModel> Root { get; } = new();
|
||||
|
||||
[ObservableProperty] private string _worktreePath = "";
|
||||
|
||||
// Set by the view (same pattern as DiffModalViewModel.CloseAction)
|
||||
public Action? CloseAction { get; set; }
|
||||
|
||||
public WorktreeModalViewModel(GitService git)
|
||||
{
|
||||
_git = git;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void Close() => CloseAction?.Invoke();
|
||||
|
||||
public async Task LoadAsync(CancellationToken ct = default)
|
||||
{
|
||||
Root.Clear();
|
||||
|
||||
string stdout;
|
||||
try { stdout = await _git.GetStatusPorcelainAsync(WorktreePath, ct); }
|
||||
catch { return; }
|
||||
|
||||
if (string.IsNullOrWhiteSpace(stdout)) return;
|
||||
|
||||
var dirs = new Dictionary<string, WorktreeNodeViewModel>(StringComparer.Ordinal);
|
||||
|
||||
foreach (var line in stdout.Split('\n', StringSplitOptions.RemoveEmptyEntries))
|
||||
{
|
||||
if (line.Length < 4) continue;
|
||||
|
||||
// porcelain format: XY<space>path (XY = two-char status)
|
||||
var xy = line[..2];
|
||||
// Pick staged char first, fall back to unstaged
|
||||
var statusChar = xy[0] != ' ' ? xy[0] : xy[1];
|
||||
var status = statusChar != ' ' ? statusChar.ToString() : null;
|
||||
var path = line[3..].Trim().Replace('\\', '/');
|
||||
|
||||
var segments = path.Split('/', StringSplitOptions.RemoveEmptyEntries);
|
||||
if (segments.Length == 0) continue;
|
||||
|
||||
WorktreeNodeViewModel? parent = null;
|
||||
var accumulated = "";
|
||||
for (var i = 0; i < segments.Length - 1; i++)
|
||||
{
|
||||
accumulated = accumulated.Length == 0 ? segments[i] : accumulated + "/" + segments[i];
|
||||
if (!dirs.TryGetValue(accumulated, out var dir))
|
||||
{
|
||||
dir = new WorktreeNodeViewModel { Name = segments[i], IsDirectory = true };
|
||||
dirs[accumulated] = dir;
|
||||
if (parent == null) Root.Add(dir);
|
||||
else parent.Children.Add(dir);
|
||||
}
|
||||
parent = dir;
|
||||
}
|
||||
|
||||
var leaf = new WorktreeNodeViewModel
|
||||
{
|
||||
Name = segments[^1],
|
||||
Status = status,
|
||||
IsDirectory = false
|
||||
};
|
||||
if (parent == null) Root.Add(leaf);
|
||||
else parent.Children.Add(leaf);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -30,7 +30,6 @@ public partial class AgentStripView : UserControl
|
||||
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);
|
||||
};
|
||||
}
|
||||
|
||||
66
src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml
Normal file
66
src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml
Normal file
@@ -0,0 +1,66 @@
|
||||
<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.WorktreeModalView"
|
||||
x:DataType="vm:WorktreeModalViewModel"
|
||||
Title="Worktree"
|
||||
Width="640" Height="720"
|
||||
WindowStartupLocation="CenterOwner"
|
||||
SystemDecorations="None"
|
||||
ExtendClientAreaToDecorationsHint="True"
|
||||
Background="Transparent"
|
||||
CanResize="False"
|
||||
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"
|
||||
PointerPressed="OnTitleBarPressed">
|
||||
<Grid ColumnDefinitions="*,Auto" Margin="14,0">
|
||||
<TextBlock Grid.Column="0" Text="Worktree" VerticalAlignment="Center"
|
||||
FontFamily="{DynamicResource MonoFont}" FontSize="12"
|
||||
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 Text="{Binding WorktreePath}"
|
||||
FontFamily="{DynamicResource MonoFont}" FontSize="11"
|
||||
Foreground="{DynamicResource TextFaintBrush}"
|
||||
TextTrimming="CharacterEllipsis"/>
|
||||
</Border>
|
||||
|
||||
<!-- File tree -->
|
||||
<TreeView DockPanel.Dock="Top" ItemsSource="{Binding Root}"
|
||||
Background="Transparent" Margin="8,0,8,8">
|
||||
<TreeView.ItemTemplate>
|
||||
<TreeDataTemplate DataType="vm:WorktreeNodeViewModel"
|
||||
ItemsSource="{Binding Children}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<TextBlock Text="{Binding Name}"
|
||||
FontFamily="{DynamicResource MonoFont}" FontSize="12"
|
||||
Foreground="{DynamicResource TextBrush}"/>
|
||||
<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="10"
|
||||
Foreground="{DynamicResource TextBrush}"/>
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</TreeDataTemplate>
|
||||
</TreeView.ItemTemplate>
|
||||
</TreeView>
|
||||
|
||||
</DockPanel>
|
||||
</Border>
|
||||
</Window>
|
||||
26
src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.axaml.cs
Normal file
26
src/ClaudeDo.Ui/Views/Modals/WorktreeModalView.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 WorktreeModalView : Window
|
||||
{
|
||||
public WorktreeModalView()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
|
||||
protected override void OnDataContextChanged(EventArgs e)
|
||||
{
|
||||
base.OnDataContextChanged(e);
|
||||
if (DataContext is WorktreeModalViewModel vm)
|
||||
vm.CloseAction = Close;
|
||||
}
|
||||
|
||||
private void OnTitleBarPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
BeginMoveDrag(e);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user