refactor(claude-do): merge Dedupe: ShellOpen-Helper für "Pfad im Explorer öffnen" (5 Co
ClaudeDo-Task: 4084c060-3cf0-4474-930a-ea79695b9ba6
This commit is contained in:
@@ -740,10 +740,12 @@
|
|||||||
"filesTab": { "workerOffline": "Worker offline.", "noneBundled": "Keine Standard-Agenten mitgeliefert.", "allPresent": "Alle Standard-Agenten bereits vorhanden.", "restored": "{0} Standard-Agent(en) wiederhergestellt.", "restoreFailed": "Wiederherstellung fehlgeschlagen: {0}", "resetToDefault": "Auf den mitgelieferten Standard zurückgesetzt." },
|
"filesTab": { "workerOffline": "Worker offline.", "noneBundled": "Keine Standard-Agenten mitgeliefert.", "allPresent": "Alle Standard-Agenten bereits vorhanden.", "restored": "{0} Standard-Agent(en) wiederhergestellt.", "restoreFailed": "Wiederherstellung fehlgeschlagen: {0}", "resetToDefault": "Auf den mitgelieferten Standard zurückgesetzt." },
|
||||||
"sessionSkillsTab": { "installed": "Installiert: {0}", "installFailed": "Installation fehlgeschlagen: {0}", "updated": "Aktualisiert.", "updateFailed": "Aktualisierung fehlgeschlagen: {0}", "removed": "Entfernt.", "removeFailed": "Entfernen fehlgeschlagen: {0}" },
|
"sessionSkillsTab": { "installed": "Installiert: {0}", "installFailed": "Installation fehlgeschlagen: {0}", "updated": "Aktualisiert.", "updateFailed": "Aktualisierung fehlgeschlagen: {0}", "removed": "Entfernt.", "removeFailed": "Entfernen fehlgeschlagen: {0}" },
|
||||||
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "{0} Worktree(s) entfernt.", "blocked": "Zwangsentfernung nicht möglich: {0} Aufgabe(n) laufen noch. Brich sie zuerst ab.", "removedFrom": "{0} Worktree(s) von {1} Aufgabe(n) entfernt.", "cleanupFailed": "Aufräumen fehlgeschlagen: {0}", "resetFailed": "Zurücksetzen fehlgeschlagen: {0}" },
|
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "{0} Worktree(s) entfernt.", "blocked": "Zwangsentfernung nicht möglich: {0} Aufgabe(n) laufen noch. Brich sie zuerst ab.", "removedFrom": "{0} Worktree(s) von {1} Aufgabe(n) entfernt.", "cleanupFailed": "Aufräumen fehlgeschlagen: {0}", "resetFailed": "Zurücksetzen fehlgeschlagen: {0}" },
|
||||||
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "Liste", "cleanupFailed": "Aufräumen fehlgeschlagen.", "cleanupFailedDetailed": "Aufräumen fehlgeschlagen: {0}", "removed": "{0} Worktree(s) entfernt.", "discardConfirm": "Worktree für \"{0}\" verwerfen? Nicht committete Arbeit geht verloren.", "discardFailed": "Worktree konnte nicht verworfen werden.", "keepFailed": "Worktree konnte nicht behalten werden.", "cannotForceRunning": "Eine laufende Aufgabe kann nicht zwangsweise entfernt werden.", "forceRemoveFailed": "Zwangsentfernung fehlgeschlagen.", "forceRemoveFailedDetailed": "Zwangsentfernung fehlgeschlagen: {0}", "batchProgress": "Merge {0}/{1}…", "batchDone": "{0} gemergt, {1} zu lösen." },
|
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "Liste", "cleanupFailed": "Aufräumen fehlgeschlagen.", "cleanupFailedDetailed": "Aufräumen fehlgeschlagen: {0}", "removed": "{0} Worktree(s) entfernt.", "discardConfirm": "Worktree für \"{0}\" verwerfen? Nicht committete Arbeit geht verloren.", "discardFailed": "Worktree konnte nicht verworfen werden.", "keepFailed": "Worktree konnte nicht behalten werden.", "cannotForceRunning": "Eine laufende Aufgabe kann nicht zwangsweise entfernt werden.", "forceRemoveFailed": "Zwangsentfernung fehlgeschlagen.", "forceRemoveFailedDetailed": "Zwangsentfernung fehlgeschlagen: {0}", "batchProgress": "Merge {0}/{1}…", "batchDone": "{0} gemergt, {1} zu lösen.", "openInExplorerFailed": "Pfad konnte nicht geöffnet werden: {0}" },
|
||||||
"listSettings": { "untitled": "Unbenannt" },
|
"listSettings": { "untitled": "Unbenannt" },
|
||||||
"detailsIsland": { "verifyFailed": "Merge ist erfolgt, aber das Verifikationskommando der Liste ist fehlgeschlagen — die Aufgabe wurde nicht auf 'Erledigt' gesetzt.", "untrackedCollision": "Merge abgelehnt — er würde eine unversionierte Datei im Ziel-Arbeitsverzeichnis überschreiben.", "stopOffline": "Worker offline — Task kann nicht gestoppt werden.", "stopFailed": "Stoppen fehlgeschlagen: {0}", "enqueueFailed": "In die Warteschlange stellen fehlgeschlagen: {0}", "dequeueFailed": "Aus der Warteschlange entfernen fehlgeschlagen: {0}", "resetAndRetryFailed": "Zurücksetzen & erneut versuchen fehlgeschlagen: {0}", "toggleDoneFailed": "Erledigt-Status konnte nicht aktualisiert werden: {0}" },
|
"detailsIsland": { "verifyFailed": "Merge ist erfolgt, aber das Verifikationskommando der Liste ist fehlgeschlagen — die Aufgabe wurde nicht auf 'Erledigt' gesetzt.", "untrackedCollision": "Merge abgelehnt — er würde eine unversionierte Datei im Ziel-Arbeitsverzeichnis überschreiben.", "stopOffline": "Worker offline — Task kann nicht gestoppt werden.", "stopFailed": "Stoppen fehlgeschlagen: {0}", "enqueueFailed": "In die Warteschlange stellen fehlgeschlagen: {0}", "dequeueFailed": "Aus der Warteschlange entfernen fehlgeschlagen: {0}", "resetAndRetryFailed": "Zurücksetzen & erneut versuchen fehlgeschlagen: {0}", "toggleDoneFailed": "Erledigt-Status konnte nicht aktualisiert werden: {0}" },
|
||||||
"lists": { "localSuffix": "{0} / lokal", "smartMyDay": "Mein Tag", "smartImportant": "Wichtig", "smartPlanned": "Geplant", "virtualQueue": "Warteschlange", "virtualRunning": "Läuft", "virtualReview": "Prüfung", "newList": "Neue Liste", "findingsNotFound": "Für diese Liste gibt es noch keine Findings.", "findingsOpenFailed": "Findings-Ordner konnte nicht geöffnet werden: {0}" },
|
"mergeSection": { "openWorktreeFailed": "Worktree konnte nicht geöffnet werden: {0}" },
|
||||||
|
"about": { "openPathFailed": "Pfad konnte nicht geöffnet werden: {0}" },
|
||||||
|
"lists": { "localSuffix": "{0} / lokal", "smartMyDay": "Mein Tag", "smartImportant": "Wichtig", "smartPlanned": "Geplant", "virtualQueue": "Warteschlange", "virtualRunning": "Läuft", "virtualReview": "Prüfung", "newList": "Neue Liste", "findingsNotFound": "Für diese Liste gibt es noch keine Findings.", "findingsOpenFailed": "Findings-Ordner konnte nicht geöffnet werden: {0}", "openInExplorerFailed": "Pfad konnte nicht geöffnet werden: {0}" },
|
||||||
"repoImport": { "loadFailed": "Gespeicherte Ordner konnten nicht geladen werden: {0}", "saveFailed": "Ordner konnten nicht gespeichert werden: {0}" }
|
"repoImport": { "loadFailed": "Gespeicherte Ordner konnten nicht geladen werden: {0}", "saveFailed": "Ordner konnten nicht gespeichert werden: {0}" }
|
||||||
},
|
},
|
||||||
"ops": {
|
"ops": {
|
||||||
|
|||||||
@@ -740,10 +740,12 @@
|
|||||||
"filesTab": { "workerOffline": "Worker offline.", "noneBundled": "No default agents bundled.", "allPresent": "All default agents already present.", "restored": "Restored {0} default agent(s).", "restoreFailed": "Restore failed: {0}", "resetToDefault": "Reset to the bundled default." },
|
"filesTab": { "workerOffline": "Worker offline.", "noneBundled": "No default agents bundled.", "allPresent": "All default agents already present.", "restored": "Restored {0} default agent(s).", "restoreFailed": "Restore failed: {0}", "resetToDefault": "Reset to the bundled default." },
|
||||||
"sessionSkillsTab": { "installed": "Installed: {0}", "installFailed": "Install failed: {0}", "updated": "Updated.", "updateFailed": "Update failed: {0}", "removed": "Removed.", "removeFailed": "Remove failed: {0}" },
|
"sessionSkillsTab": { "installed": "Installed: {0}", "installFailed": "Install failed: {0}", "updated": "Updated.", "updateFailed": "Update failed: {0}", "removed": "Removed.", "removeFailed": "Remove failed: {0}" },
|
||||||
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "Removed {0} worktree(s).", "blocked": "Cannot force-remove: {0} task(s) still running. Cancel them first.", "removedFrom": "Removed {0} worktree(s) from {1} task(s).", "cleanupFailed": "Cleanup failed: {0}", "resetFailed": "Reset failed: {0}" },
|
"worktreesTab": { "workerOffline": "Worker offline.", "removed": "Removed {0} worktree(s).", "blocked": "Cannot force-remove: {0} task(s) still running. Cancel them first.", "removedFrom": "Removed {0} worktree(s) from {1} task(s).", "cleanupFailed": "Cleanup failed: {0}", "resetFailed": "Reset failed: {0}" },
|
||||||
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "list", "cleanupFailed": "Cleanup failed.", "cleanupFailedDetailed": "Cleanup failed: {0}", "removed": "Removed {0} worktree(s).", "discardConfirm": "Discard the worktree for \"{0}\"? Uncommitted work will be lost.", "discardFailed": "Failed to discard worktree.", "keepFailed": "Failed to keep worktree.", "cannotForceRunning": "Cannot force-remove a running task.", "forceRemoveFailed": "Force remove failed.", "forceRemoveFailedDetailed": "Force remove failed: {0}", "batchProgress": "Merging {0}/{1}…", "batchDone": "Merged {0}, {1} need resolution." },
|
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "list", "cleanupFailed": "Cleanup failed.", "cleanupFailedDetailed": "Cleanup failed: {0}", "removed": "Removed {0} worktree(s).", "discardConfirm": "Discard the worktree for \"{0}\"? Uncommitted work will be lost.", "discardFailed": "Failed to discard worktree.", "keepFailed": "Failed to keep worktree.", "cannotForceRunning": "Cannot force-remove a running task.", "forceRemoveFailed": "Force remove failed.", "forceRemoveFailedDetailed": "Force remove failed: {0}", "batchProgress": "Merging {0}/{1}…", "batchDone": "Merged {0}, {1} need resolution.", "openInExplorerFailed": "Couldn't open path: {0}" },
|
||||||
"listSettings": { "untitled": "Untitled" },
|
"listSettings": { "untitled": "Untitled" },
|
||||||
"detailsIsland": { "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.", "stopOffline": "Worker offline — can't stop the task.", "stopFailed": "Stop failed: {0}", "enqueueFailed": "Queue failed: {0}", "dequeueFailed": "Remove from queue failed: {0}", "resetAndRetryFailed": "Reset & retry failed: {0}", "toggleDoneFailed": "Couldn't update done state: {0}" },
|
"detailsIsland": { "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.", "stopOffline": "Worker offline — can't stop the task.", "stopFailed": "Stop failed: {0}", "enqueueFailed": "Queue failed: {0}", "dequeueFailed": "Remove from queue failed: {0}", "resetAndRetryFailed": "Reset & retry failed: {0}", "toggleDoneFailed": "Couldn't update done state: {0}" },
|
||||||
"lists": { "localSuffix": "{0} / local", "smartMyDay": "My Day", "smartImportant": "Important", "smartPlanned": "Planned", "virtualQueue": "Queue", "virtualRunning": "Running", "virtualReview": "Review", "newList": "New list", "findingsNotFound": "No findings yet for this list.", "findingsOpenFailed": "Couldn't open findings folder: {0}" },
|
"mergeSection": { "openWorktreeFailed": "Couldn't open worktree: {0}" },
|
||||||
|
"about": { "openPathFailed": "Couldn't open path: {0}" },
|
||||||
|
"lists": { "localSuffix": "{0} / local", "smartMyDay": "My Day", "smartImportant": "Important", "smartPlanned": "Planned", "virtualQueue": "Queue", "virtualRunning": "Running", "virtualReview": "Review", "newList": "New list", "findingsNotFound": "No findings yet for this list.", "findingsOpenFailed": "Couldn't open findings folder: {0}", "openInExplorerFailed": "Couldn't open path: {0}" },
|
||||||
"repoImport": { "loadFailed": "Couldn't load remembered folders: {0}", "saveFailed": "Couldn't save folders: {0}" }
|
"repoImport": { "loadFailed": "Couldn't load remembered folders: {0}", "saveFailed": "Couldn't save folders: {0}" }
|
||||||
},
|
},
|
||||||
"ops": {
|
"ops": {
|
||||||
|
|||||||
@@ -0,0 +1,24 @@
|
|||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Services;
|
||||||
|
|
||||||
|
public static class ShellOpen
|
||||||
|
{
|
||||||
|
// "nothing to open" (blank/missing path) is not an error — callers stay silent, matching the
|
||||||
|
// existing Directory.Exists/File.Exists guards this helper replaces.
|
||||||
|
public static (bool Ok, string? Error) Path(string? path)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(path)) return (false, null);
|
||||||
|
if (!File.Exists(path) && !Directory.Exists(path)) return (false, null);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Process.Start(new ProcessStartInfo { FileName = path, UseShellExecute = true });
|
||||||
|
return (true, null);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
return (false, ex.Message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -339,6 +339,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
AgentSettings = new AgentConfigEditorViewModel(worker, AgentConfigScope.Task);
|
AgentSettings = new AgentConfigEditorViewModel(worker, AgentConfigScope.Task);
|
||||||
Merge = new MergeSectionViewModel(worker, services);
|
Merge = new MergeSectionViewModel(worker, services);
|
||||||
Merge.DiffViewed = () => ReviewDiffViewed = true;
|
Merge.DiffViewed = () => ReviewDiffViewed = true;
|
||||||
|
Merge.ErrorReported += msg => ErrorReported?.Invoke(msg);
|
||||||
Merge.PropertyChanged += (_, e) =>
|
Merge.PropertyChanged += (_, e) =>
|
||||||
{
|
{
|
||||||
if (e.PropertyName == nameof(MergeSectionViewModel.HasReviewableDiff))
|
if (e.PropertyName == nameof(MergeSectionViewModel.HasReviewableDiff))
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
{
|
{
|
||||||
var rawId = row.Id.StartsWith("user:", StringComparison.Ordinal) ? row.Id["user:".Length..] : row.Id;
|
var rawId = row.Id.StartsWith("user:", StringComparison.Ordinal) ? row.Id["user:".Length..] : row.Id;
|
||||||
var vm = _services.GetRequiredService<WorktreesOverviewModalViewModel>();
|
var vm = _services.GetRequiredService<WorktreesOverviewModalViewModel>();
|
||||||
|
vm.ErrorReported += msg => ErrorReported?.Invoke(msg);
|
||||||
vm.Configure(rawId, row.Name);
|
vm.Configure(rawId, row.Name);
|
||||||
await vm.LoadAsync();
|
await vm.LoadAsync();
|
||||||
await Dialogs.ShowWorktreesOverviewAsync(vm);
|
await Dialogs.ShowWorktreesOverviewAsync(vm);
|
||||||
@@ -107,17 +108,9 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void OpenInExplorer(ListNavItemViewModel? row)
|
private void OpenInExplorer(ListNavItemViewModel? row)
|
||||||
{
|
{
|
||||||
var dir = row?.WorkingDir;
|
var (ok, error) = ShellOpen.Path(row?.WorkingDir);
|
||||||
if (string.IsNullOrWhiteSpace(dir) || !System.IO.Directory.Exists(dir)) return;
|
if (!ok && error is not null)
|
||||||
try
|
ErrorReported?.Invoke(Loc.T("vm.lists.openInExplorerFailed", error));
|
||||||
{
|
|
||||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = dir,
|
|
||||||
UseShellExecute = true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch { /* best-effort */ }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
|
|||||||
@@ -60,6 +60,10 @@ public sealed partial class MergeSectionViewModel : ViewModelBase
|
|||||||
// review gate can record that the changes were inspected before merging.
|
// review gate can record that the changes were inspected before merging.
|
||||||
public Action? DiffViewed { get; set; }
|
public Action? DiffViewed { get; set; }
|
||||||
|
|
||||||
|
// Mirrors ListsIslandViewModel/TasksIslandViewModel.ErrorReported — DetailsIslandViewModel
|
||||||
|
// forwards this into its own ErrorReported, which the shell flashes in the footer strip.
|
||||||
|
public event Action<string>? ErrorReported;
|
||||||
|
|
||||||
// True when there is something to inspect before merging (a live worktree diff,
|
// True when there is something to inspect before merging (a live worktree diff,
|
||||||
// a merged commit range, or a planning/children combined diff). When false there
|
// a merged commit range, or a planning/children combined diff). When false there
|
||||||
// is nothing to read, so the review gate must not block approve.
|
// is nothing to read, so the review gate must not block approve.
|
||||||
@@ -191,16 +195,9 @@ public sealed partial class MergeSectionViewModel : ViewModelBase
|
|||||||
[RelayCommand(CanExecute = nameof(CanOpenWorktree))]
|
[RelayCommand(CanExecute = nameof(CanOpenWorktree))]
|
||||||
private void OpenWorktree()
|
private void OpenWorktree()
|
||||||
{
|
{
|
||||||
if (_worktreePath is null) return;
|
var (ok, error) = ShellOpen.Path(_worktreePath);
|
||||||
try
|
if (!ok && error is not null)
|
||||||
{
|
ErrorReported?.Invoke(Loc.T("vm.mergeSection.openWorktreeFailed", error));
|
||||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = _worktreePath,
|
|
||||||
UseShellExecute = true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch { }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool CanOpenWorktree() => _worktreePath != null;
|
private bool CanOpenWorktree() => _worktreePath != null;
|
||||||
|
|||||||
@@ -1432,16 +1432,9 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void OpenTaskWorktree(TaskRowViewModel? row)
|
private void OpenTaskWorktree(TaskRowViewModel? row)
|
||||||
{
|
{
|
||||||
if (row?.WorktreePath is not { } path || string.IsNullOrWhiteSpace(path)) return;
|
var (ok, error) = ShellOpen.Path(row?.WorktreePath);
|
||||||
try
|
if (!ok && error is not null)
|
||||||
{
|
ErrorReported?.Invoke(Loc.T("vm.tasksIsland.openWorktreeFailed", error));
|
||||||
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]
|
||||||
|
|||||||
@@ -544,6 +544,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
private async Task OpenAbout()
|
private async Task OpenAbout()
|
||||||
{
|
{
|
||||||
var vm = new AboutModalViewModel();
|
var vm = new AboutModalViewModel();
|
||||||
|
vm.ErrorReported += FlashFooterError;
|
||||||
if (Dialogs is not null) await Dialogs.ShowAboutAsync(vm);
|
if (Dialogs is not null) await Dialogs.ShowAboutAsync(vm);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -599,6 +600,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var vm = _worktreesOverviewVmFactory();
|
var vm = _worktreesOverviewVmFactory();
|
||||||
|
vm.ErrorReported += FlashFooterError;
|
||||||
vm.Configure(null, null);
|
vm.Configure(null, null);
|
||||||
await vm.LoadAsync();
|
await vm.LoadAsync();
|
||||||
await Dialogs.ShowWorktreesOverviewAsync(vm);
|
await Dialogs.ShowWorktreesOverviewAsync(vm);
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
using System.Diagnostics;
|
|
||||||
using System.IO;
|
using System.IO;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using ClaudeDo.Data;
|
using ClaudeDo.Data;
|
||||||
|
using ClaudeDo.Ui.Localization;
|
||||||
|
using ClaudeDo.Ui.Services;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using CommunityToolkit.Mvvm.Input;
|
using CommunityToolkit.Mvvm.Input;
|
||||||
|
|
||||||
@@ -17,18 +18,17 @@ public sealed partial class AboutModalViewModel : ViewModelBase
|
|||||||
|
|
||||||
public Action? CloseAction { get; set; }
|
public Action? CloseAction { get; set; }
|
||||||
|
|
||||||
|
// Mirrors UsageMonitorModalViewModel.ErrorReported — this modal's own window sits in front
|
||||||
|
// of the footer strip, so the caller wires this into FlashFooterError.
|
||||||
|
public event Action<string>? ErrorReported;
|
||||||
|
|
||||||
[RelayCommand] private void Close() => CloseAction?.Invoke();
|
[RelayCommand] private void Close() => CloseAction?.Invoke();
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void OpenPath(string? path)
|
private void OpenPath(string? path)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(path)) return;
|
var (ok, error) = ShellOpen.Path(path);
|
||||||
try
|
if (!ok && error is not null)
|
||||||
{
|
ErrorReported?.Invoke(Loc.T("vm.about.openPathFailed", error));
|
||||||
var target = File.Exists(path) ? path : (Directory.Exists(path) ? path : null);
|
|
||||||
if (target is null) return;
|
|
||||||
Process.Start(new ProcessStartInfo("explorer.exe", $"\"{target}\"") { UseShellExecute = true });
|
|
||||||
}
|
|
||||||
catch { /* ignore */ }
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
using System.Collections.ObjectModel;
|
using System.Collections.ObjectModel;
|
||||||
using System.Diagnostics;
|
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
using Avalonia.Controls.ApplicationLifetimes;
|
using Avalonia.Controls.ApplicationLifetimes;
|
||||||
using Avalonia.Input.Platform;
|
using Avalonia.Input.Platform;
|
||||||
@@ -94,6 +93,10 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
|
|||||||
public ObservableCollection<string> MergeTargets { get; } = new();
|
public ObservableCollection<string> MergeTargets { get; } = new();
|
||||||
public ObservableCollection<WorktreeOverviewRowViewModel> ConflictRows { get; } = new();
|
public ObservableCollection<WorktreeOverviewRowViewModel> ConflictRows { get; } = new();
|
||||||
|
|
||||||
|
// Mirrors UsageMonitorModalViewModel.ErrorReported — surfaces modal-owned failures in the
|
||||||
|
// footer strip once the caller wires it (the modal's own window sits in front of the strip).
|
||||||
|
public event Action<string>? ErrorReported;
|
||||||
|
|
||||||
public Action? CloseAction { get; set; }
|
public Action? CloseAction { get; set; }
|
||||||
public Action<DiffViewerViewModel>? ShowDiffAction { get; set; }
|
public Action<DiffViewerViewModel>? ShowDiffAction { get; set; }
|
||||||
public Action<string, string>? JumpToTaskAction { get; set; }
|
public Action<string, string>? JumpToTaskAction { get; set; }
|
||||||
@@ -262,9 +265,9 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
|
|||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private void OpenInExplorer(WorktreeOverviewRowViewModel? row)
|
private void OpenInExplorer(WorktreeOverviewRowViewModel? row)
|
||||||
{
|
{
|
||||||
if (row is null || !row.PathExistsOnDisk) return;
|
var (ok, error) = ShellOpen.Path(row?.Path);
|
||||||
try { Process.Start(new ProcessStartInfo { FileName = row.Path, UseShellExecute = true }); }
|
if (!ok && error is not null)
|
||||||
catch { }
|
ErrorReported?.Invoke(Loc.T("vm.worktreesOverview.openInExplorerFailed", error));
|
||||||
}
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
|
|||||||
@@ -0,0 +1,158 @@
|
|||||||
|
using ClaudeDo.Data;
|
||||||
|
using ClaudeDo.Data.Models;
|
||||||
|
using ClaudeDo.Ui.Services;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Islands;
|
||||||
|
using ClaudeDo.Ui.ViewModels.Modals;
|
||||||
|
using Microsoft.EntityFrameworkCore;
|
||||||
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||||
|
|
||||||
|
namespace ClaudeDo.Ui.Tests.Services;
|
||||||
|
|
||||||
|
// Dedup: five copy-pasted "open this path in the shell" implementations replaced by ShellOpen.
|
||||||
|
// Process.Start must never fire in tests, so only the "nothing to open" paths are covered here —
|
||||||
|
// an existing file/directory would actually launch the OS shell handler.
|
||||||
|
public class ShellOpenTests
|
||||||
|
{
|
||||||
|
private sealed class NoopWorkerClient : StubWorkerClient { }
|
||||||
|
|
||||||
|
[Theory]
|
||||||
|
[InlineData(null)]
|
||||||
|
[InlineData("")]
|
||||||
|
[InlineData(" ")]
|
||||||
|
public void Path_BlankInput_ReturnsFalseWithNoError(string? input)
|
||||||
|
{
|
||||||
|
var (ok, error) = ShellOpen.Path(input);
|
||||||
|
|
||||||
|
Assert.False(ok);
|
||||||
|
Assert.Null(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void Path_NonExistentPath_ReturnsFalseWithNoError_DoesNotThrow()
|
||||||
|
{
|
||||||
|
var missing = System.IO.Path.Combine(Path.GetTempPath(), $"claudedo_shellopen_missing_{Guid.NewGuid():N}");
|
||||||
|
|
||||||
|
var (ok, error) = ShellOpen.Path(missing);
|
||||||
|
|
||||||
|
Assert.False(ok);
|
||||||
|
Assert.Null(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ListsIslandViewModel_OpenInExplorer_NonExistentDir_StaysSilent()
|
||||||
|
{
|
||||||
|
var dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_shellopen_lists_{Guid.NewGuid():N}.db");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var opts = new DbContextOptionsBuilder<ClaudeDoDbContext>().UseSqlite($"Data Source={dbPath}").Options;
|
||||||
|
using (var ctx = new ClaudeDoDbContext(opts)) ctx.Database.EnsureCreated();
|
||||||
|
var factory = new TestDbFactory(() => new ClaudeDoDbContext(opts));
|
||||||
|
|
||||||
|
var vm = new ListsIslandViewModel(factory);
|
||||||
|
string? reportedError = null;
|
||||||
|
vm.ErrorReported += msg => reportedError = msg;
|
||||||
|
var row = new ListNavItemViewModel
|
||||||
|
{
|
||||||
|
Id = "L1",
|
||||||
|
Kind = ListKind.User,
|
||||||
|
WorkingDir = Path.Combine(Path.GetTempPath(), $"claudedo_shellopen_missing_dir_{Guid.NewGuid():N}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.OpenInExplorerCommand.Execute(row);
|
||||||
|
|
||||||
|
Assert.Null(reportedError);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { File.Delete(dbPath); } catch { }
|
||||||
|
try { File.Delete(dbPath + "-wal"); } catch { }
|
||||||
|
try { File.Delete(dbPath + "-shm"); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void TasksIslandViewModel_OpenTaskWorktree_NonExistentPath_StaysSilent()
|
||||||
|
{
|
||||||
|
var dbPath = Path.Combine(Path.GetTempPath(), $"claudedo_shellopen_tasks_{Guid.NewGuid():N}.db");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var opts = new DbContextOptionsBuilder<ClaudeDoDbContext>().UseSqlite($"Data Source={dbPath}").Options;
|
||||||
|
using (var ctx = new ClaudeDoDbContext(opts)) ctx.Database.EnsureCreated();
|
||||||
|
var factory = new TestDbFactory(() => new ClaudeDoDbContext(opts));
|
||||||
|
|
||||||
|
var vm = new TasksIslandViewModel(factory, new NoopWorkerClient());
|
||||||
|
string? reportedError = null;
|
||||||
|
vm.ErrorReported += msg => reportedError = msg;
|
||||||
|
var row = new TaskRowViewModel
|
||||||
|
{
|
||||||
|
Id = "task-1",
|
||||||
|
Status = TaskStatus.Idle,
|
||||||
|
WorktreePath = Path.Combine(Path.GetTempPath(), $"claudedo_shellopen_missing_wt_{Guid.NewGuid():N}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.OpenTaskWorktreeCommand.Execute(row);
|
||||||
|
|
||||||
|
Assert.Null(reportedError);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
try { File.Delete(dbPath); } catch { }
|
||||||
|
try { File.Delete(dbPath + "-wal"); } catch { }
|
||||||
|
try { File.Delete(dbPath + "-shm"); } catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void WorktreesOverviewModalViewModel_OpenInExplorer_NonExistentPath_StaysSilent()
|
||||||
|
{
|
||||||
|
var vm = new WorktreesOverviewModalViewModel(new NoopWorkerClient(), () => null!, new MergeCoordinator());
|
||||||
|
string? reportedError = null;
|
||||||
|
vm.ErrorReported += msg => reportedError = msg;
|
||||||
|
var row = new WorktreeOverviewRowViewModel
|
||||||
|
{
|
||||||
|
TaskId = "task-1",
|
||||||
|
TaskTitle = "Task 1",
|
||||||
|
TaskStatus = TaskStatus.Idle,
|
||||||
|
State = WorktreeState.Active,
|
||||||
|
Path = Path.Combine(Path.GetTempPath(), $"claudedo_shellopen_missing_row_{Guid.NewGuid():N}"),
|
||||||
|
};
|
||||||
|
|
||||||
|
vm.OpenInExplorerCommand.Execute(row);
|
||||||
|
|
||||||
|
Assert.Null(reportedError);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void AboutModalViewModel_OpenPath_NonExistentPath_StaysSilent()
|
||||||
|
{
|
||||||
|
var vm = new AboutModalViewModel();
|
||||||
|
string? reportedError = null;
|
||||||
|
vm.ErrorReported += msg => reportedError = msg;
|
||||||
|
var missing = Path.Combine(Path.GetTempPath(), $"claudedo_shellopen_missing_about_{Guid.NewGuid():N}");
|
||||||
|
|
||||||
|
vm.OpenPathCommand.Execute(missing);
|
||||||
|
|
||||||
|
Assert.Null(reportedError);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void MergeSectionViewModel_OpenWorktree_NonExistentPath_StaysSilent()
|
||||||
|
{
|
||||||
|
var vm = new MergeSectionViewModel(new NoopWorkerClient(), services: null!);
|
||||||
|
string? reportedError = null;
|
||||||
|
vm.ErrorReported += msg => reportedError = msg;
|
||||||
|
var missing = Path.Combine(Path.GetTempPath(), $"claudedo_shellopen_missing_merge_{Guid.NewGuid():N}");
|
||||||
|
vm.SyncWorktree(missing, null, null, "Active", null);
|
||||||
|
|
||||||
|
vm.OpenWorktreeCommand.Execute(null);
|
||||||
|
|
||||||
|
Assert.Null(reportedError);
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed class TestDbFactory : IDbContextFactory<ClaudeDoDbContext>
|
||||||
|
{
|
||||||
|
private readonly Func<ClaudeDoDbContext> _create;
|
||||||
|
public TestDbFactory(Func<ClaudeDoDbContext> create) => _create = create;
|
||||||
|
public ClaudeDoDbContext CreateDbContext() => _create();
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user