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>
87 lines
2.9 KiB
C#
87 lines
2.9 KiB
C#
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);
|
|
}
|
|
}
|
|
}
|