diff --git a/src/ClaudeDo.Data/Repositories/TaskRepository.cs b/src/ClaudeDo.Data/Repositories/TaskRepository.cs index bbc4748..a6d9d68 100644 --- a/src/ClaudeDo.Data/Repositories/TaskRepository.cs +++ b/src/ClaudeDo.Data/Repositories/TaskRepository.cs @@ -27,6 +27,9 @@ public sealed class TaskRepository public async Task UpdateAsync(TaskEntity entity, CancellationToken ct = default) { + var tracked = _context.Tasks.Local.FirstOrDefault(t => t.Id == entity.Id); + if (tracked is not null && !ReferenceEquals(tracked, entity)) + _context.Entry(tracked).State = Microsoft.EntityFrameworkCore.EntityState.Detached; _context.Tasks.Update(entity); await _context.SaveChangesAsync(ct); } diff --git a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs index 96e924f..15830b7 100644 --- a/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Islands/TasksIslandViewModel.cs @@ -244,7 +244,9 @@ public sealed partial class TasksIslandViewModel : ViewModelBase .Where(r => r.IsChild && !string.IsNullOrEmpty(r.ParentTaskId)) .GroupBy(r => r.ParentTaskId!) .ToDictionary(g => g.Key, g => g.ToList()); - foreach (var parent in Items.Where(r => r.IsPlanningParent && !r.IsChild)) + foreach (var parent in Items.Where(r => r.IsPlanningParent && !r.IsChild + && r.PlanningPhase == PlanningPhase.Finalized + && !r.Done)) { if (_expandedState.ContainsKey(parent.Id)) continue; if (childrenByParent.TryGetValue(parent.Id, out var kids) diff --git a/tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs b/tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs index fdcb601..9718d9a 100644 --- a/tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs +++ b/tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs @@ -6,38 +6,38 @@ public class StreamLineFormatterTests { private readonly StreamLineFormatter _formatter = new(); - // --- Text deltas --- + // --- Assistant text blocks --- [Fact] - public void FormatLine_TextDelta_ReturnsTextContent() + public void FormatLine_AssistantTextBlock_ReturnsTextContent() { - var line = """{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello world"}}}"""; - Assert.Equal("Hello world", _formatter.FormatLine(line)); + var line = """{"type":"assistant","message":{"content":[{"type":"text","text":"Hello world"}]}}"""; + Assert.Equal("Hello world\n", _formatter.FormatLine(line)); } [Fact] - public void FormatLine_ConsecutiveTextDeltas_ReturnEachDelta() + public void FormatLine_AssistantConsecutiveTextBlocks_ReturnEachAppended() { - var line1 = """{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello "}}}"""; - var line2 = """{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"world"}}}"""; - Assert.Equal("Hello ", _formatter.FormatLine(line1)); - Assert.Equal("world", _formatter.FormatLine(line2)); + var line1 = """{"type":"assistant","message":{"content":[{"type":"text","text":"Hello "}]}}"""; + var line2 = """{"type":"assistant","message":{"content":[{"type":"text","text":"world"}]}}"""; + Assert.Equal("Hello \n", _formatter.FormatLine(line1)); + Assert.Equal("world\n", _formatter.FormatLine(line2)); } [Fact] - public void FormatLine_ContentBlockStop_ReturnsNewline() + public void FormatLine_AssistantThinkingBlock_IsFiltered() { - var line = """{"type":"stream_event","event":{"type":"content_block_stop","index":0}}"""; - Assert.Equal("\n", _formatter.FormatLine(line)); + var line = """{"type":"assistant","message":{"content":[{"type":"thinking","text":"hidden"}]}}"""; + Assert.Null(_formatter.FormatLine(line)); } // --- Tool use, result, system, fallback --- [Fact] - public void FormatLine_ToolUseStart_ReturnsToolNameLine() + public void FormatLine_AssistantToolUseBlock_ReturnsToolNameLine() { - var line = """{"type":"stream_event","event":{"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"x","name":"bash"}}}"""; - Assert.Equal("\n[Tool: bash]\n", _formatter.FormatLine(line)); + var line = """{"type":"assistant","message":{"content":[{"type":"tool_use","id":"x","name":"Bash","input":{"command":"ls"}}]}}"""; + Assert.Equal("[Bash] $ ls\n", _formatter.FormatLine(line)); } [Fact] @@ -58,13 +58,13 @@ public class StreamLineFormatterTests public void FormatLine_ApiRetry_ReturnsRetryNotice() { var line = """{"type":"system","subtype":"api_retry"}"""; - Assert.Equal("\n[Retrying API call...]\n", _formatter.FormatLine(line)); + Assert.Equal("[Retrying API call...]\n", _formatter.FormatLine(line)); } [Fact] - public void FormatLine_SystemNonRetry_ReturnsNull() + public void FormatLine_SystemUnknownSubtype_ReturnsNull() { - var line = """{"type":"system","subtype":"init"}"""; + var line = """{"type":"system","subtype":"some_unknown_subtype"}"""; Assert.Null(_formatter.FormatLine(line)); } @@ -98,8 +98,8 @@ public class StreamLineFormatterTests { var lines = new[] { - """{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello"}}}""", - """{"type":"stream_event","event":{"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"x","name":"bash"}}}""", + """{"type":"assistant","message":{"content":[{"type":"text","text":"Hello"}]}}""", + """{"type":"assistant","message":{"content":[{"type":"tool_use","id":"x","name":"Bash","input":{"command":"ls"}}]}}""", """{"type":"result","result":"Done."}""", }; var file = Path.GetTempFileName(); @@ -108,7 +108,7 @@ public class StreamLineFormatterTests File.WriteAllLines(file, lines); var result = _formatter.FormatFile(file); Assert.Contains("Hello", result); - Assert.Contains("[Tool: bash]", result); + Assert.Contains("[Bash]", result); Assert.Contains("Done.", result); } finally @@ -121,7 +121,7 @@ public class StreamLineFormatterTests public void FormatFile_TrimsLargeContent() { var chunk = new string('x', 1000); - var line = "{\"type\":\"stream_event\",\"event\":{\"type\":\"content_block_delta\",\"index\":0,\"delta\":{\"type\":\"text_delta\",\"text\":\"" + chunk + "\"}}}"; + var line = "{\"type\":\"assistant\",\"message\":{\"content\":[{\"type\":\"text\",\"text\":\"" + chunk + "\"}]}}"; var lines = Enumerable.Repeat(line, 65).ToArray(); var file = Path.GetTempFileName(); try diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/ConflictResolutionViewModelTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/ConflictResolutionViewModelTests.cs index 5e8e355..6119951 100644 --- a/tests/ClaudeDo.Ui.Tests/ViewModels/ConflictResolutionViewModelTests.cs +++ b/tests/ClaudeDo.Ui.Tests/ViewModels/ConflictResolutionViewModelTests.cs @@ -48,6 +48,8 @@ public class ConflictResolutionViewModelTests public Task BuildPlanningIntegrationBranchAsync(string planningTaskId, string targetBranch) => Task.FromResult(null); public Task MergeAllPlanningAsync(string planningTaskId, string targetBranch) => Task.CompletedTask; + public Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask; + public Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default) => Task.CompletedTask; public Task ContinuePlanningMergeAsync(string planningTaskId) { diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPlanningTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPlanningTests.cs index 11a8e8a..13532fb 100644 --- a/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPlanningTests.cs +++ b/tests/ClaudeDo.Ui.Tests/ViewModels/DetailsIslandPlanningTests.cs @@ -80,6 +80,8 @@ public class DetailsIslandPlanningTests : IDisposable public Task MergeAllPlanningAsync(string planningTaskId, string targetBranch) => Task.CompletedTask; public Task ContinuePlanningMergeAsync(string planningTaskId) => Task.CompletedTask; public Task AbortPlanningMergeAsync(string planningTaskId) => Task.CompletedTask; + public Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask; + public Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default) => Task.CompletedTask; } private sealed class NullServiceProvider : IServiceProvider diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/PlanningDiffViewModelTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/PlanningDiffViewModelTests.cs index 7d693df..8155328 100644 --- a/tests/ClaudeDo.Ui.Tests/ViewModels/PlanningDiffViewModelTests.cs +++ b/tests/ClaudeDo.Ui.Tests/ViewModels/PlanningDiffViewModelTests.cs @@ -47,6 +47,8 @@ public class PlanningDiffViewModelTests public Task MergeAllPlanningAsync(string planningTaskId, string targetBranch) => Task.CompletedTask; public Task ContinuePlanningMergeAsync(string planningTaskId) => Task.CompletedTask; public Task AbortPlanningMergeAsync(string planningTaskId) => Task.CompletedTask; + public Task OpenInteractiveTerminalAsync(string taskId, CancellationToken ct = default) => Task.CompletedTask; + public Task QueuePlanningSubtasksAsync(string parentTaskId, CancellationToken ct = default) => Task.CompletedTask; } [Fact] diff --git a/tests/ClaudeDo.Worker.Tests/Repositories/AppSettingsRepositoryTests.cs b/tests/ClaudeDo.Worker.Tests/Repositories/AppSettingsRepositoryTests.cs index ddb68e5..8880526 100644 --- a/tests/ClaudeDo.Worker.Tests/Repositories/AppSettingsRepositoryTests.cs +++ b/tests/ClaudeDo.Worker.Tests/Repositories/AppSettingsRepositoryTests.cs @@ -21,7 +21,7 @@ public class AppSettingsRepositoryTests : IDisposable Assert.Equal(AppSettingsEntity.SingletonId, row.Id); Assert.Equal("sonnet", row.DefaultModel); Assert.Equal(100, row.DefaultMaxTurns); - Assert.Equal("bypassPermissions", row.DefaultPermissionMode); + Assert.Equal("auto", row.DefaultPermissionMode); Assert.Equal("sibling", row.WorktreeStrategy); Assert.Null(row.CentralWorktreeRoot); Assert.False(row.WorktreeAutoCleanupEnabled);