feat(ui): editable task status and tags from details panel
Adds a status ComboBox in the Details header (no transition guards) and a Tags section with chips + AutoCompleteBox. TaskRowViewModel.Tags becomes an ObservableCollection so chip lists stay live. TasksIsland caches AllTags for the row context menu and exposes Set/Toggle helpers. Test fakes updated for the new IWorkerClient methods. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
using System.ComponentModel;
|
using System.ComponentModel;
|
||||||
using ClaudeDo.Data.Models;
|
using ClaudeDo.Data.Models;
|
||||||
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||||
|
|
||||||
namespace ClaudeDo.Ui.Services;
|
namespace ClaudeDo.Ui.Services;
|
||||||
|
|
||||||
@@ -27,6 +28,9 @@ public interface IWorkerClient : INotifyPropertyChanged
|
|||||||
Task<List<AgentInfo>> GetAgentsAsync();
|
Task<List<AgentInfo>> GetAgentsAsync();
|
||||||
Task<ListConfigDto?> GetListConfigAsync(string listId);
|
Task<ListConfigDto?> GetListConfigAsync(string listId);
|
||||||
Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto);
|
Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto);
|
||||||
|
Task SetTaskStatusAsync(string taskId, TaskStatus status);
|
||||||
|
Task SetTaskTagsAsync(string taskId, IEnumerable<string> tagNames);
|
||||||
|
Task<List<string>> GetAllTagsAsync();
|
||||||
Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||||
Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default);
|
Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default);
|
||||||
Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default);
|
Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default);
|
||||||
|
|||||||
@@ -381,6 +381,28 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
|
|||||||
await _hub.InvokeAsync("UpdateTaskAgentSettings", dto);
|
await _hub.InvokeAsync("UpdateTaskAgentSettings", dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SetTaskStatusAsync(string taskId, ClaudeDo.Data.Models.TaskStatus status)
|
||||||
|
{
|
||||||
|
await _hub.InvokeAsync("SetTaskStatus", taskId, status.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task SetTaskTagsAsync(string taskId, IEnumerable<string> tagNames)
|
||||||
|
{
|
||||||
|
await _hub.InvokeAsync("SetTaskTags", taskId, tagNames.ToArray());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<List<string>> GetAllTagsAsync()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return await _hub.InvokeAsync<List<string>>("GetAllTags") ?? new List<string>();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return new List<string>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public async Task<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync()
|
public async Task<WorktreeCleanupDto?> CleanupFinishedWorktreesAsync()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
|
|||||||
@@ -57,6 +57,62 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
public string TaskIdBadge => Task != null ? $"#T{Task.Id[..Math.Min(3, Task.Id.Length)].ToUpperInvariant()}" : "";
|
public string TaskIdBadge => Task != null ? $"#T{Task.Id[..Math.Min(3, Task.Id.Length)].ToUpperInvariant()}" : "";
|
||||||
|
|
||||||
// Agent strip fields
|
// 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]
|
[ObservableProperty]
|
||||||
[NotifyCanExecuteChangedFor(nameof(RunNowCommand))]
|
[NotifyCanExecuteChangedFor(nameof(RunNowCommand))]
|
||||||
private string _agentStatusLabel = "Idle";
|
private string _agentStatusLabel = "Idle";
|
||||||
@@ -181,6 +237,44 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
// Set by the view so DeleteTaskCommand can show an error message
|
// Set by the view so DeleteTaskCommand can show an error message
|
||||||
public Func<string, System.Threading.Tasks.Task>? ShowErrorAsync { get; set; }
|
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)
|
||||||
|
{
|
||||||
|
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 { }
|
||||||
|
}
|
||||||
|
|
||||||
public DetailsIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IWorkerClient worker, IServiceProvider services)
|
public DetailsIslandViewModel(IDbContextFactory<ClaudeDoDbContext> dbFactory, IWorkerClient worker, IServiceProvider services)
|
||||||
{
|
{
|
||||||
_dbFactory = dbFactory;
|
_dbFactory = dbFactory;
|
||||||
@@ -229,6 +323,7 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
|
|
||||||
_worker.TaskUpdatedEvent += taskId =>
|
_worker.TaskUpdatedEvent += taskId =>
|
||||||
{
|
{
|
||||||
|
if (Task?.Id == taskId) _ = RefreshTagsAndStatusAsync(taskId);
|
||||||
if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId);
|
if (Task?.IsPlanningParent == true) _ = RefreshPlanningChildAsync(taskId);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -409,6 +504,12 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
AgentStatusLabel = "Idle";
|
AgentStatusLabel = "Idle";
|
||||||
LatestRunSessionId = null;
|
LatestRunSessionId = null;
|
||||||
ShowFailedActions = false;
|
ShowFailedActions = false;
|
||||||
|
Tags.Clear();
|
||||||
|
AvailableTags.Clear();
|
||||||
|
NewTagInput = "";
|
||||||
|
_suppressStatusSave = true;
|
||||||
|
try { SelectedStatus = ClaudeDo.Data.Models.TaskStatus.Idle; }
|
||||||
|
finally { _suppressStatusSave = false; }
|
||||||
_suppressAgentSave = true;
|
_suppressAgentSave = true;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -436,10 +537,11 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
await using var ctx = await _dbFactory.CreateDbContextAsync(ct);
|
||||||
var subtaskRepo = new SubtaskRepository(ctx);
|
var subtaskRepo = new SubtaskRepository(ctx);
|
||||||
|
|
||||||
// Own query with Include so WorktreePath/BranchLine are populated.
|
// Own query with Include so WorktreePath/BranchLine/Tags are populated.
|
||||||
var entity = await ctx.Tasks
|
var entity = await ctx.Tasks
|
||||||
.AsNoTracking()
|
.AsNoTracking()
|
||||||
.Include(t => t.Worktree)
|
.Include(t => t.Worktree)
|
||||||
|
.Include(t => t.Tags)
|
||||||
.FirstOrDefaultAsync(t => t.Id == row.Id, ct);
|
.FirstOrDefaultAsync(t => t.Id == row.Id, ct);
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
if (entity == null) return;
|
if (entity == null) return;
|
||||||
@@ -455,6 +557,11 @@ public sealed partial class DetailsIslandViewModel : ViewModelBase
|
|||||||
WorktreeStateLabel = entity.Worktree?.State.ToString();
|
WorktreeStateLabel = entity.Worktree?.State.ToString();
|
||||||
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} \u2190 main" : null;
|
BranchLine = entity.Worktree is { } w ? $"{w.BranchName} \u2190 main" : null;
|
||||||
AgentStatusLabel = entity.Status.ToString();
|
AgentStatusLabel = entity.Status.ToString();
|
||||||
|
_suppressStatusSave = true;
|
||||||
|
try { SelectedStatus = entity.Status; }
|
||||||
|
finally { _suppressStatusSave = false; }
|
||||||
|
ApplyTagsFromEntity(entity);
|
||||||
|
await RefreshAvailableTagsAsync();
|
||||||
await LoadAgentSettingsAsync(entity, ct);
|
await LoadAgentSettingsAsync(entity, ct);
|
||||||
ct.ThrowIfCancellationRequested();
|
ct.ThrowIfCancellationRequested();
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
using ClaudeDo.Data.Models;
|
using ClaudeDo.Data.Models;
|
||||||
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
using TaskStatus = ClaudeDo.Data.Models.TaskStatus;
|
||||||
@@ -7,6 +8,11 @@ namespace ClaudeDo.Ui.ViewModels.Islands;
|
|||||||
|
|
||||||
public sealed partial class TaskRowViewModel : ViewModelBase
|
public sealed partial class TaskRowViewModel : ViewModelBase
|
||||||
{
|
{
|
||||||
|
public TaskRowViewModel()
|
||||||
|
{
|
||||||
|
Tags.CollectionChanged += (_, _) => OnPropertyChanged(nameof(HasTags));
|
||||||
|
}
|
||||||
|
|
||||||
public required string Id { get; init; }
|
public required string Id { get; init; }
|
||||||
[ObservableProperty] private string _title = "";
|
[ObservableProperty] private string _title = "";
|
||||||
[ObservableProperty] private string _listName = "";
|
[ObservableProperty] private string _listName = "";
|
||||||
@@ -33,7 +39,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
public DateTime CreatedAt { get; init; }
|
public DateTime CreatedAt { get; init; }
|
||||||
public string CreatedAtFormatted => CreatedAt == default ? "—" : $"Created {CreatedAt:MMM d}";
|
public string CreatedAtFormatted => CreatedAt == default ? "—" : $"Created {CreatedAt:MMM d}";
|
||||||
|
|
||||||
public IReadOnlyList<string> Tags { get; init; } = Array.Empty<string>();
|
public ObservableCollection<string> Tags { get; } = new();
|
||||||
public int StepsCount { get; init; }
|
public int StepsCount { get; init; }
|
||||||
public int StepsCompleted { get; init; }
|
public int StepsCompleted { get; init; }
|
||||||
|
|
||||||
@@ -154,6 +160,15 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
|||||||
DiffDeletions = del;
|
DiffDeletions = del;
|
||||||
ParentTaskId = t.ParentTaskId;
|
ParentTaskId = t.ParentTaskId;
|
||||||
BlockedByTaskId = t.BlockedByTaskId;
|
BlockedByTaskId = t.BlockedByTaskId;
|
||||||
|
SetTags(t.Tags.Select(tag => tag.Name));
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetTags(IEnumerable<string> names)
|
||||||
|
{
|
||||||
|
var snapshot = names.ToList();
|
||||||
|
if (Tags.SequenceEqual(snapshot)) return;
|
||||||
|
Tags.Clear();
|
||||||
|
foreach (var n in snapshot) Tags.Add(n);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Best-effort parse of diff stat strings like "+12 -3" or "12 additions, 3 deletions".
|
// Best-effort parse of diff stat strings like "+12 -3" or "12 additions, 3 deletions".
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
|||||||
public ObservableCollection<TaskRowViewModel> OverdueItems { get; } = new();
|
public ObservableCollection<TaskRowViewModel> OverdueItems { get; } = new();
|
||||||
public ObservableCollection<TaskRowViewModel> OpenItems { get; } = new();
|
public ObservableCollection<TaskRowViewModel> OpenItems { get; } = new();
|
||||||
public ObservableCollection<TaskRowViewModel> CompletedItems { get; } = new();
|
public ObservableCollection<TaskRowViewModel> CompletedItems { get; } = new();
|
||||||
|
public ObservableCollection<string> AllTags { get; } = new();
|
||||||
|
|
||||||
[ObservableProperty] private string _newTaskTitle = "";
|
[ObservableProperty] private string _newTaskTitle = "";
|
||||||
[ObservableProperty] private TaskRowViewModel? _selectedTask;
|
[ObservableProperty] private TaskRowViewModel? _selectedTask;
|
||||||
@@ -54,9 +55,22 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
|||||||
_worker.TaskUpdatedEvent += OnWorkerTaskUpdated;
|
_worker.TaskUpdatedEvent += OnWorkerTaskUpdated;
|
||||||
_worker.WorktreeUpdatedEvent += OnWorkerTaskUpdated;
|
_worker.WorktreeUpdatedEvent += OnWorkerTaskUpdated;
|
||||||
_worker.TaskMessageEvent += OnWorkerTaskMessage;
|
_worker.TaskMessageEvent += OnWorkerTaskMessage;
|
||||||
|
_ = RefreshAllTagsAsync();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task RefreshAllTagsAsync()
|
||||||
|
{
|
||||||
|
if (_worker is null) return;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var tags = await _worker.GetAllTagsAsync();
|
||||||
|
AllTags.Clear();
|
||||||
|
foreach (var t in tags) AllTags.Add(t);
|
||||||
|
}
|
||||||
|
catch { /* offline */ }
|
||||||
|
}
|
||||||
|
|
||||||
private void OnWorkerTaskMessage(string taskId, string line)
|
private void OnWorkerTaskMessage(string taskId, string line)
|
||||||
{
|
{
|
||||||
var row = Items.FirstOrDefault(r => r.Id == taskId);
|
var row = Items.FirstOrDefault(r => r.Id == taskId);
|
||||||
@@ -83,6 +97,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
|||||||
var entity = await db.Tasks
|
var entity = await db.Tasks
|
||||||
.Include(t => t.List)
|
.Include(t => t.List)
|
||||||
.Include(t => t.Worktree)
|
.Include(t => t.Worktree)
|
||||||
|
.Include(t => t.Tags)
|
||||||
.FirstOrDefaultAsync(t => t.Id == taskId);
|
.FirstOrDefaultAsync(t => t.Id == taskId);
|
||||||
|
|
||||||
var existing = Items.FirstOrDefault(r => r.Id == taskId);
|
var existing = Items.FirstOrDefault(r => r.Id == taskId);
|
||||||
@@ -171,6 +186,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
|||||||
var all = await db.Tasks
|
var all = await db.Tasks
|
||||||
.Include(t => t.List)
|
.Include(t => t.List)
|
||||||
.Include(t => t.Worktree)
|
.Include(t => t.Worktree)
|
||||||
|
.Include(t => t.Tags)
|
||||||
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
.OrderBy(t => t.SortOrder).ThenBy(t => t.CreatedAt)
|
||||||
.ToListAsync(ct);
|
.ToListAsync(ct);
|
||||||
|
|
||||||
@@ -462,6 +478,30 @@ public sealed partial class TasksIslandViewModel : ViewModelBase
|
|||||||
TasksChanged?.Invoke(this, EventArgs.Empty);
|
TasksChanged?.Invoke(this, EventArgs.Empty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public async Task SetStatusOnRowAsync(TaskRowViewModel row, TaskStatus status)
|
||||||
|
{
|
||||||
|
if (_worker is null) return;
|
||||||
|
try { await _worker.SetTaskStatusAsync(row.Id, status); }
|
||||||
|
catch { /* offline; broadcast won't fire */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task ToggleTagOnRowAsync(TaskRowViewModel row, string tagName)
|
||||||
|
{
|
||||||
|
if (_worker is null) return;
|
||||||
|
var name = tagName.Trim().ToLowerInvariant();
|
||||||
|
if (name.Length == 0) return;
|
||||||
|
var current = row.Tags.ToList();
|
||||||
|
var next = current.Contains(name)
|
||||||
|
? current.Where(t => t != name).ToList()
|
||||||
|
: current.Append(name).ToList();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await _worker.SetTaskTagsAsync(row.Id, next);
|
||||||
|
await RefreshAllTagsAsync();
|
||||||
|
}
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
|
||||||
[RelayCommand]
|
[RelayCommand]
|
||||||
private async Task SendToQueueAsync(TaskRowViewModel? row)
|
private async Task SendToQueueAsync(TaskRowViewModel? row)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
|
|
||||||
<!-- ── Header (sticky top): eyebrow · title · gear (agent-settings flyout) ── -->
|
<!-- ── Header (sticky top): eyebrow · title · gear (agent-settings flyout) ── -->
|
||||||
<Border DockPanel.Dock="Top" Classes="island-header">
|
<Border DockPanel.Dock="Top" Classes="island-header">
|
||||||
<Grid ColumnDefinitions="*,Auto">
|
<Grid ColumnDefinitions="*,Auto,Auto">
|
||||||
<StackPanel Grid.Column="0" Spacing="0">
|
<StackPanel Grid.Column="0" Spacing="0">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,0,0,4">
|
<StackPanel Orientation="Horizontal" Spacing="6" Margin="0,0,0,4">
|
||||||
<Ellipse Width="5" Height="5" Fill="{DynamicResource AccentBrush}"
|
<Ellipse Width="5" Height="5" Fill="{DynamicResource AccentBrush}"
|
||||||
@@ -56,7 +56,15 @@
|
|||||||
Padding="0"/>
|
Padding="0"/>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<Button Grid.Column="1" Classes="icon-btn"
|
<ComboBox Grid.Column="1"
|
||||||
|
ItemsSource="{Binding StatusOptions}"
|
||||||
|
SelectedItem="{Binding SelectedStatus, Mode=TwoWay}"
|
||||||
|
ToolTip.Tip="Set status (no transition guards)"
|
||||||
|
VerticalAlignment="Top"
|
||||||
|
MinWidth="110"
|
||||||
|
Margin="6,0,0,0"/>
|
||||||
|
|
||||||
|
<Button Grid.Column="2" Classes="icon-btn"
|
||||||
ToolTip.Tip="Agent settings"
|
ToolTip.Tip="Agent settings"
|
||||||
IsEnabled="{Binding IsAgentSectionEnabled}"
|
IsEnabled="{Binding IsAgentSectionEnabled}"
|
||||||
VerticalAlignment="Top"
|
VerticalAlignment="Top"
|
||||||
@@ -139,6 +147,46 @@
|
|||||||
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
<ScrollViewer VerticalScrollBarVisibility="Auto">
|
||||||
<StackPanel Spacing="0">
|
<StackPanel Spacing="0">
|
||||||
|
|
||||||
|
<!-- Tags section -->
|
||||||
|
<Border Padding="18,12,18,12"
|
||||||
|
BorderBrush="{DynamicResource LineBrush}"
|
||||||
|
BorderThickness="0,0,0,1">
|
||||||
|
<StackPanel Spacing="6">
|
||||||
|
<TextBlock Classes="section-label" Text="TAGS" Margin="0,0,0,2"/>
|
||||||
|
<ItemsControl ItemsSource="{Binding Tags}">
|
||||||
|
<ItemsControl.ItemsPanel>
|
||||||
|
<ItemsPanelTemplate>
|
||||||
|
<WrapPanel Orientation="Horizontal"/>
|
||||||
|
</ItemsPanelTemplate>
|
||||||
|
</ItemsControl.ItemsPanel>
|
||||||
|
<ItemsControl.ItemTemplate>
|
||||||
|
<DataTemplate x:DataType="x:String">
|
||||||
|
<Border Classes="chip chip-tag" Margin="0,0,6,4">
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="4" VerticalAlignment="Center">
|
||||||
|
<TextBlock Text="{Binding}" VerticalAlignment="Center"/>
|
||||||
|
<Button Classes="icon-btn"
|
||||||
|
Padding="2,0"
|
||||||
|
VerticalAlignment="Center"
|
||||||
|
ToolTip.Tip="Remove tag"
|
||||||
|
Command="{Binding $parent[ItemsControl].((vm:DetailsIslandViewModel)DataContext).RemoveTagCommand}"
|
||||||
|
CommandParameter="{Binding}">
|
||||||
|
<TextBlock Text="×" FontSize="12"/>
|
||||||
|
</Button>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
</DataTemplate>
|
||||||
|
</ItemsControl.ItemTemplate>
|
||||||
|
</ItemsControl>
|
||||||
|
<AutoCompleteBox ItemsSource="{Binding AvailableTags}"
|
||||||
|
Text="{Binding NewTagInput, Mode=TwoWay}"
|
||||||
|
Watermark="Add tag (Enter to add)">
|
||||||
|
<AutoCompleteBox.KeyBindings>
|
||||||
|
<KeyBinding Gesture="Enter" Command="{Binding AddTagCommand}"/>
|
||||||
|
</AutoCompleteBox.KeyBindings>
|
||||||
|
</AutoCompleteBox>
|
||||||
|
</StackPanel>
|
||||||
|
</Border>
|
||||||
|
|
||||||
<!-- Planning merge section — visible only for planning parent tasks -->
|
<!-- Planning merge section — visible only for planning parent tasks -->
|
||||||
<Border Padding="18,12,18,12"
|
<Border Padding="18,12,18,12"
|
||||||
BorderBrush="{DynamicResource LineBrush}"
|
BorderBrush="{DynamicResource LineBrush}"
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ public class ConflictResolutionViewModelTests
|
|||||||
public Task<List<AgentInfo>> GetAgentsAsync() => Task.FromResult(new List<AgentInfo>());
|
public Task<List<AgentInfo>> GetAgentsAsync() => Task.FromResult(new List<AgentInfo>());
|
||||||
public Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult<ListConfigDto?>(null);
|
public Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult<ListConfigDto?>(null);
|
||||||
public Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask;
|
public Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask;
|
||||||
|
public Task SetTaskStatusAsync(string taskId, ClaudeDo.Data.Models.TaskStatus status) => Task.CompletedTask;
|
||||||
|
public Task SetTaskTagsAsync(string taskId, IEnumerable<string> tagNames) => Task.CompletedTask;
|
||||||
|
public Task<List<string>> GetAllTagsAsync() => Task.FromResult(new List<string>());
|
||||||
public Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
public Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
public Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
public Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
public Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
public Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
|
|||||||
@@ -67,6 +67,9 @@ public class DetailsIslandPlanningTests : IDisposable
|
|||||||
public Task<List<AgentInfo>> GetAgentsAsync() => Task.FromResult(new List<AgentInfo>());
|
public Task<List<AgentInfo>> GetAgentsAsync() => Task.FromResult(new List<AgentInfo>());
|
||||||
public Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult<ListConfigDto?>(null);
|
public Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult<ListConfigDto?>(null);
|
||||||
public Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask;
|
public Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask;
|
||||||
|
public Task SetTaskStatusAsync(string taskId, TaskStatus status) => Task.CompletedTask;
|
||||||
|
public Task SetTaskTagsAsync(string taskId, IEnumerable<string> tagNames) => Task.CompletedTask;
|
||||||
|
public Task<List<string>> GetAllTagsAsync() => Task.FromResult(new List<string>());
|
||||||
public Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
public Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
public Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
public Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
public Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
public Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
|
|||||||
@@ -34,6 +34,9 @@ public class PlanningDiffViewModelTests
|
|||||||
public Task<List<AgentInfo>> GetAgentsAsync() => Task.FromResult(new List<AgentInfo>());
|
public Task<List<AgentInfo>> GetAgentsAsync() => Task.FromResult(new List<AgentInfo>());
|
||||||
public Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult<ListConfigDto?>(null);
|
public Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult<ListConfigDto?>(null);
|
||||||
public Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask;
|
public Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask;
|
||||||
|
public Task SetTaskStatusAsync(string taskId, ClaudeDo.Data.Models.TaskStatus status) => Task.CompletedTask;
|
||||||
|
public Task SetTaskTagsAsync(string taskId, IEnumerable<string> tagNames) => Task.CompletedTask;
|
||||||
|
public Task<List<string>> GetAllTagsAsync() => Task.FromResult(new List<string>());
|
||||||
public Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
public Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
public Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
public Task ResumePlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
public Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
public Task DiscardPlanningSessionAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
|
|||||||
@@ -37,6 +37,9 @@ sealed class FakeWorkerClient : IWorkerClient
|
|||||||
public Task<List<AgentInfo>> GetAgentsAsync() => Task.FromResult(new List<AgentInfo>());
|
public Task<List<AgentInfo>> GetAgentsAsync() => Task.FromResult(new List<AgentInfo>());
|
||||||
public Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult<ListConfigDto?>(null);
|
public Task<ListConfigDto?> GetListConfigAsync(string listId) => Task.FromResult<ListConfigDto?>(null);
|
||||||
public Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask;
|
public Task UpdateTaskAgentSettingsAsync(UpdateTaskAgentSettingsDto dto) => Task.CompletedTask;
|
||||||
|
public Task SetTaskStatusAsync(string taskId, TaskStatus status) => Task.CompletedTask;
|
||||||
|
public Task SetTaskTagsAsync(string taskId, IEnumerable<string> tagNames) => Task.CompletedTask;
|
||||||
|
public Task<List<string>> GetAllTagsAsync() => Task.FromResult(new List<string>());
|
||||||
public Task WakeQueueAsync() { WakeQueueCalls++; return Task.CompletedTask; }
|
public Task WakeQueueAsync() { WakeQueueCalls++; return Task.CompletedTask; }
|
||||||
public Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) { StartPlanningCalls++; return Task.CompletedTask; }
|
public Task StartPlanningSessionAsync(string taskId, CancellationToken ct = default) { StartPlanningCalls++; return Task.CompletedTask; }
|
||||||
public Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
public Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask;
|
||||||
|
|||||||
Reference in New Issue
Block a user