feat(claude-do): merge Task context menu: "Mark as → Idle" for cancelled tasks
ClaudeDo-Task: ad39dbb38d314e96803decc4e194e180
This commit is contained in:
@@ -209,6 +209,7 @@
|
|||||||
"ctxMarkAs": "Markieren als",
|
"ctxMarkAs": "Markieren als",
|
||||||
"ctxMarkDone": "Erledigt",
|
"ctxMarkDone": "Erledigt",
|
||||||
"ctxMarkCancelled": "Abgebrochen",
|
"ctxMarkCancelled": "Abgebrochen",
|
||||||
|
"ctxMarkIdle": "Offen",
|
||||||
"ctxOpenConPtySession": "Interaktive Sitzung öffnen",
|
"ctxOpenConPtySession": "Interaktive Sitzung öffnen",
|
||||||
"ctxOpenWorktree": "Worktree öffnen",
|
"ctxOpenWorktree": "Worktree öffnen",
|
||||||
"ctxOpenPlanningSession": "Planungssitzung öffnen",
|
"ctxOpenPlanningSession": "Planungssitzung öffnen",
|
||||||
|
|||||||
@@ -209,6 +209,7 @@
|
|||||||
"ctxMarkAs": "Mark as",
|
"ctxMarkAs": "Mark as",
|
||||||
"ctxMarkDone": "Done",
|
"ctxMarkDone": "Done",
|
||||||
"ctxMarkCancelled": "Cancelled",
|
"ctxMarkCancelled": "Cancelled",
|
||||||
|
"ctxMarkIdle": "Idle",
|
||||||
"ctxOpenConPtySession": "Open interactive session",
|
"ctxOpenConPtySession": "Open interactive session",
|
||||||
"ctxOpenWorktree": "Open worktree",
|
"ctxOpenWorktree": "Open worktree",
|
||||||
"ctxOpenPlanningSession": "Open planning Session",
|
"ctxOpenPlanningSession": "Open planning Session",
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ public partial class TaskRowView : UserControl
|
|||||||
var markAs = new MenuItem { Header = Loc.T("tasks.ctxMarkAs"), DataContext = row };
|
var markAs = new MenuItem { Header = Loc.T("tasks.ctxMarkAs"), DataContext = row };
|
||||||
markAs.Items.Add(MakeItem("tasks.ctxMarkDone", OnSetStatusClick, tag: "Done", reason: row.MarkAsDisabledReason));
|
markAs.Items.Add(MakeItem("tasks.ctxMarkDone", OnSetStatusClick, tag: "Done", reason: row.MarkAsDisabledReason));
|
||||||
markAs.Items.Add(MakeItem("tasks.ctxMarkCancelled", OnSetStatusClick, tag: "Cancelled", reason: row.MarkAsDisabledReason));
|
markAs.Items.Add(MakeItem("tasks.ctxMarkCancelled", OnSetStatusClick, tag: "Cancelled", reason: row.MarkAsDisabledReason));
|
||||||
|
markAs.Items.Add(MakeItem("tasks.ctxMarkIdle", OnSetStatusClick, row.Status == TaskStatus.Cancelled, tag: "Idle", reason: row.MarkAsDisabledReason));
|
||||||
markAs.Items.Add(new Separator());
|
markAs.Items.Add(new Separator());
|
||||||
markAs.Items.Add(MakeItem("tasks.ctxMarkManual", OnToggleManualClick, !row.IsManual));
|
markAs.Items.Add(MakeItem("tasks.ctxMarkManual", OnToggleManualClick, !row.IsManual));
|
||||||
markAs.Items.Add(MakeItem("tasks.ctxMarkClaudeTask", OnToggleManualClick, row.IsManual));
|
markAs.Items.Add(MakeItem("tasks.ctxMarkClaudeTask", OnToggleManualClick, row.IsManual));
|
||||||
|
|||||||
@@ -677,10 +677,14 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
|
|||||||
throw new HubException("Cannot change the status of a running task. Cancel it first.");
|
throw new HubException("Cannot change the status of a running task. Cancel it first.");
|
||||||
|
|
||||||
// Queueing goes through the gated transition so draft subtasks can't be queued;
|
// Queueing goes through the gated transition so draft subtasks can't be queued;
|
||||||
|
// Idle goes through ResetToIdleAsync so StartedAt/FinishedAt/Result are cleared;
|
||||||
// other statuses keep the unconditional "set status freely" affordance.
|
// other statuses keep the unconditional "set status freely" affordance.
|
||||||
var result = parsed == TaskStatus.Queued
|
var result = parsed switch
|
||||||
? await _state.EnqueueAsync(taskId, Context.ConnectionAborted)
|
{
|
||||||
: await _state.ForceSetStatusAsync(taskId, parsed, Context.ConnectionAborted);
|
TaskStatus.Queued => await _state.EnqueueAsync(taskId, Context.ConnectionAborted),
|
||||||
|
TaskStatus.Idle => await _state.ResetToIdleAsync(taskId, Context.ConnectionAborted),
|
||||||
|
_ => await _state.ForceSetStatusAsync(taskId, parsed, Context.ConnectionAborted),
|
||||||
|
};
|
||||||
if (!result.Ok) throw new HubException(result.Reason ?? "set status failed");
|
if (!result.Ok) throw new HubException(result.Reason ?? "set status failed");
|
||||||
return new SetTaskStatusResultDto(
|
return new SetTaskStatusResultDto(
|
||||||
result.BaseDirty is { } w ? new BaseDirtyWarningDto(w.ModifiedCount, w.UntrackedCount) : null);
|
result.BaseDirty is { } w ? new BaseDirtyWarningDto(w.ModifiedCount, w.UntrackedCount) : null);
|
||||||
|
|||||||
@@ -76,6 +76,38 @@ public sealed class SetTaskStatusHubTests : IDisposable
|
|||||||
Assert.Equal(TaskStatus.Cancelled, task!.Status);
|
Assert.Equal(TaskStatus.Cancelled, task!.Status);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task SetTaskStatus_Idle_OnCancelledTask_ClearsTimestampsAndKeepsWorktree()
|
||||||
|
{
|
||||||
|
var taskId = await SeedTaskAsync(TaskStatus.Cancelled);
|
||||||
|
await using (var seed = _db.CreateContext())
|
||||||
|
{
|
||||||
|
var t = (await seed.Tasks.FindAsync(taskId))!;
|
||||||
|
t.StartedAt = DateTime.UtcNow.AddMinutes(-5);
|
||||||
|
t.FinishedAt = DateTime.UtcNow;
|
||||||
|
t.Result = "stale";
|
||||||
|
await seed.SaveChangesAsync();
|
||||||
|
await new WorktreeRepository(seed).AddAsync(new WorktreeEntity
|
||||||
|
{
|
||||||
|
TaskId = taskId, Path = "/tmp/x", BranchName = "claudedo/x",
|
||||||
|
BaseCommit = "deadbeef", State = WorktreeState.Active, CreatedAt = DateTime.UtcNow,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
var hub = CreateHub();
|
||||||
|
|
||||||
|
await hub.SetTaskStatus(taskId, "Idle");
|
||||||
|
|
||||||
|
await using var ctx = _db.CreateContext();
|
||||||
|
var task = await ctx.Tasks.AsNoTracking().SingleAsync(t => t.Id == taskId);
|
||||||
|
Assert.Equal(TaskStatus.Idle, task.Status);
|
||||||
|
Assert.Null(task.StartedAt);
|
||||||
|
Assert.Null(task.FinishedAt);
|
||||||
|
Assert.Null(task.Result);
|
||||||
|
var wt = await ctx.Worktrees.AsNoTracking().SingleAsync(w => w.TaskId == taskId);
|
||||||
|
Assert.Equal(WorktreeState.Active, wt.State);
|
||||||
|
Assert.Equal("claudedo/x", wt.BranchName);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task SetTaskStatus_Missing_Throws()
|
public async Task SetTaskStatus_Missing_Throws()
|
||||||
{
|
{
|
||||||
|
|||||||
Reference in New Issue
Block a user