feat(claude-do): merge feat(ui): "Worktree öffnen" im Kontextmenü der Task-Zeile

ClaudeDo-Task: 9be17e29e0b54f42ba9385b0cebe89d5
This commit is contained in:
mika kuns
2026-08-21 18:14:45 +02:00
6 changed files with 92 additions and 2 deletions
+3 -1
View File
@@ -167,6 +167,7 @@
"ctxMarkDone": "Erledigt", "ctxMarkDone": "Erledigt",
"ctxMarkCancelled": "Abgebrochen", "ctxMarkCancelled": "Abgebrochen",
"ctxOpenConPtySession": "Interaktive Sitzung öffnen", "ctxOpenConPtySession": "Interaktive Sitzung öffnen",
"ctxOpenWorktree": "Worktree öffnen",
"ctxOpenPlanningSession": "Planungssitzung öffnen", "ctxOpenPlanningSession": "Planungssitzung öffnen",
"ctxResumePlanningSession": "Planungssitzung fortsetzen", "ctxResumePlanningSession": "Planungssitzung fortsetzen",
"ctxFinalizePlanningSession": "Plan finalisieren", "ctxFinalizePlanningSession": "Plan finalisieren",
@@ -199,6 +200,7 @@
"reasonIsPlanningParent": "Teil eines Plans", "reasonIsPlanningParent": "Teil eines Plans",
"reasonIsChildTask": "Teilaufgaben haben keine eigene Planungssitzung", "reasonIsChildTask": "Teilaufgaben haben keine eigene Planungssitzung",
"reasonPlanAlreadyFinalized": "Plan ist bereits finalisiert", "reasonPlanAlreadyFinalized": "Plan ist bereits finalisiert",
"reasonNoWorktree": "Kein Worktree für diesen Task",
"approve": "Genehmigen", "approve": "Genehmigen",
"approveTip": "Genehmigen — als Erledigt markieren", "approveTip": "Genehmigen — als Erledigt markieren",
"reject": "Ablehnen", "reject": "Ablehnen",
@@ -719,7 +721,7 @@
"planningBadge": { "active": "PLANUNG", "finalized": "GEPLANT" }, "planningBadge": { "active": "PLANUNG", "finalized": "GEPLANT" },
"taskRow": { "createdPrefix": "Erstellt {0}", "stepsText": "{0}/{1} Schritte" }, "taskRow": { "createdPrefix": "Erstellt {0}", "stepsText": "{0}/{1} Schritte" },
"queue": { "baseDirtyWarning": "Achtung: Das Repo dieser Liste hat uncommittete Änderungen ({0} geändert, {1} untracked) — ein neuer Worktree startet vom letzten Commit und enthält sie nicht." }, "queue": { "baseDirtyWarning": "Achtung: Das Repo dieser Liste hat uncommittete Änderungen ({0} geändert, {1} untracked) — ein neuer Worktree startet vom letzten Commit und enthält sie nicht." },
"tasksIsland": { "completedHeader": "ABGESCHLOSSEN", "completedHeaderCount": "ABGESCHLOSSEN · {0}", "planningOpenFailed": "Planungssitzung konnte nicht geöffnet werden: {0}", "planningResumeFailed": "Planungssitzung konnte nicht fortgesetzt werden: {0}", "approveFailed": "Genehmigen & Mergen fehlgeschlagen: {0}", "cancelReviewFailed": "Prüfung abbrechen fehlgeschlagen: {0}", "sendToQueueFailed": "In die Warteschlange stellen fehlgeschlagen: {0}", "queuePlanBlockedInteractive": "Plan kann nicht in die Warteschlange gestellt werden — {0} hat eine offene interaktive Sitzung und muss zuerst geschlossen werden.", "moveRunningRejected": "Ein laufender Task kann nicht in eine andere Liste verschoben werden.", "moveWorktreeRejected": "Verschieben nicht möglich — dieser Task hat einen aktiven Worktree, der auf sein aktuelles Repo zeigt.", "moveRepoConfirm": "Unterschiedliche Repos — {0} → {1}. Task trotzdem verschieben?", "moveConfirmUnavailable": "Verschieben nicht möglich — der Bestätigungsdialog ist nicht verfügbar.", "quickClaudeNoWorkingDir": "Für diese Liste ist kein Arbeitsverzeichnis konfiguriert.", "quickClaudeDirMissing": "Arbeitsverzeichnis existiert nicht mehr: {0}", "setStatusFailed": "Status konnte nicht aktualisiert werden: {0}", "cancelFailed": "Abbrechen fehlgeschlagen: {0}", "rejectToQueueFailed": "Zurückweisen in die Warteschlange fehlgeschlagen: {0}", "rejectToIdleFailed": "Zurückweisen fehlgeschlagen: {0}", "deleteTaskConfirm": "\"{0}\" löschen? Dies kann nicht rückgängig gemacht werden.", "deleteTaskFailed": "Löschen fehlgeschlagen: {0}", "toggleDoneFailed": "Erledigt-Status konnte nicht aktualisiert werden: {0}", "removeFromQueueFailed": "Aus der Warteschlange entfernen fehlgeschlagen: {0}" }, "tasksIsland": { "completedHeader": "ABGESCHLOSSEN", "completedHeaderCount": "ABGESCHLOSSEN · {0}", "planningOpenFailed": "Planungssitzung konnte nicht geöffnet werden: {0}", "planningResumeFailed": "Planungssitzung konnte nicht fortgesetzt werden: {0}", "approveFailed": "Genehmigen & Mergen fehlgeschlagen: {0}", "cancelReviewFailed": "Prüfung abbrechen fehlgeschlagen: {0}", "sendToQueueFailed": "In die Warteschlange stellen fehlgeschlagen: {0}", "queuePlanBlockedInteractive": "Plan kann nicht in die Warteschlange gestellt werden — {0} hat eine offene interaktive Sitzung und muss zuerst geschlossen werden.", "moveRunningRejected": "Ein laufender Task kann nicht in eine andere Liste verschoben werden.", "moveWorktreeRejected": "Verschieben nicht möglich — dieser Task hat einen aktiven Worktree, der auf sein aktuelles Repo zeigt.", "moveRepoConfirm": "Unterschiedliche Repos — {0} → {1}. Task trotzdem verschieben?", "moveConfirmUnavailable": "Verschieben nicht möglich — der Bestätigungsdialog ist nicht verfügbar.", "quickClaudeNoWorkingDir": "Für diese Liste ist kein Arbeitsverzeichnis konfiguriert.", "quickClaudeDirMissing": "Arbeitsverzeichnis existiert nicht mehr: {0}", "setStatusFailed": "Status konnte nicht aktualisiert werden: {0}", "cancelFailed": "Abbrechen fehlgeschlagen: {0}", "rejectToQueueFailed": "Zurückweisen in die Warteschlange fehlgeschlagen: {0}", "rejectToIdleFailed": "Zurückweisen fehlgeschlagen: {0}", "deleteTaskConfirm": "\"{0}\" löschen? Dies kann nicht rückgängig gemacht werden.", "deleteTaskFailed": "Löschen fehlgeschlagen: {0}", "toggleDoneFailed": "Erledigt-Status konnte nicht aktualisiert werden: {0}", "removeFromQueueFailed": "Aus der Warteschlange entfernen fehlgeschlagen: {0}", "openWorktreeFailed": "Worktree konnte nicht geöffnet werden: {0}" },
"diff": { "loadFailed": "Diff konnte nicht geladen werden: {0}", "noChanges": "Keine Änderungen anzuzeigen.", "unavailable": "Diff nicht mehr verfügbar — Commit-Bereich unvollständig." }, "diff": { "loadFailed": "Diff konnte nicht geladen werden: {0}", "noChanges": "Keine Änderungen anzuzeigen.", "unavailable": "Diff nicht mehr verfügbar — Commit-Bereich unvollständig." },
"planningDiff": { "hubError": "Kombinierte Vorschau konnte nicht erstellt werden (Hub-Fehler).", "conflict": "Kombinierte Vorschau nicht möglich: Teilaufgabe {0} steht im Konflikt mit einer früheren Teilaufgabe ({1} Dateien).", "buildFailed": "Kombinierte Vorschau konnte nicht erstellt werden: {0}" }, "planningDiff": { "hubError": "Kombinierte Vorschau konnte nicht erstellt werden (Hub-Fehler).", "conflict": "Kombinierte Vorschau nicht möglich: Teilaufgabe {0} steht im Konflikt mit einer früheren Teilaufgabe ({1} Dateien).", "buildFailed": "Kombinierte Vorschau konnte nicht erstellt werden: {0}" },
"merge": { "commitMessage": "chore: merge {0}", "progressMerging": "Wird zusammengeführt…", "progressVerifying": "Verify-Kommando der Liste läuft… ({0})", "workerOfflineBranches": "Worker offline — Branches können nicht aufgelistet werden.", "loadBranchesFailed": "Branches konnten nicht geladen werden: {0}", "merged": "Zusammengeführt.", "conflict": "Merge-Konflikt — Ziel-Branch wiederhergestellt. Manuell oder über Fortsetzen lösen, dann erneut versuchen.", "blocked": "Blockiert: {0}", "verifyFailed": "Merge ist gelandet, aber das Verify-Kommando der Liste ist fehlgeschlagen — die Aufgabe wurde nicht auf Erledigt gesetzt.", "untrackedCollision": "Merge abgelehnt — er würde eine unversionierte Datei im Ziel-Arbeitsverzeichnis überschreiben.", "unknownStatus": "Unbekannter Status: {0}", "mergeFailed": "Merge fehlgeschlagen: {0}" }, "merge": { "commitMessage": "chore: merge {0}", "progressMerging": "Wird zusammengeführt…", "progressVerifying": "Verify-Kommando der Liste läuft… ({0})", "workerOfflineBranches": "Worker offline — Branches können nicht aufgelistet werden.", "loadBranchesFailed": "Branches konnten nicht geladen werden: {0}", "merged": "Zusammengeführt.", "conflict": "Merge-Konflikt — Ziel-Branch wiederhergestellt. Manuell oder über Fortsetzen lösen, dann erneut versuchen.", "blocked": "Blockiert: {0}", "verifyFailed": "Merge ist gelandet, aber das Verify-Kommando der Liste ist fehlgeschlagen — die Aufgabe wurde nicht auf Erledigt gesetzt.", "untrackedCollision": "Merge abgelehnt — er würde eine unversionierte Datei im Ziel-Arbeitsverzeichnis überschreiben.", "unknownStatus": "Unbekannter Status: {0}", "mergeFailed": "Merge fehlgeschlagen: {0}" },
+3 -1
View File
@@ -167,6 +167,7 @@
"ctxMarkDone": "Done", "ctxMarkDone": "Done",
"ctxMarkCancelled": "Cancelled", "ctxMarkCancelled": "Cancelled",
"ctxOpenConPtySession": "Open interactive session", "ctxOpenConPtySession": "Open interactive session",
"ctxOpenWorktree": "Open worktree",
"ctxOpenPlanningSession": "Open planning Session", "ctxOpenPlanningSession": "Open planning Session",
"ctxResumePlanningSession": "Resume planning Session", "ctxResumePlanningSession": "Resume planning Session",
"ctxFinalizePlanningSession": "Finalize plan", "ctxFinalizePlanningSession": "Finalize plan",
@@ -199,6 +200,7 @@
"reasonIsPlanningParent": "Part of a plan", "reasonIsPlanningParent": "Part of a plan",
"reasonIsChildTask": "Subtasks don't have their own planning session", "reasonIsChildTask": "Subtasks don't have their own planning session",
"reasonPlanAlreadyFinalized": "Plan is already finalized", "reasonPlanAlreadyFinalized": "Plan is already finalized",
"reasonNoWorktree": "No worktree for this task",
"approve": "Approve", "approve": "Approve",
"approveTip": "Approve — mark Done", "approveTip": "Approve — mark Done",
"reject": "Reject", "reject": "Reject",
@@ -719,7 +721,7 @@
"planningBadge": { "active": "PLANNING", "finalized": "PLANNED" }, "planningBadge": { "active": "PLANNING", "finalized": "PLANNED" },
"taskRow": { "createdPrefix": "Created {0}", "stepsText": "{0}/{1} steps" }, "taskRow": { "createdPrefix": "Created {0}", "stepsText": "{0}/{1} steps" },
"queue": { "baseDirtyWarning": "Heads up: this list's repo has uncommitted changes ({0} modified, {1} untracked) — a new worktree starts from the last commit and won't include them." }, "queue": { "baseDirtyWarning": "Heads up: this list's repo has uncommitted changes ({0} modified, {1} untracked) — a new worktree starts from the last commit and won't include them." },
"tasksIsland": { "completedHeader": "COMPLETED", "completedHeaderCount": "COMPLETED · {0}", "planningOpenFailed": "Couldn't open planning session: {0}", "planningResumeFailed": "Couldn't resume planning session: {0}", "approveFailed": "Approve & merge failed: {0}", "cancelReviewFailed": "Cancel review failed: {0}", "sendToQueueFailed": "Send to queue failed: {0}", "queuePlanBlockedInteractive": "Can't queue the plan — {0} has an open interactive session and must be closed first.", "moveRunningRejected": "Can't move a running task to another list.", "moveWorktreeRejected": "Can't move — this task has an active worktree pointing at its current repo.", "moveRepoConfirm": "Different repos — {0} → {1}. Move the task anyway?", "moveConfirmUnavailable": "Can't move — the confirmation dialog isn't available.", "quickClaudeNoWorkingDir": "This list has no working directory configured.", "quickClaudeDirMissing": "Working directory no longer exists: {0}", "setStatusFailed": "Failed to update status: {0}", "cancelFailed": "Cancel failed: {0}", "rejectToQueueFailed": "Reject to queue failed: {0}", "rejectToIdleFailed": "Reject failed: {0}", "deleteTaskConfirm": "Delete \"{0}\"? This cannot be undone.", "deleteTaskFailed": "Delete failed: {0}", "toggleDoneFailed": "Couldn't update done state: {0}", "removeFromQueueFailed": "Remove from queue failed: {0}" }, "tasksIsland": { "completedHeader": "COMPLETED", "completedHeaderCount": "COMPLETED · {0}", "planningOpenFailed": "Couldn't open planning session: {0}", "planningResumeFailed": "Couldn't resume planning session: {0}", "approveFailed": "Approve & merge failed: {0}", "cancelReviewFailed": "Cancel review failed: {0}", "sendToQueueFailed": "Send to queue failed: {0}", "queuePlanBlockedInteractive": "Can't queue the plan — {0} has an open interactive session and must be closed first.", "moveRunningRejected": "Can't move a running task to another list.", "moveWorktreeRejected": "Can't move — this task has an active worktree pointing at its current repo.", "moveRepoConfirm": "Different repos — {0} → {1}. Move the task anyway?", "moveConfirmUnavailable": "Can't move — the confirmation dialog isn't available.", "quickClaudeNoWorkingDir": "This list has no working directory configured.", "quickClaudeDirMissing": "Working directory no longer exists: {0}", "setStatusFailed": "Failed to update status: {0}", "cancelFailed": "Cancel failed: {0}", "rejectToQueueFailed": "Reject to queue failed: {0}", "rejectToIdleFailed": "Reject failed: {0}", "deleteTaskConfirm": "Delete \"{0}\"? This cannot be undone.", "deleteTaskFailed": "Delete failed: {0}", "toggleDoneFailed": "Couldn't update done state: {0}", "removeFromQueueFailed": "Remove from queue failed: {0}", "openWorktreeFailed": "Couldn't open worktree: {0}" },
"diff": { "loadFailed": "Failed to load diff: {0}", "noChanges": "No changes to show.", "unavailable": "Diff no longer available — commit range incomplete." }, "diff": { "loadFailed": "Failed to load diff: {0}", "noChanges": "No changes to show.", "unavailable": "Diff no longer available — commit range incomplete." },
"planningDiff": { "hubError": "Could not build combined preview (hub error).", "conflict": "Cannot build combined preview: subtask {0} conflicts with an earlier subtask ({1} files).", "buildFailed": "Could not build combined preview: {0}" }, "planningDiff": { "hubError": "Could not build combined preview (hub error).", "conflict": "Cannot build combined preview: subtask {0} conflicts with an earlier subtask ({1} files).", "buildFailed": "Could not build combined preview: {0}" },
"merge": { "commitMessage": "chore: merge {0}", "progressMerging": "Merging…", "progressVerifying": "Running the list's verify command… ({0})", "workerOfflineBranches": "Worker offline — cannot list branches.", "loadBranchesFailed": "Failed to load branches: {0}", "merged": "Merged.", "conflict": "Merge conflict — target branch restored. Resolve manually or via Continue, then retry.", "blocked": "Blocked: {0}", "verifyFailed": "Merge landed, but the list's verify command failed — the task was kept out of Done.", "untrackedCollision": "Merge refused — it would overwrite an untracked file in the target working directory.", "unknownStatus": "Unknown status: {0}", "mergeFailed": "Merge failed: {0}" }, "merge": { "commitMessage": "chore: merge {0}", "progressMerging": "Merging…", "progressVerifying": "Running the list's verify command… ({0})", "workerOfflineBranches": "Worker offline — cannot list branches.", "loadBranchesFailed": "Failed to load branches: {0}", "merged": "Merged.", "conflict": "Merge conflict — target branch restored. Resolve manually or via Continue, then retry.", "blocked": "Blocked: {0}", "verifyFailed": "Merge landed, but the list's verify command failed — the task was kept out of Done.", "untrackedCollision": "Merge refused — it would overwrite an untracked file in the target working directory.", "unknownStatus": "Unknown status: {0}", "mergeFailed": "Merge failed: {0}" },
@@ -20,6 +20,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
[ObservableProperty] private string? _branch; [ObservableProperty] private string? _branch;
[ObservableProperty] private string? _diffStat; [ObservableProperty] private string? _diffStat;
[ObservableProperty] private ClaudeDo.Data.Models.WorktreeState? _worktreeState; [ObservableProperty] private ClaudeDo.Data.Models.WorktreeState? _worktreeState;
[ObservableProperty] private string? _worktreePath;
[ObservableProperty] private DateTime? _scheduledFor; [ObservableProperty] private DateTime? _scheduledFor;
[ObservableProperty] private int _diffAdditions; [ObservableProperty] private int _diffAdditions;
[ObservableProperty] private int _diffDeletions; [ObservableProperty] private int _diffDeletions;
@@ -179,6 +180,9 @@ public sealed partial class TaskRowViewModel : ViewModelBase
// placeholder so the menu wiring is uniform; always null until a real gate exists. // placeholder so the menu wiring is uniform; always null until a real gate exists.
public string? QuickSessionDisabledReason => null; public string? QuickSessionDisabledReason => null;
public bool CanOpenWorktree => !string.IsNullOrWhiteSpace(WorktreePath);
public string? OpenWorktreeDisabledReason => CanOpenWorktree ? null : Loc.T("tasks.reasonNoWorktree");
public string? RefineDisabledReason public string? RefineDisabledReason
{ {
get get
@@ -425,6 +429,11 @@ public sealed partial class TaskRowViewModel : ViewModelBase
partial void OnNumberChanged(int value) => OnPropertyChanged(nameof(ShowNumberBadge)); partial void OnNumberChanged(int value) => OnPropertyChanged(nameof(ShowNumberBadge));
partial void OnBranchChanged(string? value) => OnPropertyChanged(nameof(HasBranch)); partial void OnBranchChanged(string? value) => OnPropertyChanged(nameof(HasBranch));
partial void OnWorktreePathChanged(string? value)
{
OnPropertyChanged(nameof(CanOpenWorktree));
OnPropertyChanged(nameof(OpenWorktreeDisabledReason));
}
partial void OnWorktreeStateChanged(ClaudeDo.Data.Models.WorktreeState? value) partial void OnWorktreeStateChanged(ClaudeDo.Data.Models.WorktreeState? value)
{ {
OnPropertyChanged(nameof(IsParked)); OnPropertyChanged(nameof(IsParked));
@@ -485,6 +494,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
Branch = t.Worktree?.BranchName; Branch = t.Worktree?.BranchName;
DiffStat = t.Worktree?.DiffStat; DiffStat = t.Worktree?.DiffStat;
WorktreeState = t.Worktree?.State; WorktreeState = t.Worktree?.State;
WorktreePath = t.Worktree?.Path;
ScheduledFor = t.ScheduledFor; ScheduledFor = t.ScheduledFor;
DiffAdditions = add; DiffAdditions = add;
DiffDeletions = del; DiffDeletions = del;
@@ -1429,6 +1429,21 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
OpenConPtySessionRequested?.Invoke(row.Id); OpenConPtySessionRequested?.Invoke(row.Id);
} }
[RelayCommand]
private void OpenTaskWorktree(TaskRowViewModel? row)
{
if (row?.WorktreePath is not { } path || string.IsNullOrWhiteSpace(path)) return;
try
{
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
{
FileName = path,
UseShellExecute = true,
});
}
catch (Exception ex) { ErrorReported?.Invoke(Loc.T("vm.tasksIsland.openWorktreeFailed", ex.Message)); }
}
[RelayCommand] [RelayCommand]
private async Task ResumePlanningSessionAsync(TaskRowViewModel? row) private async Task ResumePlanningSessionAsync(TaskRowViewModel? row)
{ {
@@ -61,6 +61,7 @@ public partial class TaskRowView : UserControl
menu.Items.Add(removeFromQueueItem); menu.Items.Add(removeFromQueueItem);
menu.Items.Add(MakeItem("tasks.ctxCancelExecution", OnCancelExecutionClick, reason: row.CancelDisabledReason)); menu.Items.Add(MakeItem("tasks.ctxCancelExecution", OnCancelExecutionClick, reason: row.CancelDisabledReason));
menu.Items.Add(MakeItem("tasks.ctxOpenConPtySession", OnOpenConPtySessionClick, reason: row.QuickSessionDisabledReason)); menu.Items.Add(MakeItem("tasks.ctxOpenConPtySession", OnOpenConPtySessionClick, reason: row.QuickSessionDisabledReason));
menu.Items.Add(MakeItem("tasks.ctxOpenWorktree", OnOpenWorktreeClick, reason: row.OpenWorktreeDisabledReason));
menu.Items.Add(MakeItem("tasks.ctxRefineTask", OnRefineTaskClick, reason: row.RefineDisabledReason)); menu.Items.Add(MakeItem("tasks.ctxRefineTask", OnRefineTaskClick, reason: row.RefineDisabledReason));
menu.Items.Add(new Separator()); menu.Items.Add(new Separator());
@@ -169,6 +170,12 @@ public partial class TaskRowView : UserControl
vm.OpenConPtySessionCommand.Execute(row); vm.OpenConPtySessionCommand.Execute(row);
} }
private void OnOpenWorktreeClick(object? sender, RoutedEventArgs e)
{
if (DataContext is TaskRowViewModel row && FindTasksVm() is { } vm)
vm.OpenTaskWorktreeCommand.Execute(row);
}
// The status chip is only actionable while a ConPTY session is open; the open command dedupes // The status chip is only actionable while a ConPTY session is open; the open command dedupes
// by task id, so it surfaces Mission Control and focuses the existing pane. // by task id, so it surfaces Mission Control and focuses the existing pane.
private void OnStatusChipTapped(object? sender, RoutedEventArgs e) private void OnStatusChipTapped(object? sender, RoutedEventArgs e)
@@ -64,6 +64,60 @@ public class TaskRowViewModelTests
Assert.Contains(nameof(TaskRowViewModel.ShowNumberBadge), raised); Assert.Contains(nameof(TaskRowViewModel.ShowNumberBadge), raised);
} }
[Fact]
public void CanOpenWorktree_True_And_ReasonNull_When_Path_Present()
{
var vm = new TaskRowViewModel { Id = "t" };
vm.WorktreePath = @"C:\repo\worktrees\t";
Assert.True(vm.CanOpenWorktree);
Assert.Null(vm.OpenWorktreeDisabledReason);
}
[Theory]
[InlineData(null)]
[InlineData("")]
[InlineData(" ")]
public void CanOpenWorktree_False_And_ReasonSet_When_Path_Missing(string? path)
{
var vm = new TaskRowViewModel { Id = "t" };
vm.WorktreePath = path;
Assert.False(vm.CanOpenWorktree);
Assert.NotNull(vm.OpenWorktreeDisabledReason);
}
[Fact]
public void OpenWorktreeGate_Raises_PropertyChanged_On_WorktreePath_Change()
{
var vm = new TaskRowViewModel { Id = "t" };
var raised = new List<string?>();
vm.PropertyChanged += (_, e) => raised.Add(e.PropertyName);
vm.WorktreePath = @"C:\repo\worktrees\t";
Assert.Contains(nameof(TaskRowViewModel.CanOpenWorktree), raised);
Assert.Contains(nameof(TaskRowViewModel.OpenWorktreeDisabledReason), raised);
}
[Fact]
public void WorktreePath_SetFromEntity_MapsFromWorktreePath()
{
var entity = new TaskEntity
{
Id = "t", ListId = "l1", Title = "Task", CreatedAt = DateTime.UtcNow,
Worktree = new WorktreeEntity
{
TaskId = "t", Path = @"C:\repo\worktrees\t", BranchName = "b",
BaseCommit = "abc123", CreatedAt = DateTime.UtcNow,
},
};
var vm = TaskRowViewModel.FromEntity(entity);
Assert.Equal(@"C:\repo\worktrees\t", vm.WorktreePath);
Assert.True(vm.CanOpenWorktree);
}
[Fact] [Fact]
public void IsDropTarget_Follows_Either_DropHint_And_Raises_PropertyChanged() public void IsDropTarget_Follows_Either_DropHint_And_Raises_PropertyChanged()
{ {