fix(ui): failureReason "usage_limit" nachziehen + Worktree-Gate prüft die Platte
Zwei liegengebliebene Consumer aus den letzten beiden Commits:
- TaskRunner klassifiziert seit 07dd7570 "usage_limit", aber weder
TaskRowViewModel.FailureReasonLabel noch vm.failureReason (de/en) noch
die get_task-Tool-Beschreibung kannten den Wert — die UI zeigte
"Grund unbekannt", das MCP-Doc listete weiterhin max_turns|timeout|error.
- TaskRowViewModel.CanOpenWorktree prüfte nur auf einen nicht-leeren
String. Die Zeile behält den Path eines gemergten/verworfenen Worktrees,
also war der Menüpunkt aktiv und Process.Start warf in den Footer.
Jetzt zusätzlich Directory.Exists — dieselbe Prüfung, die
WorktreesOverviewModalViewModel und MergeSectionViewModel schon machen.
This commit is contained in:
@@ -719,7 +719,7 @@
|
||||
"mainWindow": { "closeConfirm": { "message": "{0} Mission-Control-Sitzung(en) laufen noch. Beim Beenden werden sie abgebrochen. Trotzdem schließen?" } },
|
||||
"agentStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "review": "Prüfung", "children": "Wartet auf Teilaufgaben", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen" },
|
||||
"taskStatus": { "idle": "Leerlauf", "queued": "In Warteschlange", "running": "Läuft", "waitingForReview": "Wartet auf Prüfung", "waitingForChildren": "Wartet auf Teilaufgaben", "done": "Fertig", "failed": "Fehlgeschlagen", "cancelled": "Abgebrochen", "parked": "Geparkt", "interactive": "Interaktiv" },
|
||||
"failureReason": { "maxTurns": "Turn-Limit erreicht", "timeout": "Zeitüberschreitung", "error": "Fehler", "cancelled": "Abgebrochen", "unknown": "Grund unbekannt" },
|
||||
"failureReason": { "maxTurns": "Turn-Limit erreicht", "timeout": "Zeitüberschreitung", "usageLimit": "Nutzungslimit erreicht", "error": "Fehler", "cancelled": "Abgebrochen", "unknown": "Grund unbekannt" },
|
||||
"failureReasonTooltip": { "maxTurns": "Turn-Limit erreicht ({0}/{1} Turns) — der Worktree ist meist brauchbar; Task fortsetzen statt zurücksetzen." },
|
||||
"planningBadge": { "active": "PLANUNG", "finalized": "GEPLANT" },
|
||||
"taskRow": { "createdPrefix": "Erstellt {0}", "stepsText": "{0}/{1} Schritte" },
|
||||
|
||||
@@ -719,7 +719,7 @@
|
||||
"mainWindow": { "closeConfirm": { "message": "{0} Mission Control session(s) are still running. Closing the app will end them. Close anyway?" } },
|
||||
"agentStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "review": "Review", "children": "Waiting for Subtasks", "done": "Done", "failed": "Failed", "cancelled": "Cancelled" },
|
||||
"taskStatus": { "idle": "Idle", "queued": "Queued", "running": "Running", "waitingForReview": "Waiting for Review", "waitingForChildren": "Waiting for Subtasks", "done": "Done", "failed": "Failed", "cancelled": "Cancelled", "parked": "Parked", "interactive": "Interactive" },
|
||||
"failureReason": { "maxTurns": "Turn limit reached", "timeout": "Timed out", "error": "Error", "cancelled": "Cancelled", "unknown": "Unknown reason" },
|
||||
"failureReason": { "maxTurns": "Turn limit reached", "timeout": "Timed out", "usageLimit": "Usage limit reached", "error": "Error", "cancelled": "Cancelled", "unknown": "Unknown reason" },
|
||||
"failureReasonTooltip": { "maxTurns": "Turn limit reached ({0}/{1} turns) — the worktree is usually fine; continue the task instead of resetting it." },
|
||||
"planningBadge": { "active": "PLANNING", "finalized": "PLANNED" },
|
||||
"taskRow": { "createdPrefix": "Created {0}", "stepsText": "{0}/{1} steps" },
|
||||
|
||||
@@ -180,7 +180,11 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
// placeholder so the menu wiring is uniform; always null until a real gate exists.
|
||||
public string? QuickSessionDisabledReason => null;
|
||||
|
||||
public bool CanOpenWorktree => !string.IsNullOrWhiteSpace(WorktreePath);
|
||||
// The row keeps a merged/discarded worktree's recorded Path, so a non-empty string is not
|
||||
// proof the folder is still there — same on-disk check the worktrees overview and the merge
|
||||
// section do before offering "open". Only read when the context menu is built, not per row.
|
||||
public bool CanOpenWorktree =>
|
||||
!string.IsNullOrWhiteSpace(WorktreePath) && System.IO.Directory.Exists(WorktreePath);
|
||||
public string? OpenWorktreeDisabledReason => CanOpenWorktree ? null : Loc.T("tasks.reasonNoWorktree");
|
||||
|
||||
public string? RefineDisabledReason
|
||||
@@ -235,6 +239,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
{
|
||||
"max_turns" => Loc.T("vm.failureReason.maxTurns"),
|
||||
"timeout" => Loc.T("vm.failureReason.timeout"),
|
||||
"usage_limit" => Loc.T("vm.failureReason.usageLimit"),
|
||||
"cancelled" => Loc.T("vm.failureReason.cancelled"),
|
||||
"error" => Loc.T("vm.failureReason.error"),
|
||||
_ => Loc.T("vm.failureReason.unknown"),
|
||||
|
||||
+1
-1
@@ -333,7 +333,7 @@ public sealed class ExternalMcpService
|
||||
"Done/Failed/Cancelled tasks can be reset to Idle for re-execution. A Queued task with a blocker waits " +
|
||||
"for its predecessor before the picker will claim it, and WaitingForChildren is a parent whose own work " +
|
||||
"is done but whose children are still running. For Status=Failed, failureReason (max_turns|timeout|" +
|
||||
"error|cancelled|unknown) plus failureTurnsUsed/failureMaxTurns say why without pulling get_task_log." +
|
||||
"usage_limit|error|cancelled|unknown) plus failureTurnsUsed/failureMaxTurns say why without pulling get_task_log." +
|
||||
McpToolDocs.TaskNumberHint)]
|
||||
public async Task<TaskDto> GetTask(string taskId, CancellationToken cancellationToken)
|
||||
{
|
||||
|
||||
@@ -65,14 +65,20 @@ public class TaskRowViewModelTests
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void CanOpenWorktree_True_And_ReasonNull_When_Path_Present()
|
||||
public void CanOpenWorktree_True_And_ReasonNull_When_Folder_Exists()
|
||||
{
|
||||
var dir = Path.Combine(Path.GetTempPath(), $"cd_wt_{Guid.NewGuid():N}");
|
||||
Directory.CreateDirectory(dir);
|
||||
try
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
vm.WorktreePath = @"C:\repo\worktrees\t";
|
||||
vm.WorktreePath = dir;
|
||||
|
||||
Assert.True(vm.CanOpenWorktree);
|
||||
Assert.Null(vm.OpenWorktreeDisabledReason);
|
||||
}
|
||||
finally { Directory.Delete(dir, true); }
|
||||
}
|
||||
|
||||
[Theory]
|
||||
[InlineData(null)]
|
||||
@@ -87,6 +93,18 @@ public class TaskRowViewModelTests
|
||||
Assert.NotNull(vm.OpenWorktreeDisabledReason);
|
||||
}
|
||||
|
||||
// A merged/discarded worktree leaves its recorded Path on the row long after the folder is
|
||||
// gone — the menu entry has to gray out, not throw inside Process.Start.
|
||||
[Fact]
|
||||
public void CanOpenWorktree_False_When_Recorded_Folder_Is_Gone()
|
||||
{
|
||||
var vm = new TaskRowViewModel { Id = "t" };
|
||||
vm.WorktreePath = Path.Combine(Path.GetTempPath(), $"cd_wt_gone_{Guid.NewGuid():N}");
|
||||
|
||||
Assert.False(vm.CanOpenWorktree);
|
||||
Assert.NotNull(vm.OpenWorktreeDisabledReason);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void OpenWorktreeGate_Raises_PropertyChanged_On_WorktreePath_Change()
|
||||
{
|
||||
@@ -115,7 +133,6 @@ public class TaskRowViewModelTests
|
||||
var vm = TaskRowViewModel.FromEntity(entity);
|
||||
|
||||
Assert.Equal(@"C:\repo\worktrees\t", vm.WorktreePath);
|
||||
Assert.True(vm.CanOpenWorktree);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
||||
Reference in New Issue
Block a user