feat(ui): richer diff viewer + surface child roadblocks on parents
All checks were successful
Changelog / changelog (push) Successful in 1s
Release / release (push) Successful in 38s

- UnifiedDiffParser detects added/deleted/renamed/binary files; diff
  modal shows a file list, binary/empty placeholders, and can diff a
  merged task by commit range after its worktree is gone
- DetailsIslandViewModel flags children needing attention (failed,
  cancelled, awaiting review, or with roadblocks) on the parent
- GitService gains worktree head-commit/range support; planning chain,
  merge orchestration, and session manager tweaks with updated tests
- refresh app/installer/worker icons

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-09 16:40:59 +02:00
parent c300f8c313
commit f21c65be18
28 changed files with 509 additions and 119 deletions

View File

@@ -75,7 +75,7 @@ public sealed class PlanningChainCoordinatorTests : IDisposable
{
await SeedPlanningFamilyAsync("P", 3);
var count = await _sut.SetupChainAsync("P", default);
var count = await _sut.SetupChainAsync("P", enqueue: true, default);
Assert.Equal(3, count);
var kids = await GetChildrenAsync("P");
@@ -92,7 +92,7 @@ public sealed class PlanningChainCoordinatorTests : IDisposable
{
await SeedPlanningFamilyAsync("P", 2, childStatus: TaskStatus.Idle);
var count = await _sut.SetupChainAsync("P", default);
var count = await _sut.SetupChainAsync("P", enqueue: true, default);
Assert.Equal(2, count);
}
@@ -101,7 +101,7 @@ public sealed class PlanningChainCoordinatorTests : IDisposable
public async Task OnChildDone_UnblocksTheSuccessor()
{
await SeedPlanningFamilyAsync("P", 3);
await _sut.SetupChainAsync("P", default);
await _sut.SetupChainAsync("P", enqueue: true, default);
// Mark the head child Done before announcing.
await using (var ctx = _factory.CreateDbContext())
@@ -128,7 +128,7 @@ public sealed class PlanningChainCoordinatorTests : IDisposable
public async Task OnChildFailed_CancelsPendingSuccessors_ChainIsNotWedged()
{
await SeedPlanningFamilyAsync("P", 3);
await _sut.SetupChainAsync("P", default);
await _sut.SetupChainAsync("P", enqueue: true, default);
await using (var ctx = _factory.CreateDbContext())
{
@@ -153,7 +153,7 @@ public sealed class PlanningChainCoordinatorTests : IDisposable
{
// Chain: c0 → c1 → c2 → c3. c1 fails mid-chain; c2 and c3 must be cancelled.
await SeedPlanningFamilyAsync("P", 4);
await _sut.SetupChainAsync("P", default);
await _sut.SetupChainAsync("P", enqueue: true, default);
// Mark c0 Done so c1 was unblocked; c1 ran and failed.
await using (var ctx = _factory.CreateDbContext())
@@ -182,7 +182,7 @@ public sealed class PlanningChainCoordinatorTests : IDisposable
public async Task OnChildDone_LastChild_ReturnsNull()
{
await SeedPlanningFamilyAsync("P", 2);
await _sut.SetupChainAsync("P", default);
await _sut.SetupChainAsync("P", enqueue: true, default);
await using (var ctx = _factory.CreateDbContext())
{
@@ -208,7 +208,7 @@ public sealed class PlanningChainCoordinatorTests : IDisposable
}
await Assert.ThrowsAsync<InvalidOperationException>(
() => _sut.SetupChainAsync("P", default));
() => _sut.SetupChainAsync("P", enqueue: true, default));
}
[Fact]
@@ -230,7 +230,7 @@ public sealed class PlanningChainCoordinatorTests : IDisposable
await ctx.SaveChangesAsync();
}
var count = await _sut.SetupChainAsync("P", default);
var count = await _sut.SetupChainAsync("P", enqueue: true, default);
Assert.Equal(4, count);
var kids = await GetChildrenAsync("P");
@@ -257,7 +257,7 @@ public sealed class PlanningChainCoordinatorTests : IDisposable
await ctx.SaveChangesAsync();
}
var count = await _sut.SetupChainAsync("P", default);
var count = await _sut.SetupChainAsync("P", enqueue: true, default);
// Only the two non-terminal tail children get chained.
Assert.Equal(2, count);
@@ -303,4 +303,21 @@ public sealed class PlanningChainCoordinatorTests : IDisposable
var kids = await GetChildrenAsync("P");
Assert.All(kids, k => Assert.Equal(TaskStatus.Idle, k.Status));
}
[Fact]
public async Task SetupChain_LinkOnly_LeavesChildrenIdle_ButEstablishesChain()
{
// Finalize path: children must stay Idle (nothing auto-queues) but still
// get the blocked-by chain so a later "Queue plan" runs them in order.
await SeedPlanningFamilyAsync("P", 3);
var count = await _sut.SetupChainAsync("P", enqueue: false, default);
Assert.Equal(3, count);
var kids = await GetChildrenAsync("P");
Assert.All(kids, k => Assert.Equal(TaskStatus.Idle, k.Status));
Assert.Null(kids[0].BlockedByTaskId);
Assert.Equal(kids[0].Id, kids[1].BlockedByTaskId);
Assert.Equal(kids[1].Id, kids[2].BlockedByTaskId);
}
}