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:
@@ -6,6 +6,7 @@ using ClaudeDo.Ui;
|
|||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
using ClaudeDo.Ui.ViewModels;
|
using ClaudeDo.Ui.ViewModels;
|
||||||
using ClaudeDo.Ui.ViewModels.Islands;
|
using ClaudeDo.Ui.ViewModels.Islands;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Modals;
|
||||||
using Microsoft.EntityFrameworkCore;
|
using Microsoft.EntityFrameworkCore;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using System;
|
using System;
|
||||||
@@ -78,6 +79,7 @@ sealed class Program
|
|||||||
// ViewModels
|
// ViewModels
|
||||||
sc.AddTransient<ListEditorViewModel>();
|
sc.AddTransient<ListEditorViewModel>();
|
||||||
sc.AddTransient<TaskEditorViewModel>();
|
sc.AddTransient<TaskEditorViewModel>();
|
||||||
|
sc.AddTransient<WorktreeModalViewModel>();
|
||||||
sc.AddSingleton<StatusBarViewModel>();
|
sc.AddSingleton<StatusBarViewModel>();
|
||||||
sc.AddSingleton<TaskDetailViewModel>();
|
sc.AddSingleton<TaskDetailViewModel>();
|
||||||
sc.AddSingleton<TaskListViewModel>(sp =>
|
sc.AddSingleton<TaskListViewModel>(sp =>
|
||||||
|
|||||||
@@ -260,6 +260,35 @@
|
|||||||
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
|
<Setter Property="Foreground" Value="{StaticResource TextDimBrush}" />
|
||||||
</Style>
|
</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 -->
|
<!-- 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;
|
var owner = TopLevel.GetTopLevel(this) as Window;
|
||||||
if (owner == null) return;
|
if (owner == null) return;
|
||||||
var modal = new WorktreeModalView { DataContext = worktreeVm };
|
var modal = new WorktreeModalView { DataContext = worktreeVm };
|
||||||
worktreeVm.CloseCommand.Subscribe(_ => modal.Close());
|
|
||||||
await modal.ShowDialog(owner);
|
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