fix: resolve critical bugs and improve reliability across worker, data, UI
- Fix worker using wrong DB by defaulting to CurrentUser service account and expanding ~ to absolute paths at install time - Fix DbContext disposed before fire-and-forget by passing taskId instead of TaskEntity into RunInSlotAsync, which creates its own context - Fix ActiveTaskDto property casing mismatch between hub and client - Move WAL mode PRAGMA before migrations to prevent concurrent lock issues - Replace FirstAsync with FirstOrDefaultAsync + null guards in tag operations - Add delete confirmation flow for lists - Log fire-and-forget exceptions instead of swallowing them - Broadcast RunCreated event from WorkerHub.RunNow - Add IDisposable to MainWindowViewModel for event handler cleanup - Preserve subtask CreatedAt on updates instead of overwriting - Replace bare catch blocks with Debug.WriteLine logging Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -13,7 +13,7 @@ using Microsoft.EntityFrameworkCore;
|
||||
|
||||
namespace ClaudeDo.Ui.ViewModels;
|
||||
|
||||
public partial class MainWindowViewModel : ViewModelBase
|
||||
public partial class MainWindowViewModel : ViewModelBase, IDisposable
|
||||
{
|
||||
private readonly IDbContextFactory<ClaudeDoDbContext> _dbFactory;
|
||||
private readonly WorkerClient _worker;
|
||||
@@ -27,6 +27,8 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
public TaskDetailViewModel TaskDetail { get; }
|
||||
public StatusBarViewModel StatusBar { get; }
|
||||
|
||||
private readonly Action<string> _onTaskChanged;
|
||||
|
||||
public MainWindowViewModel(
|
||||
IDbContextFactory<ClaudeDoDbContext> dbFactory,
|
||||
WorkerClient worker,
|
||||
@@ -42,8 +44,15 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
TaskDetail = taskDetail;
|
||||
StatusBar = statusBar;
|
||||
|
||||
_onTaskChanged = taskId => _ = TaskList.RefreshSingleAsync(taskId);
|
||||
TaskList.SelectedTaskChanged += OnSelectedTaskChanged;
|
||||
TaskDetail.TaskChanged += taskId => _ = TaskList.RefreshSingleAsync(taskId);
|
||||
TaskDetail.TaskChanged += _onTaskChanged;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
TaskList.SelectedTaskChanged -= OnSelectedTaskChanged;
|
||||
TaskDetail.TaskChanged -= _onTaskChanged;
|
||||
}
|
||||
|
||||
public async Task InitializeAsync()
|
||||
@@ -61,7 +70,11 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
StatusBar.ShowMessage($"Error loading lists: {ex.Message}");
|
||||
}
|
||||
|
||||
_ = _worker.StartAsync();
|
||||
_ = _worker.StartAsync().ContinueWith(t =>
|
||||
{
|
||||
if (t.IsFaulted)
|
||||
System.Diagnostics.Debug.WriteLine($"Worker connection failed: {t.Exception?.Message}");
|
||||
}, TaskScheduler.Default);
|
||||
}
|
||||
|
||||
partial void OnSelectedListChanged(ListItemViewModel? value)
|
||||
@@ -154,23 +167,46 @@ public partial class MainWindowViewModel : ViewModelBase
|
||||
}
|
||||
}
|
||||
|
||||
[ObservableProperty] private bool _isDeleteConfirmVisible;
|
||||
private ListItemViewModel? _pendingDeleteList;
|
||||
|
||||
[RelayCommand]
|
||||
private async Task DeleteList()
|
||||
private void DeleteList()
|
||||
{
|
||||
if (SelectedList is null) return;
|
||||
// TODO: confirmation dialog
|
||||
_pendingDeleteList = SelectedList;
|
||||
IsDeleteConfirmVisible = true;
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private async Task ConfirmDeleteList()
|
||||
{
|
||||
IsDeleteConfirmVisible = false;
|
||||
if (_pendingDeleteList is null) return;
|
||||
try
|
||||
{
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var listRepo = new ListRepository(context);
|
||||
await listRepo.DeleteAsync(SelectedList.Id);
|
||||
Lists.Remove(SelectedList);
|
||||
SelectedList = null;
|
||||
await listRepo.DeleteAsync(_pendingDeleteList.Id);
|
||||
Lists.Remove(_pendingDeleteList);
|
||||
if (SelectedList == _pendingDeleteList)
|
||||
SelectedList = null;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
StatusBar.ShowMessage($"Error deleting list: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_pendingDeleteList = null;
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void CancelDeleteList()
|
||||
{
|
||||
IsDeleteConfirmVisible = false;
|
||||
_pendingDeleteList = null;
|
||||
}
|
||||
|
||||
private static async Task ShowDialogAsync(Window dialog)
|
||||
|
||||
@@ -282,16 +282,18 @@ public partial class TaskDetailViewModel : ViewModelBase
|
||||
if (e.PropertyName is not (nameof(SubtaskItemViewModel.Title) or nameof(SubtaskItemViewModel.Completed))) return;
|
||||
try
|
||||
{
|
||||
if (_taskId is null) return;
|
||||
using var context = _dbFactory.CreateDbContext();
|
||||
var orig = await context.Subtasks.AsNoTracking().FirstOrDefaultAsync(s => s.Id == vm.Id);
|
||||
var subtaskRepo = new SubtaskRepository(context);
|
||||
await subtaskRepo.UpdateAsync(new SubtaskEntity
|
||||
{
|
||||
Id = vm.Id,
|
||||
TaskId = _taskId ?? "",
|
||||
TaskId = _taskId,
|
||||
Title = vm.Title,
|
||||
Completed = vm.Completed,
|
||||
OrderNum = Subtasks.IndexOf(vm),
|
||||
CreatedAt = DateTime.UtcNow,
|
||||
CreatedAt = orig?.CreatedAt ?? DateTime.UtcNow,
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -378,13 +380,15 @@ public partial class TaskDetailViewModel : ViewModelBase
|
||||
UseShellExecute = true,
|
||||
});
|
||||
}
|
||||
catch { /* best effort */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Failed to open worktree: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
private void ShowDiff()
|
||||
{
|
||||
// TODO: open a proper diff viewer; for now open git diff in a console
|
||||
if (WorktreePath is null) return;
|
||||
try
|
||||
{
|
||||
@@ -395,7 +399,10 @@ public partial class TaskDetailViewModel : ViewModelBase
|
||||
UseShellExecute = true,
|
||||
});
|
||||
}
|
||||
catch { /* best effort */ }
|
||||
catch (Exception ex)
|
||||
{
|
||||
Debug.WriteLine($"Failed to show diff: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
[RelayCommand]
|
||||
|
||||
@@ -215,7 +215,10 @@ public partial class TaskEditorViewModel : ViewModelBase
|
||||
{
|
||||
if (vm.Id == "") continue;
|
||||
if (vm.Title != vm.OriginalTitle || vm.Completed != vm.OriginalCompleted)
|
||||
await subtaskRepo.UpdateAsync(new SubtaskEntity { Id = vm.Id, TaskId = taskId, Title = vm.Title, Completed = vm.Completed, OrderNum = idx, CreatedAt = DateTime.UtcNow });
|
||||
{
|
||||
var origSub = existing.FirstOrDefault(e => e.Id == vm.Id);
|
||||
await subtaskRepo.UpdateAsync(new SubtaskEntity { Id = vm.Id, TaskId = taskId, Title = vm.Title, Completed = vm.Completed, OrderNum = idx, CreatedAt = origSub?.CreatedAt ?? DateTime.UtcNow });
|
||||
}
|
||||
else
|
||||
{
|
||||
// update order_num if position changed
|
||||
|
||||
Reference in New Issue
Block a user