feat(ui,worker): surface task numbers in row/detail UI and worker log
Slice 4/5 of task-numbers: TaskRowViewModel.Number renders as a dimmed "#123" before the row title; DetailsIslandViewModel.TaskIdBadge now shows "#123" instead of the unusable "#T<guid-prefix>" handle; and the curated WorkerLog business events in TaskRunner, TaskMergeService, and TaskResetService prefix their quoted title with "#<Number>".
This commit is contained in:
@@ -315,7 +315,7 @@ public sealed class TaskMergeService
|
||||
"Auto-rebase of task {TaskId} branch {Branch} onto {Target} failed, left as-is: {Detail}",
|
||||
candidate.Id, wt.BranchName, targetBranch, detail);
|
||||
await _broadcaster.WorkerLog(
|
||||
$"Auto-rebase failed for \"{candidate.Title}\" onto {targetBranch} — left as-is ({detail})",
|
||||
$"Auto-rebase failed for #{candidate.Number} \"{candidate.Title}\" onto {targetBranch} — left as-is ({detail})",
|
||||
WorkerLogLevel.Warn, DateTime.UtcNow);
|
||||
return;
|
||||
}
|
||||
@@ -429,7 +429,7 @@ public sealed class TaskMergeService
|
||||
if (verifyFailure is not null)
|
||||
{
|
||||
_logger.LogWarning("Verify command failed after merging task {TaskId}: {Reason}", taskId, verifyFailure.ErrorMessage);
|
||||
await _broadcaster.WorkerLog($"Verify failed for \"{task.Title}\" after merge into {targetBranch}", WorkerLogLevel.Warn, DateTime.UtcNow);
|
||||
await _broadcaster.WorkerLog($"Verify failed for #{task.Number} \"{task.Title}\" after merge into {targetBranch}", WorkerLogLevel.Warn, DateTime.UtcNow);
|
||||
return verifyFailure;
|
||||
}
|
||||
|
||||
@@ -438,7 +438,7 @@ public sealed class TaskMergeService
|
||||
_logger.LogInformation(
|
||||
"Merged task {TaskId} branch {Branch} into {Target} (remove worktree: {Remove})",
|
||||
taskId, wt.BranchName, targetBranch, removeWorktree);
|
||||
await _broadcaster.WorkerLog($"Merged \"{task.Title}\" into {targetBranch}", WorkerLogLevel.Success, DateTime.UtcNow);
|
||||
await _broadcaster.WorkerLog($"Merged #{task.Number} \"{task.Title}\" into {targetBranch}", WorkerLogLevel.Success, DateTime.UtcNow);
|
||||
|
||||
return new MergeResult(StatusMerged, Array.Empty<string>(), cleanupWarning);
|
||||
}
|
||||
@@ -523,7 +523,7 @@ public sealed class TaskMergeService
|
||||
if (verifyFailure is not null)
|
||||
{
|
||||
_logger.LogWarning("Verify command failed after continuing merge of task {TaskId}: {Reason}", taskId, verifyFailure.ErrorMessage);
|
||||
await _broadcaster.WorkerLog($"Verify failed for \"{task.Title}\" after merge", WorkerLogLevel.Warn, DateTime.UtcNow);
|
||||
await _broadcaster.WorkerLog($"Verify failed for #{task.Number} \"{task.Title}\" after merge", WorkerLogLevel.Warn, DateTime.UtcNow);
|
||||
return verifyFailure;
|
||||
}
|
||||
|
||||
@@ -625,7 +625,7 @@ public sealed class TaskMergeService
|
||||
_logger.LogInformation(
|
||||
"Reverted merge of task {TaskId} (merge commit {MergeSha}) via revert commit {RevertSha}",
|
||||
taskId, wt.MergeCommit, revertSha);
|
||||
await _broadcaster.WorkerLog($"Reverted merge of \"{task.Title}\"", WorkerLogLevel.Warn, DateTime.UtcNow);
|
||||
await _broadcaster.WorkerLog($"Reverted merge of #{task.Number} \"{task.Title}\"", WorkerLogLevel.Warn, DateTime.UtcNow);
|
||||
|
||||
return new RevertResult(StatusReverted, revertSha, Array.Empty<string>(), null);
|
||||
}
|
||||
|
||||
@@ -55,7 +55,7 @@ public sealed class TaskResetService
|
||||
if (wt is not null && wt.State == WorktreeState.Active && list.WorkingDir is not null)
|
||||
{
|
||||
await _wtManager.DiscardAsync(wt, list.WorkingDir, ct);
|
||||
await _broadcaster.WorkerLog($"Discarded worktree for \"{task.Title}\"", WorkerLogLevel.Warn, DateTime.UtcNow);
|
||||
await _broadcaster.WorkerLog($"Discarded worktree for #{task.Number} \"{task.Title}\"", WorkerLogLevel.Warn, DateTime.UtcNow);
|
||||
worktreeChanged = true;
|
||||
}
|
||||
|
||||
@@ -66,6 +66,6 @@ public sealed class TaskResetService
|
||||
await _broadcaster.WorktreeUpdated(taskId);
|
||||
|
||||
_logger.LogInformation("Reset task {TaskId} to Idle (worktree discarded: {Discarded})", taskId, worktreeChanged);
|
||||
await _broadcaster.WorkerLog($"Reset \"{task.Title}\"", WorkerLogLevel.Warn, DateTime.UtcNow);
|
||||
await _broadcaster.WorkerLog($"Reset #{task.Number} \"{task.Title}\"", WorkerLogLevel.Warn, DateTime.UtcNow);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -73,7 +73,7 @@ public sealed class TaskRunner
|
||||
list = await listRepo.GetByIdAsync(task.ListId, ct);
|
||||
if (list is null)
|
||||
{
|
||||
await MarkFailed(task.Id, task.Title, slot, "List not found.");
|
||||
await MarkFailed(task.Id, task.Number, task.Title, slot, "List not found.");
|
||||
return;
|
||||
}
|
||||
listConfig = await listRepo.GetConfigAsync(task.ListId, ct);
|
||||
@@ -117,7 +117,7 @@ public sealed class TaskRunner
|
||||
var prep = await PrepareRunDirectoryAsync(task, list, ct);
|
||||
if (prep.FailureReason is not null)
|
||||
{
|
||||
await MarkFailed(task.Id, task.Title, slot, prep.FailureReason);
|
||||
await MarkFailed(task.Id, task.Number, task.Title, slot, prep.FailureReason);
|
||||
return;
|
||||
}
|
||||
var wtCtx = prep.WtCtx;
|
||||
@@ -135,7 +135,7 @@ public sealed class TaskRunner
|
||||
attachmentPaths);
|
||||
|
||||
// Run 1.
|
||||
var result = await RunOnceAsync(task.Id, task.Title, slot, runDir, resolvedConfig, 1, false, prompt, ct);
|
||||
var result = await RunOnceAsync(task.Id, task.Number, task.Title, slot, runDir, resolvedConfig, 1, false, prompt, ct);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
@@ -150,7 +150,7 @@ public sealed class TaskRunner
|
||||
var retryConfig = resolvedConfig with { ResumeSessionId = result.SessionId };
|
||||
var retryPrompt = BuildRetryPrompt(result.ErrorMarkdown);
|
||||
|
||||
var retryResult = await RunOnceAsync(task.Id, task.Title, slot, runDir, retryConfig, 2, true, retryPrompt, ct);
|
||||
var retryResult = await RunOnceAsync(task.Id, task.Number, task.Title, slot, runDir, retryConfig, 2, true, retryPrompt, ct);
|
||||
|
||||
if (retryResult.IsSuccess)
|
||||
{
|
||||
@@ -158,13 +158,13 @@ public sealed class TaskRunner
|
||||
}
|
||||
else
|
||||
{
|
||||
await MarkFailed(task.Id, task.Title, slot, retryResult.ErrorMarkdown, retryResult.TurnCount,
|
||||
await MarkFailed(task.Id, task.Number, task.Title, slot, retryResult.ErrorMarkdown, retryResult.TurnCount,
|
||||
retryConfig.MaxTurns, ClassifyFailureReason(retryResult.TerminalReason));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
await MarkFailed(task.Id, task.Title, slot, result.ErrorMarkdown, result.TurnCount,
|
||||
await MarkFailed(task.Id, task.Number, task.Title, slot, result.ErrorMarkdown, result.TurnCount,
|
||||
resolvedConfig.MaxTurns, ClassifyFailureReason(result.TerminalReason));
|
||||
}
|
||||
}
|
||||
@@ -174,12 +174,12 @@ public sealed class TaskRunner
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("Task {TaskId} was cancelled", task.Id);
|
||||
await MarkFailed(task.Id, task.Title, slot, "Task cancelled.", failureReason: "cancelled");
|
||||
await MarkFailed(task.Id, task.Number, task.Title, slot, "Task cancelled.", failureReason: "cancelled");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unhandled exception running task {TaskId}", task.Id);
|
||||
await MarkFailed(task.Id, task.Title, slot, $"Unhandled error: {ex.Message}");
|
||||
await MarkFailed(task.Id, task.Number, task.Title, slot, $"Unhandled error: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -264,7 +264,7 @@ public sealed class TaskRunner
|
||||
try
|
||||
{
|
||||
var nextRunNumber = lastRun.RunNumber + 1;
|
||||
var result = await RunOnceAsync(taskId, task.Title, slot, runDir, resolvedConfig, nextRunNumber, false, followUpPrompt, ct);
|
||||
var result = await RunOnceAsync(taskId, task.Number, task.Title, slot, runDir, resolvedConfig, nextRunNumber, false, followUpPrompt, ct);
|
||||
|
||||
if (result.IsSuccess)
|
||||
{
|
||||
@@ -272,7 +272,7 @@ public sealed class TaskRunner
|
||||
}
|
||||
else
|
||||
{
|
||||
await MarkFailed(taskId, task.Title, slot, result.ErrorMarkdown, result.TurnCount,
|
||||
await MarkFailed(taskId, task.Number, task.Title, slot, result.ErrorMarkdown, result.TurnCount,
|
||||
resolvedConfig.MaxTurns, ClassifyFailureReason(result.TerminalReason));
|
||||
}
|
||||
|
||||
@@ -281,12 +281,12 @@ public sealed class TaskRunner
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
_logger.LogInformation("Task {TaskId} was cancelled during continue", taskId);
|
||||
await MarkFailed(taskId, task.Title, slot, "Task cancelled.", failureReason: "cancelled");
|
||||
await MarkFailed(taskId, task.Number, task.Title, slot, "Task cancelled.", failureReason: "cancelled");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Unhandled exception continuing task {TaskId}", taskId);
|
||||
await MarkFailed(taskId, task.Title, slot, $"Unhandled error: {ex.Message}");
|
||||
await MarkFailed(taskId, task.Number, task.Title, slot, $"Unhandled error: {ex.Message}");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -329,7 +329,7 @@ public sealed class TaskRunner
|
||||
try
|
||||
{
|
||||
var wtCtx = await _wtManager.CreateAsync(task, list, ct);
|
||||
await _broadcaster.WorkerLog($"Created worktree for \"{task.Title}\"", WorkerLogLevel.Info, DateTime.UtcNow);
|
||||
await _broadcaster.WorkerLog($"Created worktree for #{task.Number} \"{task.Title}\"", WorkerLogLevel.Info, DateTime.UtcNow);
|
||||
// The worktrees row was just inserted; without this the UI keeps showing the task
|
||||
// as having no worktree until some unrelated event happens to refresh it.
|
||||
await _broadcaster.WorktreeUpdated(task.Id);
|
||||
@@ -348,7 +348,7 @@ public sealed class TaskRunner
|
||||
}
|
||||
|
||||
private async Task<RunResult> RunOnceAsync(
|
||||
string taskId, string taskTitle, string slot, string runDir, ClaudeRunConfig config,
|
||||
string taskId, int taskNumber, string taskTitle, string slot, string runDir, ClaudeRunConfig config,
|
||||
int runNumber, bool isRetry, string prompt, CancellationToken ct)
|
||||
{
|
||||
var runId = Guid.NewGuid().ToString();
|
||||
@@ -383,7 +383,7 @@ public sealed class TaskRunner
|
||||
|
||||
try
|
||||
{
|
||||
await _broadcaster.WorkerLog($"Started Claude for \"{taskTitle}\"", WorkerLogLevel.Info, DateTime.UtcNow);
|
||||
await _broadcaster.WorkerLog($"Started Claude for #{taskNumber} \"{taskTitle}\"", WorkerLogLevel.Info, DateTime.UtcNow);
|
||||
var result = await _claude.RunAsync(
|
||||
arguments,
|
||||
prompt,
|
||||
@@ -486,7 +486,7 @@ public sealed class TaskRunner
|
||||
committed = await _wtManager.CommitIfChangedAsync(wtCtx, task, list, ct);
|
||||
if (committed)
|
||||
{
|
||||
await _broadcaster.WorkerLog($"Committed changes in \"{task.Title}\"", WorkerLogLevel.Info, DateTime.UtcNow);
|
||||
await _broadcaster.WorkerLog($"Committed changes in #{task.Number} \"{task.Title}\"", WorkerLogLevel.Info, DateTime.UtcNow);
|
||||
await _broadcaster.WorktreeUpdated(task.Id);
|
||||
}
|
||||
}
|
||||
@@ -505,7 +505,7 @@ public sealed class TaskRunner
|
||||
if (wtCtx is not null && !committed && deniedWrites.Count > 0)
|
||||
{
|
||||
await MarkFailed(
|
||||
task.Id, task.Title, slot,
|
||||
task.Id, task.Number, task.Title, slot,
|
||||
$"All edits were blocked by permission denials ({string.Join(", ", deniedWrites)}) and nothing " +
|
||||
"was changed. Check the run's permission mode (get_effective_run_config).",
|
||||
result.TurnCount);
|
||||
@@ -542,20 +542,20 @@ public sealed class TaskRunner
|
||||
foreach (var child in pendingChildren)
|
||||
await _state.EnqueueAsync(child.Id, CancellationToken.None);
|
||||
await _broadcaster.WorkerLog(
|
||||
$"Finished \"{task.Title}\" (waiting on {pendingChildren.Count} improvement(s))",
|
||||
$"Finished #{task.Number} \"{task.Title}\" (waiting on {pendingChildren.Count} improvement(s))",
|
||||
WorkerLogLevel.Success, DateTime.UtcNow);
|
||||
await _broadcaster.TaskFinished(slot, task.Id, "waiting_for_children", finishedAt);
|
||||
}
|
||||
else if (isStandalone)
|
||||
{
|
||||
await _state.SubmitForReviewAsync(task.Id, finishedAt, reviewResult, CancellationToken.None);
|
||||
await _broadcaster.WorkerLog($"Finished \"{task.Title}\" (waiting for review)", WorkerLogLevel.Success, DateTime.UtcNow);
|
||||
await _broadcaster.WorkerLog($"Finished #{task.Number} \"{task.Title}\" (waiting for review)", WorkerLogLevel.Success, DateTime.UtcNow);
|
||||
await _broadcaster.TaskFinished(slot, task.Id, "waiting_for_review", finishedAt);
|
||||
}
|
||||
else
|
||||
{
|
||||
await _state.CompleteAsync(task.Id, finishedAt, reviewResult, CancellationToken.None);
|
||||
await _broadcaster.WorkerLog($"Finished \"{task.Title}\" (done)", WorkerLogLevel.Success, DateTime.UtcNow);
|
||||
await _broadcaster.WorkerLog($"Finished #{task.Number} \"{task.Title}\" (done)", WorkerLogLevel.Success, DateTime.UtcNow);
|
||||
await _broadcaster.TaskFinished(slot, task.Id, "done", finishedAt);
|
||||
}
|
||||
_logger.LogInformation("Task {TaskId} completed (turns={Turns}, tokens_in={In}, tokens_out={Out})",
|
||||
@@ -563,7 +563,7 @@ public sealed class TaskRunner
|
||||
}
|
||||
|
||||
private async Task MarkFailed(
|
||||
string taskId, string taskTitle, string slot, string? error, int turnCount = 0,
|
||||
string taskId, int taskNumber, string taskTitle, string slot, string? error, int turnCount = 0,
|
||||
int? maxTurns = null, string failureReason = "error")
|
||||
{
|
||||
// Terminal write for a failed task: never cancel (the status must always
|
||||
@@ -573,7 +573,7 @@ public sealed class TaskRunner
|
||||
var finishedAt = DateTime.UtcNow;
|
||||
await _state.FailAsync(taskId, finishedAt, error, CancellationToken.None,
|
||||
failureReason, turnCount > 0 ? turnCount : null, maxTurns);
|
||||
await _broadcaster.WorkerLog($"Finished \"{taskTitle}\" (failed)", WorkerLogLevel.Error, DateTime.UtcNow);
|
||||
await _broadcaster.WorkerLog($"Finished #{taskNumber} \"{taskTitle}\" (failed)", WorkerLogLevel.Error, DateTime.UtcNow);
|
||||
await _broadcaster.TaskFinished(slot, taskId, "failed", finishedAt);
|
||||
_logger.LogWarning("Task {TaskId} failed (turns={Turns}): {Error}", taskId, turnCount, error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user