Merge branch 'main' into feat/poweruse-list-handler
Kollision mit #264 (Handoff-Budget im Brief sichtbar machen): beide Seiten haben HandoffRoundTracker, HandoffMcpTools und InteractiveLaunchSpecService angefasst. Der Konflikt war inhaltlich, nicht nur textuell. #264 liest den Cap an zwei Stellen aus der Konstante (GetStatus und das roundInfo-Rendering), waehrend dieser Branch ihn zum Parameter aus app_settings gemacht hat. Ein reiner Text-Merge haette kompiliert und trotzdem gelogen: Register haette den konfigurierten Cap durchgesetzt, waehrend der Brief dem Modell weiter "2 Runden uebrig" gemeldet haette, obwohl es 39 hat. Aufgeloest so, dass der Cap eine einzige Quelle hat: - GetStatus(taskId, maxHandoffs) nimmt den Cap als Parameter, mit demselben Clamp wie Register. Die Rechnerei liegt damit an einer Stelle. - GetHandoffStatus liest das Setting und gibt es weiter. - Das roundInfo-Rendering benutzt GetStatus statt eigener Arithmetik. Zwei Tests nageln genau diesen Drift fest, damit er beim naechsten Merge nicht unbemerkt zurueckkommt: einer auf GetHandoffStatus, einer auf den Brief. MaxHandoffs -> DefaultMaxHandoffs in #264s vier Test-Referenzen; die laufen alle mit Default-Settings, die Semantik bleibt.
This commit is contained in:
@@ -113,7 +113,14 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
[NotifyPropertyChangedFor(nameof(IsWaiting))]
|
||||
[NotifyPropertyChangedFor(nameof(StatusChipClass))]
|
||||
private string? _blockedByTaskId;
|
||||
[ObservableProperty] private string? _dependsOnTaskId;
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(DependsOnTaskId))]
|
||||
private IReadOnlyList<string> _dependsOnTaskIds = [];
|
||||
|
||||
// The single predecessor, or null when the task has none or several. The chain rail can only
|
||||
// draw a line, so a task waiting on n predecessors gets a ChainAfterLabel instead — see
|
||||
// TasksIslandViewModel.ApplyChainGrouping.
|
||||
public string? DependsOnTaskId => DependsOnTaskIds.Count == 1 ? DependsOnTaskIds[0] : null;
|
||||
[ObservableProperty] private bool _isExpanded = true;
|
||||
[ObservableProperty]
|
||||
[NotifyPropertyChangedFor(nameof(IsPlanningParent))]
|
||||
@@ -488,7 +495,10 @@ public sealed partial class TaskRowViewModel : ViewModelBase
|
||||
ParentTaskId = t.ParentTaskId;
|
||||
CreatedBy = t.CreatedBy;
|
||||
BlockedByTaskId = t.BlockedByTaskId;
|
||||
DependsOnTaskId = t.DependsOnTaskId;
|
||||
// Guarded: TaskEntity.DependsOnTaskIds projects a fresh List every read, so a plain
|
||||
// assignment would fire PropertyChanged on every reconcile tick even when nothing moved.
|
||||
var deps = t.DependsOnTaskIds;
|
||||
if (!DependsOnTaskIds.SequenceEqual(deps)) DependsOnTaskIds = deps;
|
||||
RoadblockCount = t.RoadblockCount;
|
||||
FailureReason = t.FailureReason;
|
||||
FailureTurnsUsed = t.FailureTurnsUsed;
|
||||
|
||||
@@ -313,6 +313,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
||||
var entity = await db.Tasks
|
||||
.Include(t => t.List)
|
||||
.Include(t => t.Worktree)
|
||||
.Include(t => t.Dependencies)
|
||||
.FirstOrDefaultAsync(t => t.Id == taskId);
|
||||
|
||||
// A newer refresh for this task started while we were reading — its result is fresher.
|
||||
@@ -453,7 +454,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
||||
{
|
||||
await using var db = await _dbFactory.CreateDbContextAsync(ct);
|
||||
var filter = _filters.Resolve(list.Id);
|
||||
var baseQuery = db.Tasks.Include(t => t.List).Include(t => t.Worktree);
|
||||
var baseQuery = db.Tasks.Include(t => t.List).Include(t => t.Worktree).Include(t => t.Dependencies);
|
||||
|
||||
var filteredList = filter is null
|
||||
? new List<TaskEntity>()
|
||||
@@ -722,12 +723,17 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
||||
}
|
||||
|
||||
// Content only — "after " + localization is Slice 2's concern (the view), which is why this
|
||||
// holds just the identifying fragment rather than an assembled sentence.
|
||||
// holds just the identifying fragment rather than an assembled sentence. Names every
|
||||
// predecessor it can resolve: a task can wait on n of them, and hiding all but the first
|
||||
// would misreport what the row is actually waiting for.
|
||||
private static string? BuildAfterLabel(TaskRowViewModel row, Dictionary<string, TaskRowViewModel> rowsById)
|
||||
{
|
||||
if (string.IsNullOrEmpty(row.DependsOnTaskId)) return null;
|
||||
if (!rowsById.TryGetValue(row.DependsOnTaskId, out var predecessor)) return null;
|
||||
return predecessor.Number > 0 ? $"#{predecessor.Number}" : predecessor.Title;
|
||||
var parts = row.DependsOnTaskIds
|
||||
.Select(id => rowsById.TryGetValue(id, out var p) ? p : null)
|
||||
.Where(p => p is not null)
|
||||
.Select(p => p!.Number > 0 ? $"#{p.Number}" : p.Title)
|
||||
.ToList();
|
||||
return parts.Count == 0 ? null : string.Join(", ", parts);
|
||||
}
|
||||
|
||||
// Assigns ShowAsChainMember/ChainStep/ChainAfterLabel for every row in `section`, then
|
||||
@@ -743,7 +749,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
||||
|
||||
foreach (var r in section)
|
||||
{
|
||||
if (string.IsNullOrEmpty(r.DependsOnTaskId))
|
||||
if (r.DependsOnTaskIds.Count == 0)
|
||||
{
|
||||
r.ShowAsChainMember = false;
|
||||
r.ChainStep = null;
|
||||
@@ -751,6 +757,16 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
||||
continue;
|
||||
}
|
||||
|
||||
// Several predecessors aren't a chain — the rail draws one line, so a fan-in row
|
||||
// only ever gets the label listing everything it waits on.
|
||||
if (r.DependsOnTaskId is null)
|
||||
{
|
||||
r.ShowAsChainMember = false;
|
||||
r.ChainStep = null;
|
||||
r.ChainAfterLabel = BuildAfterLabel(r, rowsById);
|
||||
continue;
|
||||
}
|
||||
|
||||
// A planning child keeps its parent indent — chain membership only ever shows as a
|
||||
// label for it, never as a second, nested rail (design: "parent wins").
|
||||
if (r.IsChild)
|
||||
@@ -1684,6 +1700,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
||||
entities = await db.Tasks
|
||||
.Include(t => t.List)
|
||||
.Include(t => t.Worktree)
|
||||
.Include(t => t.Dependencies)
|
||||
.Where(t => idSet.Contains(t.Id))
|
||||
.ToListAsync(ct);
|
||||
dbOk = true;
|
||||
@@ -1728,6 +1745,7 @@ public sealed partial class TasksIslandViewModel : ViewModelBase, IDisposable
|
||||
// The slice of a row that Regroup reads: which section it lands in, where it sits inside it,
|
||||
// and what its chain rail/after-chip says. Only fields UpdateFromEntity actually writes —
|
||||
// IsExpanded and HasPlanningChildren are owned elsewhere and would produce false positives.
|
||||
private static (bool, DateTime?, string?, string?, PlanningPhase, int, string) GroupingKey(TaskRowViewModel r) =>
|
||||
(r.Done, r.ScheduledFor, r.ParentTaskId, r.DependsOnTaskId, r.PlanningPhase, r.Number, r.Title);
|
||||
private static (bool, DateTime?, string?, string, PlanningPhase, int, string) GroupingKey(TaskRowViewModel r) =>
|
||||
(r.Done, r.ScheduledFor, r.ParentTaskId, string.Join(",", r.DependsOnTaskIds),
|
||||
r.PlanningPhase, r.Number, r.Title);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user