refactor(tags): remove tag entity and all references

Drops TagEntity, TagRepository, and tag wiring across data layer, worker,
and UI. Adds RemoveTags migration to clean up schema.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-05-19 08:07:24 +02:00
parent 8d34db3f9b
commit 623ebf147b
42 changed files with 333 additions and 1118 deletions

View File

@@ -21,7 +21,9 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
// Current task row (set by IslandsShellViewModel via Bind)
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(RunNowCommand))]
[NotifyCanExecuteChangedFor(nameof(EnqueueCommand))]
[NotifyCanExecuteChangedFor(nameof(DequeueCommand))]
[NotifyCanExecuteChangedFor(nameof(ResetAndRetryCommand))]
private TaskRowViewModel? _task;
// Editable fields
@@ -56,74 +58,23 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
// Short task-id badge, e.g. "#T1A"
public string TaskIdBadge => Task != null ? $"#T{Task.Id[..Math.Min(3, Task.Id.Length)].ToUpperInvariant()}" : "";
// Agent strip fields
// Status editor (Details panel) — set freely; broadcast refreshes other panes.
public System.Collections.ObjectModel.ObservableCollection<ClaudeDo.Data.Models.TaskStatus> StatusOptions { get; } = new()
{
ClaudeDo.Data.Models.TaskStatus.Idle,
ClaudeDo.Data.Models.TaskStatus.Queued,
ClaudeDo.Data.Models.TaskStatus.Running,
ClaudeDo.Data.Models.TaskStatus.Done,
ClaudeDo.Data.Models.TaskStatus.Failed,
ClaudeDo.Data.Models.TaskStatus.Cancelled,
};
private bool _suppressStatusSave;
[ObservableProperty] private ClaudeDo.Data.Models.TaskStatus _selectedStatus;
partial void OnSelectedStatusChanged(ClaudeDo.Data.Models.TaskStatus value)
{
if (_suppressStatusSave || Task is null) return;
_ = SaveStatusAsync(value);
}
private async System.Threading.Tasks.Task SaveStatusAsync(ClaudeDo.Data.Models.TaskStatus value)
{
if (Task is null) return;
try { await _worker.SetTaskStatusAsync(Task.Id, value); }
catch { /* offline */ }
}
// Tag editor
public ObservableCollection<string> Tags { get; } = new();
public ObservableCollection<string> AvailableTags { get; } = new();
[ObservableProperty] private string _newTagInput = "";
[RelayCommand]
private async System.Threading.Tasks.Task AddTagAsync()
{
if (Task is null) return;
var name = NewTagInput?.Trim().ToLowerInvariant();
NewTagInput = "";
if (string.IsNullOrEmpty(name)) return;
if (Tags.Contains(name)) return;
var next = Tags.ToList();
next.Add(name);
try { await _worker.SetTaskTagsAsync(Task.Id, next); }
catch { /* offline */ }
}
[RelayCommand]
private async System.Threading.Tasks.Task RemoveTagAsync(string? tagName)
{
if (Task is null || string.IsNullOrWhiteSpace(tagName)) return;
if (!Tags.Contains(tagName)) return;
var next = Tags.Where(t => t != tagName).ToList();
try { await _worker.SetTaskTagsAsync(Task.Id, next); }
catch { /* offline */ }
}
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(RunNowCommand))]
private string _agentStatusLabel = "Idle";
public bool IsRunning => AgentStatusLabel == "Running";
public bool IsDone => AgentStatusLabel == "Done";
public bool IsFailed => AgentStatusLabel == "Failed";
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(EnqueueCommand))]
[NotifyCanExecuteChangedFor(nameof(DequeueCommand))]
[NotifyCanExecuteChangedFor(nameof(ResetAndRetryCommand))]
[NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
[NotifyCanExecuteChangedFor(nameof(ResetCommand))]
private bool _showFailedActions;
private string _agentStatusLabel = "Idle";
public bool IsIdle => AgentStatusLabel == "Idle";
public bool IsQueued => AgentStatusLabel == "Queued";
public bool IsRunning => AgentStatusLabel == "Running";
public bool IsDone => AgentStatusLabel == "Done";
public bool IsFailed => AgentStatusLabel == "Failed";
public bool IsCancelled => AgentStatusLabel == "Cancelled";
// Recovery actions: Continue (resume session) for Failed/Cancelled.
public bool ShowContinue => IsFailed || IsCancelled;
// Reset & retry available from any terminal state.
public bool ShowResetAndRetry => IsFailed || IsCancelled || IsDone;
[ObservableProperty]
[NotifyCanExecuteChangedFor(nameof(ContinueCommand))]
@@ -131,11 +82,15 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
partial void OnAgentStatusLabelChanged(string value)
{
OnPropertyChanged(nameof(IsIdle));
OnPropertyChanged(nameof(IsQueued));
OnPropertyChanged(nameof(IsRunning));
OnPropertyChanged(nameof(IsDone));
OnPropertyChanged(nameof(IsFailed));
OnPropertyChanged(nameof(IsCancelled));
OnPropertyChanged(nameof(ShowContinue));
OnPropertyChanged(nameof(ShowResetAndRetry));
OnPropertyChanged(nameof(IsAgentSectionEnabled));
ShowFailedActions = value == "Failed";
}
[ObservableProperty] private string? _model;
@@ -237,40 +192,17 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
// Set by the view so DeleteTaskCommand can show an error message
public Func<string, System.Threading.Tasks.Task>? ShowErrorAsync { get; set; }
private void ApplyTagsFromEntity(ClaudeDo.Data.Models.TaskEntity entity)
{
Tags.Clear();
foreach (var t in entity.Tags) Tags.Add(t.Name);
}
private async System.Threading.Tasks.Task RefreshAvailableTagsAsync()
{
try
{
var all = await _worker.GetAllTagsAsync();
AvailableTags.Clear();
foreach (var t in all) AvailableTags.Add(t);
}
catch { }
}
private async System.Threading.Tasks.Task RefreshTagsAndStatusAsync(string taskId)
private async System.Threading.Tasks.Task RefreshStatusAsync(string taskId)
{
try
{
await using var ctx = await _dbFactory.CreateDbContextAsync();
var entity = await ctx.Tasks
.AsNoTracking()
.Include(t => t.Tags)
.FirstOrDefaultAsync(t => t.Id == taskId);
if (entity is null || Task?.Id != taskId) return;
_suppressStatusSave = true;
try { SelectedStatus = entity.Status; }
finally { _suppressStatusSave = false; }
AgentStatusLabel = entity.Status.ToString();
ApplyTagsFromEntity(entity);
await RefreshAvailableTagsAsync();
}
catch { }
}
@@ -289,9 +221,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
{
if (e.PropertyName == nameof(WorkerClient.IsConnected))
{
RunNowCommand.NotifyCanExecuteChanged();
EnqueueCommand.NotifyCanExecuteChanged();
DequeueCommand.NotifyCanExecuteChanged();
ResetAndRetryCommand.NotifyCanExecuteChanged();
ContinueCommand.NotifyCanExecuteChanged();
ResetCommand.NotifyCanExecuteChanged();
ApproveMergeCommand.NotifyCanExecuteChanged();
}
};
@@ -323,7 +256,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
_worker.TaskUpdatedEvent += taskId =>
{
if (Task?.Id == taskId) _ = RefreshTagsAndStatusAsync(taskId);
if (Task?.Id == taskId) _ = RefreshStatusAsync(taskId);
if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId);
};
@@ -503,13 +436,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
BranchLine = null;
AgentStatusLabel = "Idle";
LatestRunSessionId = null;
ShowFailedActions = false;
Tags.Clear();
AvailableTags.Clear();
NewTagInput = "";
_suppressStatusSave = true;
try { SelectedStatus = ClaudeDo.Data.Models.TaskStatus.Idle; }
finally { _suppressStatusSave = false; }
_suppressAgentSave = true;
try
{
@@ -537,11 +463,10 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
var subtaskRepo = new SubtaskRepository(ctx);
// Own query with Include so WorktreePath/BranchLine/Tags are populated.
// Own query with Include so WorktreePath/BranchLine are populated.
var entity = await ctx.Tasks
.AsNoTracking()
.Include(t => t.Worktree)
.Include(t => t.Tags)
.FirstOrDefaultAsync(t => t.Id == row.Id, ct);
ct.ThrowIfCancellationRequested();
if (entity == null) return;
@@ -557,11 +482,6 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
WorktreeStateLabel = entity.Worktree?.State.ToString();
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} \u2190 main" : null;
AgentStatusLabel = entity.Status.ToString();
_suppressStatusSave = true;
try { SelectedStatus = entity.Status; }
finally { _suppressStatusSave = false; }
ApplyTagsFromEntity(entity);
await RefreshAvailableTagsAsync();
await LoadAgentSettingsAsync(entity, ct);
ct.ThrowIfCancellationRequested();
@@ -926,24 +846,35 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
await _worker.CancelTaskAsync(Task.Id);
}
[RelayCommand(CanExecute = nameof(CanRunNow))]
private async System.Threading.Tasks.Task RunNowAsync()
[RelayCommand(CanExecute = nameof(CanEnqueue))]
private async System.Threading.Tasks.Task EnqueueAsync()
{
if (Task == null) return;
AgentStatusLabel = "Running";
try
{
await _worker.RunNowAsync(Task.Id);
}
catch
{
AgentStatusLabel = "Failed";
throw;
await _worker.SetTaskStatusAsync(Task.Id, ClaudeDo.Data.Models.TaskStatus.Queued);
AgentStatusLabel = "Queued";
}
catch { /* offline */ }
}
private bool CanRunNow() =>
Task != null && _worker.IsConnected && !IsRunning;
private bool CanEnqueue() =>
Task != null && _worker.IsConnected && IsIdle;
[RelayCommand(CanExecute = nameof(CanDequeue))]
private async System.Threading.Tasks.Task DequeueAsync()
{
if (Task == null) return;
try
{
await _worker.SetTaskStatusAsync(Task.Id, ClaudeDo.Data.Models.TaskStatus.Idle);
AgentStatusLabel = "Idle";
}
catch { /* offline */ }
}
private bool CanDequeue() =>
Task != null && _worker.IsConnected && IsQueued;
[RelayCommand(CanExecute = nameof(CanContinue))]
private async System.Threading.Tasks.Task ContinueAsync()
@@ -953,23 +884,32 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
}
private bool CanContinue() =>
Task != null && _worker.IsConnected && ShowFailedActions && !string.IsNullOrEmpty(LatestRunSessionId);
Task != null && _worker.IsConnected && ShowContinue && !string.IsNullOrEmpty(LatestRunSessionId);
[RelayCommand(CanExecute = nameof(CanReset))]
private async System.Threading.Tasks.Task ResetAsync()
[RelayCommand(CanExecute = nameof(CanResetAndRetry))]
private async System.Threading.Tasks.Task ResetAndRetryAsync()
{
if (Task == null) return;
if (ConfirmAsync == null) return;
var branchName = $"claudedo/{Task.Id.Replace("-", "")}";
var ok = await ConfirmAsync($"Discard worktree and reset task?\nThis deletes branch {branchName} and all uncommitted changes.");
var ok = await ConfirmAsync(
$"Reset and retry?\nThis discards branch {branchName} (and uncommitted changes), then queues the task to run from the beginning.");
if (!ok) return;
await _worker.ResetTaskAsync(Task.Id);
if (WorktreePath != null)
await _worker.ResetTaskAsync(Task.Id);
try
{
await _worker.SetTaskStatusAsync(Task.Id, ClaudeDo.Data.Models.TaskStatus.Queued);
AgentStatusLabel = "Queued";
}
catch { /* offline */ }
}
private bool CanReset() =>
Task != null && _worker.IsConnected && ShowFailedActions;
private bool CanResetAndRetry() =>
Task != null && _worker.IsConnected && ShowResetAndRetry;
}
public sealed partial class SubtaskRowViewModel : ViewModelBase