Merge branch 'claudedo/c209d54cef5642278673d41e8b98fc43'

This commit is contained in:
mika kuns
2026-09-02 22:38:42 +02:00
6 changed files with 48 additions and 2 deletions
-1
View File
@@ -15,7 +15,6 @@ Checkliste lebt in `docs/verification-handoff.md`.
- **Planning-aktiver Parent zeigt weiter „Idle":** Parent in `planning_phase=active` hat `Status=Idle` (korrekt im Modell), aber `TaskRowViewModel.StatusLabel` (Zeile 132) kennt nur `HasInteractiveSession`/`IsParked` als Overrides — der `PlanningBadge` ersetzt den Chip nicht sichtbar → liest sich wie ein normaler Idle-Task. Wunsch: klarer „Planning/Draft aktiv"-Zustand, der Idle überschreibt. (Kinder zeigen korrekt „Draft".)
- **Blocked-by-Kette nicht sichtbar:** Nach Finalize ist die sequentielle Kette korrekt gesetzt (child[i] blocked-by child[i-1]), aber die UI stellt Reihenfolge/Abhängigkeit nicht dar — es gibt nur den „waiting"-Chip. Wunsch: Kette visualisieren (z.B. „wartet auf <Vorgänger>").
- **Dequeue-„X" fehlt auf wartenden (blockierten) Kettengliedern:** `CanRemoveFromQueue = IsQueued || HasQueuedSubtasks` (TaskRowViewModel.cs:99), `IsQueued` verlangt leeres `blocked_by`. Ein gequeuetes, aber blockiertes Kind (`IsWaiting`) bekommt daher kein Remove-from-queue-X — nur Parent + erstes (entsperrtes) Kind.
- **Modal-Bodies unten abgeschnitten — gefixt, Sichtprüfung offen** (Ursache 2026-09-02 verifiziert: `ScrollViewer.Padding` zählt in Avalonia 12 nicht zum Extent; Fix ist das Padding auf ein `Margin` des inneren Inhalts umzuziehen). Betraf `AboutModalView`, `MergeHelperSelectionModal`, `MergeModalView`, `RepoImportModalView`, `WorktreesOverviewModalView` — alle fünf umgestellt. `ListSettingsModalView`, `LogVisualizerView` und `WorkConsole` hatten zu diesem Zeitpunkt bereits kein ScrollViewer-Padding mehr.
- **Usage-Monitor-Modal: Analyse-Tabellen ausbaufähig** (Sichtprüfung 2026-08-06; Gauges, Info-Bänder, Presets/Custom-Range und Refresh-Button sind in Ordnung). Models-Tab: die Spaltenköpfe „OTHER CACHE" und „SHARE" kollidieren zu `OTHER CACHISHARE`; `claude-haiku-4-5-20251001` läuft in die IN-Spalte; alle Zahlen linksbündig und ohne Tausendertrenner (`506830708`). Tasks-Tab: Task-Titel wird ohne Ellipse hart an der LIST-Spalte abgeschnitten, der LIST-Text ebenfalls; MODEL ist bei Runs von vor der `task_runs.model`-Spalte leer (besser „—"). Wunsch: Zahlen rechtsbündig + `#,##0` (oder k/M), Spaltenbreiten/Truncation fixen.
- **Conflict-Resolver: mehrere Konfliktdateien schlecht erkennbar:** Beim 2-Datei-Konflikt schwer zu sehen, dass zwei Dateien betroffen sind (File-Switcher/Anzahl zu unauffällig). Prominentere Datei-Liste / „x von y Dateien".
@@ -112,6 +112,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
[NotifyPropertyChangedFor(nameof(IsQueued))]
[NotifyPropertyChangedFor(nameof(IsWaiting))]
[NotifyPropertyChangedFor(nameof(StatusChipClass))]
[NotifyPropertyChangedFor(nameof(CanRemoveFromQueue))]
private string? _blockedByTaskId;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(DependsOnTaskId))]
@@ -276,7 +277,7 @@ public sealed partial class TaskRowViewModel : ViewModelBase
&& !HasInteractiveSession;
public bool IsQueued => Status == TaskStatus.Queued && string.IsNullOrEmpty(BlockedByTaskId);
public bool IsWaiting => Status == TaskStatus.Queued && !string.IsNullOrEmpty(BlockedByTaskId);
public bool CanRemoveFromQueue => IsQueued || HasQueuedSubtasks;
public bool CanRemoveFromQueue => IsQueued || IsWaiting || HasQueuedSubtasks;
// "Send to queue" is the single queue entry. On a finalized planning parent it queues the
// plan (children) via CanQueuePlan; an Active (not-yet-finalized) planning parent is hidden —
// it must be finalized first. The worker never sees a UI-hosted ConPTY session (it never
@@ -430,6 +430,17 @@ public sealed class TaskStateService : ITaskStateService
if (affected == 0)
return new TransitionResult(false, "Task is not queued; cannot remove from queue.");
// A dequeued task may have been a planning-chain predecessor. Without unblocking its
// successor here, the successor is left permanently wedged on a BlockedByTaskId link to
// a task that has left the queue and will never reach the terminal state that would
// otherwise resolve it (PlanningChainCoordinator.OnChildFinishedAsync).
var successorId = await ctx.Tasks.AsNoTracking()
.Where(t => t.BlockedByTaskId == taskId)
.Select(t => t.Id)
.FirstOrDefaultAsync(ct);
if (successorId is not null)
await UnblockAsync(successorId, ct);
await NotifyAsync(taskId);
return new TransitionResult(true, null);
}
@@ -41,6 +41,23 @@ public class TaskRowContextMenuTests
Assert.NotNull(row.SendToQueueDisabledReason);
}
[Fact]
public void CanRemoveFromQueue_True_When_Queued_And_Blocked()
{
var row = MakeRow();
row.Status = TaskStatus.Queued;
row.BlockedByTaskId = "predecessor-id";
Assert.True(row.CanRemoveFromQueue);
}
[Fact]
public void CanRemoveFromQueue_False_When_Idle()
{
var row = MakeRow();
row.Status = TaskStatus.Idle;
Assert.False(row.CanRemoveFromQueue);
}
[Fact]
public void CancelDisabledReason_Null_While_Running()
{
@@ -585,6 +585,23 @@ public sealed class TaskStateServiceTests : IDisposable
Assert.Equal(TaskStatus.Running, await GetStatusAsync(id));
}
[Fact]
public async Task DequeueToIdleAsync_MidChain_UnblocksSuccessor()
{
// pred (queued, blocked) is removed from the queue in the middle of a chain;
// successor was blocked on pred and must not be left wedged forever.
var pred = await SeedTaskAsync(TaskStatus.Queued);
var id = await SeedTaskAsync(TaskStatus.Queued, blockedBy: pred);
var successor = await SeedTaskAsync(TaskStatus.Queued, blockedBy: id);
var result = await _sut.DequeueToIdleAsync(id, default);
Assert.True(result.Ok);
var successorTask = await GetTaskAsync(successor);
Assert.Equal(TaskStatus.Queued, successorTask.Status);
Assert.Null(successorTask.BlockedByTaskId);
}
// ─── StartPlanningAsync ───────────────────────────────────────────────
[Fact]
@@ -281,6 +281,7 @@ public class TaskRowViewModelNotificationsTests
nameof(TaskRowViewModel.IsQueued),
nameof(TaskRowViewModel.IsWaiting),
nameof(TaskRowViewModel.StatusChipClass),
nameof(TaskRowViewModel.CanRemoveFromQueue),
}, raised);
}