fix(worker-client): stop swallowing mutating hub call failures
TryInvokeAsync catches every exception and returns null, which is fine
for read-only calls but hid real HubException reasons behind a generic
"offline" message for the 7 mutating call sites (RestoreDefaultAgents,
UpsertPrimeSchedule, AddDailyNote, CleanupFinishedWorktrees,
ResetAllWorktrees, ForceRemoveWorktree, BuildPlanningIntegrationBranch)
— the same bug class fixed for ApproveReview in e1807fd. Each of the 22
TryInvokeAsync call sites was audited; the 15 read-only ones are left
unchanged (empty/offline is the right display). For the 7 switched to
a direct hub invoke, every caller was checked and, where it had no
catch, one was added so the exception surfaces (StatusMessage,
ShowErrorAsync/CombinedWarning) instead of crashing.
This commit is contained in:
@@ -543,7 +543,7 @@
|
||||
"taskRow": { "createdPrefix": "Erstellt {0}", "stepsText": "{0}/{1} Schritte" },
|
||||
"tasksIsland": { "completedHeader": "ABGESCHLOSSEN", "completedHeaderCount": "ABGESCHLOSSEN · {0}", "planningOpenFailed": "Planungssitzung konnte nicht geöffnet werden: {0}", "planningResumeFailed": "Planungssitzung konnte nicht fortgesetzt werden: {0}", "pickUpInTerminalFailed": "Im Terminal fortsetzen fehlgeschlagen: {0}", "approveFailed": "Genehmigen & Mergen fehlgeschlagen: {0}" },
|
||||
"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)." },
|
||||
"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": "Merge-Aufgabe: {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}", "unknownStatus": "Unbekannter Status: {0}", "mergeFailed": "Merge fehlgeschlagen: {0}" },
|
||||
"conflictResolution": { "vsCodeError": "VS Code konnte nicht gestartet werden: {0}. Die Pfade sind oben aufgeführt — kopiere sie manuell.", "subtaskPrefix": "Konflikte in Teilaufgabe: {0}", "targetPrefix": "Zusammenführen in: {0}" },
|
||||
"settingsModal": { "workerOffline": "Worker offline — Einstellungen schreibgeschützt.", "saveFailed": "Speichern fehlgeschlagen: {0}" },
|
||||
@@ -551,8 +551,8 @@
|
||||
"weeklyReport": { "invalidRange": "Ungültiger Datumsbereich.", "generating": "Bericht wird erstellt…", "error": "Fehler: {0}" },
|
||||
"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}", "openFailed": "Öffnen 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." },
|
||||
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "Liste", "cleanupFailed": "Aufräumen fehlgeschlagen.", "removed": "{0} Worktree(s) entfernt.", "discardFailed": "Worktree konnte nicht verworfen werden.", "keepFailed": "Worktree konnte nicht behalten werden.", "cannotForceRunning": "Eine laufende Aufgabe kann nicht zwangsweise entfernt werden.", "forceRemoveFailed": "Zwangsentfernung fehlgeschlagen.", "batchProgress": "Merge {0}/{1}…", "batchDone": "{0} gemergt, {1} zu lösen." },
|
||||
"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.", "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." },
|
||||
"listSettings": { "untitled": "Unbenannt" },
|
||||
"lists": { "localSuffix": "{0} / lokal", "smartMyDay": "Mein Tag", "smartImportant": "Wichtig", "smartPlanned": "Geplant", "virtualQueue": "Warteschlange", "virtualRunning": "Läuft", "virtualReview": "Prüfung", "newList": "Neue Liste" }
|
||||
}
|
||||
|
||||
@@ -543,7 +543,7 @@
|
||||
"taskRow": { "createdPrefix": "Created {0}", "stepsText": "{0}/{1} steps" },
|
||||
"tasksIsland": { "completedHeader": "COMPLETED", "completedHeaderCount": "COMPLETED · {0}", "planningOpenFailed": "Couldn't open planning session: {0}", "planningResumeFailed": "Couldn't resume planning session: {0}", "pickUpInTerminalFailed": "Pick up in terminal failed: {0}", "approveFailed": "Approve & merge failed: {0}" },
|
||||
"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)." },
|
||||
"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": "Merge task: {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}", "unknownStatus": "Unknown status: {0}", "mergeFailed": "Merge failed: {0}" },
|
||||
"conflictResolution": { "vsCodeError": "Could not launch VS Code: {0}. Paths are listed above — copy them manually.", "subtaskPrefix": "Conflicts in subtask: {0}", "targetPrefix": "Merging into: {0}" },
|
||||
"settingsModal": { "workerOffline": "Worker offline — settings read-only.", "saveFailed": "Save failed: {0}" },
|
||||
@@ -551,8 +551,8 @@
|
||||
"weeklyReport": { "invalidRange": "Invalid date range.", "generating": "Generating report…", "error": "Error: {0}" },
|
||||
"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}", "openFailed": "Open 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)." },
|
||||
"worktreesOverview": { "titleAll": "Worktrees", "titleList": "Worktrees — {0}", "listFallback": "list", "cleanupFailed": "Cleanup failed.", "removed": "Removed {0} worktree(s).", "discardFailed": "Failed to discard worktree.", "keepFailed": "Failed to keep worktree.", "cannotForceRunning": "Cannot force-remove a running task.", "forceRemoveFailed": "Force remove failed.", "batchProgress": "Merging {0}/{1}…", "batchDone": "Merged {0}, {1} need resolution." },
|
||||
"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).", "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." },
|
||||
"listSettings": { "untitled": "Untitled" },
|
||||
"lists": { "localSuffix": "{0} / local", "smartMyDay": "My Day", "smartImportant": "Important", "smartPlanned": "Planned", "virtualQueue": "Queue", "virtualRunning": "Running", "virtualReview": "Review", "newList": "New list" }
|
||||
}
|
||||
|
||||
@@ -329,8 +329,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
await _hub.InvokeAsync("RefreshAgents");
|
||||
}
|
||||
|
||||
public Task<SeedResultDto?> RestoreDefaultAgentsAsync()
|
||||
=> TryInvokeAsync<SeedResultDto>("RestoreDefaultAgents");
|
||||
public async Task<SeedResultDto?> RestoreDefaultAgentsAsync()
|
||||
=> await _hub.InvokeAsync<SeedResultDto>("RestoreDefaultAgents");
|
||||
|
||||
private async Task SeedActiveTasksAsync()
|
||||
{
|
||||
@@ -372,8 +372,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
public async Task<List<PrimeScheduleDto>> GetPrimeSchedulesAsync()
|
||||
=> await TryInvokeAsync<List<PrimeScheduleDto>>("ListPrimeSchedules") ?? new List<PrimeScheduleDto>();
|
||||
|
||||
public Task<PrimeScheduleDto?> UpsertPrimeScheduleAsync(PrimeScheduleDto dto)
|
||||
=> TryInvokeAsync<PrimeScheduleDto>("UpsertPrimeSchedule", dto);
|
||||
public async Task<PrimeScheduleDto?> UpsertPrimeScheduleAsync(PrimeScheduleDto dto)
|
||||
=> await _hub.InvokeAsync<PrimeScheduleDto>("UpsertPrimeSchedule", dto);
|
||||
|
||||
public async Task DeletePrimeScheduleAsync(Guid id)
|
||||
{
|
||||
@@ -400,8 +400,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
public async Task<List<DailyNoteDto>> GetDailyNotesAsync(DateOnly day)
|
||||
=> await TryInvokeAsync<List<DailyNoteDto>>("GetDailyNotes", IsoDay(day)) ?? new List<DailyNoteDto>();
|
||||
|
||||
public Task<DailyNoteDto?> AddDailyNoteAsync(DateOnly day, string text)
|
||||
=> TryInvokeAsync<DailyNoteDto>("AddDailyNote", IsoDay(day), text);
|
||||
public async Task<DailyNoteDto?> AddDailyNoteAsync(DateOnly day, string text)
|
||||
=> await _hub.InvokeAsync<DailyNoteDto>("AddDailyNote", IsoDay(day), text);
|
||||
|
||||
public async Task UpdateDailyNoteAsync(string id, string text)
|
||||
=> await _hub.InvokeAsync("UpdateDailyNote", id, text);
|
||||
@@ -474,11 +474,11 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
await _hub.InvokeAsync("CancelReview", taskId);
|
||||
}
|
||||
|
||||
public Task<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync(string? listId = null)
|
||||
=> TryInvokeAsync<WorktreeCleanupDto>("CleanupFinishedWorktrees", listId);
|
||||
public async Task<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync(string? listId = null)
|
||||
=> await _hub.InvokeAsync<WorktreeCleanupDto>("CleanupFinishedWorktrees", listId);
|
||||
|
||||
public Task<WorktreeResetDto?> ResetAllWorktreesAsync()
|
||||
=> TryInvokeAsync<WorktreeResetDto>("ResetAllWorktrees");
|
||||
public async Task<WorktreeResetDto?> ResetAllWorktreesAsync()
|
||||
=> await _hub.InvokeAsync<WorktreeResetDto>("ResetAllWorktrees");
|
||||
|
||||
public async Task<List<WorktreeOverviewDto>> GetWorktreesOverviewAsync(string? listId)
|
||||
=> await TryInvokeAsync<List<WorktreeOverviewDto>>("GetWorktreesOverview", listId)
|
||||
@@ -501,8 +501,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
}
|
||||
}
|
||||
|
||||
public Task<ForceRemoveResultDto?> ForceRemoveWorktreeAsync(string taskId)
|
||||
=> TryInvokeAsync<ForceRemoveResultDto>("ForceRemoveWorktree", taskId);
|
||||
public async Task<ForceRemoveResultDto?> ForceRemoveWorktreeAsync(string taskId)
|
||||
=> await _hub.InvokeAsync<ForceRemoveResultDto>("ForceRemoveWorktree", taskId);
|
||||
|
||||
public async Task<PlanningSessionStartInfo> StartPlanningSessionAsync(string taskId, CancellationToken ct = default)
|
||||
=> await _hub.InvokeAsync<PlanningSessionStartInfo>("StartPlanningSessionAsync", taskId, ct);
|
||||
@@ -543,8 +543,8 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
||||
public async Task<IReadOnlyList<SubtaskDiffDto>> GetPlanningAggregateAsync(string planningTaskId)
|
||||
=> await TryInvokeAsync<List<SubtaskDiffDto>>("GetPlanningAggregate", planningTaskId) ?? [];
|
||||
|
||||
public Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch)
|
||||
=> TryInvokeAsync<CombinedDiffResultDto>("BuildPlanningIntegrationBranch", planningTaskId, targetBranch);
|
||||
public async Task<CombinedDiffResultDto?> BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch)
|
||||
=> await _hub.InvokeAsync<CombinedDiffResultDto>("BuildPlanningIntegrationBranch", planningTaskId, targetBranch);
|
||||
|
||||
public async Task ContinuePlanningMergeAsync(string planningTaskId)
|
||||
{
|
||||
|
||||
@@ -311,6 +311,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase, IDisposable
|
||||
Prep = new PrepPanelViewModel(worker);
|
||||
|
||||
Notes = new NotesEditorViewModel(_notesApi);
|
||||
Notes.ErrorReported += msg => { if (ShowErrorAsync is not null) _ = ShowErrorAsync(msg); };
|
||||
Subtasks.CollectionChanged += (_, _) => NotifyStepsChanged();
|
||||
Subtasks.CollectionChanged += (_, _) => Merge.SyncChildOutcomes(HasChildOutcomes, Subtasks.Count);
|
||||
Attachments.CollectionChanged += (_, _) => OnPropertyChanged(nameof(FilesBadge));
|
||||
|
||||
@@ -24,6 +24,9 @@ public sealed partial class NotesEditorViewModel : ViewModelBase
|
||||
|
||||
public NotesEditorViewModel(INotesApi api) => _api = api;
|
||||
|
||||
// Raised when a worker call fails so the host VM can surface it (e.g. via ShowErrorAsync).
|
||||
public event Action<string>? ErrorReported;
|
||||
|
||||
public ObservableCollection<NoteBulletViewModel> Bullets { get; } = new();
|
||||
|
||||
[ObservableProperty] private DateOnly _currentDay = DateOnly.FromDateTime(DateTime.Today);
|
||||
@@ -55,9 +58,16 @@ public sealed partial class NotesEditorViewModel : ViewModelBase
|
||||
{
|
||||
var text = NewBulletText.Trim();
|
||||
if (text.Length == 0) return;
|
||||
var dto = await _api.AddAsync(CurrentDay, text);
|
||||
if (dto is not null) Bullets.Add(MakeBullet(dto.Id, dto.Text));
|
||||
NewBulletText = "";
|
||||
try
|
||||
{
|
||||
var dto = await _api.AddAsync(CurrentDay, text);
|
||||
if (dto is not null) Bullets.Add(MakeBullet(dto.Id, dto.Text));
|
||||
NewBulletText = "";
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ErrorReported?.Invoke(ex.Message);
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand] private Task PrevDay() => LoadDayAsync(CurrentDay.AddDays(-1));
|
||||
|
||||
@@ -197,6 +197,11 @@ public sealed partial class DiffViewerViewModel : ViewModelBase
|
||||
DisplayedDiff = "";
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
DisplayedDiff = "";
|
||||
CombinedWarning = Loc.T("vm.planningDiff.buildFailed", ex.Message);
|
||||
}
|
||||
finally
|
||||
{
|
||||
IsLoadingCombined = false;
|
||||
|
||||
@@ -46,6 +46,7 @@ public sealed partial class WorktreesSettingsTabViewModel : ViewModelBase
|
||||
var r = await _worker.CleanupFinishedWorktreesAsync();
|
||||
StatusMessage = r is null ? Loc.T("vm.worktreesTab.workerOffline") : Loc.T("vm.worktreesTab.removed", r.Removed);
|
||||
}
|
||||
catch (Exception ex) { StatusMessage = Loc.T("vm.worktreesTab.cleanupFailed", ex.Message); }
|
||||
finally { IsBusy = false; }
|
||||
}
|
||||
|
||||
@@ -63,6 +64,7 @@ public sealed partial class WorktreesSettingsTabViewModel : ViewModelBase
|
||||
else if (r.Blocked) StatusMessage = Loc.T("vm.worktreesTab.blocked", r.RunningTasks);
|
||||
else StatusMessage = Loc.T("vm.worktreesTab.removedFrom", r.Removed, r.TasksAffected);
|
||||
}
|
||||
catch (Exception ex) { StatusMessage = Loc.T("vm.worktreesTab.resetFailed", ex.Message); }
|
||||
finally { IsBusy = false; }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -174,6 +174,7 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
|
||||
StatusMessage = result is null ? Loc.T("vm.worktreesOverview.cleanupFailed") : Loc.T("vm.worktreesOverview.removed", result.Removed);
|
||||
await LoadAsync();
|
||||
}
|
||||
catch (Exception ex) { StatusMessage = Loc.T("vm.worktreesOverview.cleanupFailedDetailed", ex.Message); }
|
||||
finally { IsBusy = false; }
|
||||
}
|
||||
|
||||
@@ -241,7 +242,16 @@ public sealed partial class WorktreesOverviewModalViewModel : ViewModelBase
|
||||
if (row.IsRunning) { StatusMessage = Loc.T("vm.worktreesOverview.cannotForceRunning"); return; }
|
||||
if (ConfirmAction is not null && !await ConfirmAction($"Force remove worktree for '{row.TaskTitle}'? This deletes the directory and branch.")) return;
|
||||
|
||||
var result = await _worker.ForceRemoveWorktreeAsync(row.TaskId);
|
||||
ForceRemoveResultDto? result;
|
||||
try
|
||||
{
|
||||
result = await _worker.ForceRemoveWorktreeAsync(row.TaskId);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusMessage = Loc.T("vm.worktreesOverview.forceRemoveFailedDetailed", ex.Message);
|
||||
return;
|
||||
}
|
||||
if (result is null || !result.Removed)
|
||||
{
|
||||
StatusMessage = result?.Reason ?? Loc.T("vm.worktreesOverview.forceRemoveFailed");
|
||||
|
||||
Reference in New Issue
Block a user