refactor(ui): dedupe path-open helper across five view models
ListsIslandViewModel.OpenInExplorer, MergeSectionViewModel.OpenWorktree,
WorktreesOverviewModalViewModel.OpenInExplorer, AboutModalViewModel.OpenPath and
TasksIslandViewModel.OpenTaskWorktree each reimplemented "open this path in the
shell" with their own existence check, launch mechanism and error handling.
Replace all five with the new ShellOpen.Path helper and route failures through
the existing ErrorReported -> footer-strip convention instead of bare catch {}.
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." },
|
||||
"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}" },
|
||||
"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" },
|
||||
"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}" }
|
||||
},
|
||||
"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." },
|
||||
"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}" },
|
||||
"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" },
|
||||
"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}" }
|
||||
},
|
||||
"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);
|
||||
Merge = new MergeSectionViewModel(worker, services);
|
||||
Merge.DiffViewed = () => ReviewDiffViewed = true;
|
||||
Merge.ErrorReported += msg => ErrorReported?.Invoke(msg);
|
||||
Merge.PropertyChanged += (_, e) =>
|
||||
{
|
||||
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 vm = _services.GetRequiredService<WorktreesOverviewModalViewModel>();
|
||||
vm.ErrorReported += msg => ErrorReported?.Invoke(msg);
|
||||
vm.Configure(rawId, row.Name);
|
||||
await vm.LoadAsync();
|
||||
await Dialogs.ShowWorktreesOverviewAsync(vm);
|
||||
@@ -107,17 +108,9 @@ public sealed partial class ListsIslandViewModel : ViewModelBase, IDisposable
|
||||
[RelayCommand]
|
||||
private void OpenInExplorer(ListNavItemViewModel? row)
|
||||
{
|
||||
var dir = row?.WorkingDir;
|
||||
if (string.IsNullOrWhiteSpace(dir) || !System.IO.Directory.Exists(dir)) return;
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = dir,
|
||||
UseShellExecute = true,
|
||||
});
|
||||
}
|
||||
catch { /* best-effort */ }
|
||||
var (ok, error) = ShellOpen.Path(row?.WorkingDir);
|
||||
if (!ok && error is not null)
|
||||
ErrorReported?.Invoke(Loc.T("vm.lists.openInExplorerFailed", error));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
||||
@@ -60,6 +60,10 @@ public sealed partial class MergeSectionViewModel : ViewModelBase
|
||||
// review gate can record that the changes were inspected before merging.
|
||||
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,
|
||||
// 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.
|
||||
@@ -191,16 +195,9 @@ public sealed partial class MergeSectionViewModel : ViewModelBase
|
||||
[RelayCommand(CanExecute = nameof(CanOpenWorktree))]
|
||||
private void OpenWorktree()
|
||||
{
|
||||
if (_worktreePath is null) return;
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo
|
||||
{
|
||||
FileName = _worktreePath,
|
||||
UseShellExecute = true,
|
||||
});
|
||||
}
|
||||
catch { }
|
||||
var (ok, error) = ShellOpen.Path(_worktreePath);
|
||||
if (!ok && error is not null)
|
||||
ErrorReported?.Invoke(Loc.T("vm.mergeSection.openWorktreeFailed", error));
|
||||
}
|
||||
|
||||
private bool CanOpenWorktree() => _worktreePath != null;
|
||||
|
||||
@@ -1432,16 +1432,9 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
||||
[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)); }
|
||||
var (ok, error) = ShellOpen.Path(row?.WorktreePath);
|
||||
if (!ok && error is not null)
|
||||
ErrorReported?.Invoke(Loc.T("vm.tasksIsland.openWorktreeFailed", error));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
||||
@@ -544,6 +544,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
||||
private async Task OpenAbout()
|
||||
{
|
||||
var vm = new AboutModalViewModel();
|
||||
vm.ErrorReported += FlashFooterError;
|
||||
if (Dialogs is not null) await Dialogs.ShowAboutAsync(vm);
|
||||
}
|
||||
|
||||
@@ -599,6 +600,7 @@ public sealed partial class IslandsShellViewModel : ViewModelBase, IDisposable
|
||||
try
|
||||
{
|
||||
var vm = _worktreesOverviewVmFactory();
|
||||
vm.ErrorReported += FlashFooterError;
|
||||
vm.Configure(null, null);
|
||||
await vm.LoadAsync();
|
||||
await Dialogs.ShowWorktreesOverviewAsync(vm);
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using ClaudeDo.Data;
|
||||
using ClaudeDo.Ui.Localization;
|
||||
using ClaudeDo.Ui.Services;
|
||||
using CommunityToolkit.Mvvm.ComponentModel;
|
||||
using CommunityToolkit.Mvvm.Input;
|
||||
|
||||
@@ -17,18 +18,17 @@ public sealed partial class AboutModalViewModel : ViewModelBase
|
||||
|
||||
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 OpenPath(string? path)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(path)) return;
|
||||
try
|
||||
{
|
||||
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 */ }
|
||||
var (ok, error) = ShellOpen.Path(path);
|
||||
if (!ok && error is not null)
|
||||
ErrorReported?.Invoke(Loc.T("vm.about.openPathFailed", error));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Diagnostics;
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Input.Platform;
|
||||
@@ -94,6 +93,10 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
|
||||
public ObservableCollection<string> MergeTargets { 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<DiffViewerViewModel>? ShowDiffAction { get; set; }
|
||||
public Action<string, string>? JumpToTaskAction { get; set; }
|
||||
@@ -262,9 +265,9 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
|
||||
[RelayCommand]
|
||||
private void OpenInExplorer(WorktreeOverviewRowViewModel? row)
|
||||
{
|
||||
if (row is null || !row.PathExistsOnDisk) return;
|
||||
try { Process.Start(new ProcessStartInfo { FileName = row.Path, UseShellExecute = true }); }
|
||||
catch { }
|
||||
var (ok, error) = ShellOpen.Path(row?.Path);
|
||||
if (!ok && error is not null)
|
||||
ErrorReported?.Invoke(Loc.T("vm.worktreesOverview.openInExplorerFailed", error));
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
||||
Reference in New Issue
Block a user