diff --git a/docs/superpowers/plans/2026-04-14-ui-fixes.md b/docs/superpowers/plans/2026-04-14-ui-fixes.md new file mode 100644 index 0000000..72179b7 --- /dev/null +++ b/docs/superpowers/plans/2026-04-14-ui-fixes.md @@ -0,0 +1,1550 @@ +# UI Fixes Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Fix four post-integration issues: raw NDJSON display, missing start feedback, lost live output, and missing config editors + modal theming. + +**Architecture:** A new `StreamLineFormatter` in the UI layer parses NDJSON for display. `TaskDetailViewModel` switches from `ObservableCollection` to a single `string` property. Optimistic UI feedback via a local `RunNowRequestedEvent`. Existing editor dialogs get config sections and proper theming. + +**Tech Stack:** .NET 8, Avalonia 12 (Fluent dark theme), CommunityToolkit.Mvvm, System.Text.Json, xUnit + +--- + +## File Structure + +### New Files +- `src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs` — NDJSON-to-text parser for UI display +- `tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj` — test project for UI helpers +- `tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs` — formatter unit tests + +### Modified Files +- `src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs` — LiveText, formatter, start feedback, log reload +- `src/ClaudeDo.Ui/Views/TaskDetailView.axaml` — TextBox replaces ItemsControl, auto-scroll +- `src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs` — auto-scroll handler +- `src/ClaudeDo.Ui/Services/WorkerClient.cs` — RunNowRequestedEvent, GetAgentsAsync, AgentInfo DTO +- `src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs` — IsStarting property +- `src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs` — wire RunNowRequestedEvent +- `src/ClaudeDo.Ui/Views/TaskListView.axaml` — starting state visual +- `src/ClaudeDo.Ui/ViewModels/ListEditorViewModel.cs` — config fields, agent loading +- `src/ClaudeDo.Ui/Views/ListEditorView.axaml` — config section, theming +- `src/ClaudeDo.Ui/ViewModels/TaskEditorViewModel.cs` — config override fields +- `src/ClaudeDo.Ui/Views/TaskEditorView.axaml` — config section, theming +- `src/ClaudeDo.Worker/Runner/TaskRunner.cs` — default model fallback +- `ClaudeDo.slnx` — add new test project + +--- + +### Task 1: Create UI test project + +**Files:** +- Create: `tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj` + +- [ ] **Step 1: Create the test project** + +```xml + + + + net8.0 + enable + false + + + + + + + + + + +``` + +- [ ] **Step 2: Add to solution** + +Run: `dotnet sln ClaudeDo.slnx add tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj` +Expected: Project added successfully. + +- [ ] **Step 3: Verify build** + +Run: `dotnet build tests/ClaudeDo.Ui.Tests` +Expected: Build succeeded. + +- [ ] **Step 4: Commit** + +```bash +git add tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj ClaudeDo.slnx +git commit -m "chore(tests): add ClaudeDo.Ui.Tests project" +``` + +--- + +### Task 2: StreamLineFormatter — text deltas (TDD) + +**Files:** +- Create: `src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs` +- Create: `tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs` + +- [ ] **Step 1: Write failing tests for text delta extraction** + +```csharp +// tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs +using ClaudeDo.Ui.Helpers; + +namespace ClaudeDo.Ui.Tests.Helpers; + +public class StreamLineFormatterTests +{ + private readonly StreamLineFormatter _sut = new(); + + [Fact] + public void FormatLine_TextDelta_ReturnsTextContent() + { + var line = """{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hello world"}}}"""; + var result = _sut.FormatLine(line); + Assert.Equal("Hello world", result); + } + + [Fact] + public void FormatLine_ConsecutiveTextDeltas_ReturnEachDelta() + { + 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 ", _sut.FormatLine(line1)); + Assert.Equal("world", _sut.FormatLine(line2)); + } + + [Fact] + public void FormatLine_ContentBlockStop_ReturnsNewline() + { + var delta = """{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"Hi"}}}"""; + var stop = """{"type":"stream_event","event":{"type":"content_block_stop","index":0}}"""; + _sut.FormatLine(delta); + Assert.Equal("\n", _sut.FormatLine(stop)); + } +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `dotnet test tests/ClaudeDo.Ui.Tests --filter "StreamLineFormatterTests" -v quiet` +Expected: Build error — `StreamLineFormatter` does not exist. + +- [ ] **Step 3: Write minimal StreamLineFormatter** + +```csharp +// src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs +using System.Text.Json; + +namespace ClaudeDo.Ui.Helpers; + +public sealed class StreamLineFormatter +{ + public string? FormatLine(string ndjsonLine) + { + if (string.IsNullOrWhiteSpace(ndjsonLine)) return null; + + try + { + using var doc = JsonDocument.Parse(ndjsonLine); + var root = doc.RootElement; + + if (!root.TryGetProperty("type", out var typeProp)) return null; + var type = typeProp.GetString(); + + return type switch + { + "stream_event" => HandleStreamEvent(root), + _ => null, + }; + } + catch (JsonException) + { + return ndjsonLine; // Fallback: show raw line + } + } + + private static string? HandleStreamEvent(JsonElement root) + { + if (!root.TryGetProperty("event", out var evt)) return null; + if (!evt.TryGetProperty("type", out var evtTypeProp)) return null; + var evtType = evtTypeProp.GetString(); + + return evtType switch + { + "content_block_delta" => HandleDelta(evt), + "content_block_stop" => "\n", + _ => null, + }; + } + + private static string? HandleDelta(JsonElement evt) + { + if (!evt.TryGetProperty("delta", out var delta)) return null; + if (!delta.TryGetProperty("type", out var deltaType)) return null; + + return deltaType.GetString() switch + { + "text_delta" => delta.TryGetProperty("text", out var text) ? text.GetString() : null, + _ => null, // input_json_delta etc. — skip + }; + } +} +``` + +- [ ] **Step 4: Run tests to verify they pass** + +Run: `dotnet test tests/ClaudeDo.Ui.Tests --filter "StreamLineFormatterTests" -v quiet` +Expected: 3 passed. + +- [ ] **Step 5: Commit** + +```bash +git add src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs +git commit -m "feat(ui): add StreamLineFormatter with text delta parsing (TDD)" +``` + +--- + +### Task 3: StreamLineFormatter — tool use, result, system, fallback (TDD) + +**Files:** +- Modify: `src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs` +- Modify: `tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs` + +- [ ] **Step 1: Write failing tests for remaining event types** + +Append to `StreamLineFormatterTests.cs`: + +```csharp +[Fact] +public void FormatLine_ToolUseStart_ReturnsToolNameLine() +{ + var line = """{"type":"stream_event","event":{"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"toolu_xxx","name":"Read","input":{}}}}"""; + var result = _sut.FormatLine(line); + Assert.Equal("\n[Tool: Read]\n", result); +} + +[Fact] +public void FormatLine_InputJsonDelta_ReturnsNull() +{ + var line = """{"type":"stream_event","event":{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"file\":"}}}"""; + Assert.Null(_sut.FormatLine(line)); +} + +[Fact] +public void FormatLine_Result_ReturnsFormattedResult() +{ + var line = """{"type":"result","result":"Task completed successfully.","session_id":"sess_123"}"""; + var result = _sut.FormatLine(line); + Assert.Equal("\n--- Result ---\nTask completed successfully.\n", result); +} + +[Fact] +public void FormatLine_ApiRetry_ReturnsRetryNotice() +{ + var line = """{"type":"system","subtype":"api_retry","message":"Retrying..."}"""; + var result = _sut.FormatLine(line); + Assert.Equal("\n[Retrying API call...]\n", result); +} + +[Fact] +public void FormatLine_SystemNonRetry_ReturnsNull() +{ + var line = """{"type":"system","subtype":"init"}"""; + Assert.Null(_sut.FormatLine(line)); +} + +[Fact] +public void FormatLine_AssistantType_ReturnsNull() +{ + var line = """{"type":"assistant","message":{"role":"assistant","content":[]}}"""; + Assert.Null(_sut.FormatLine(line)); +} + +[Fact] +public void FormatLine_MalformedJson_ReturnsRawLine() +{ + var line = "this is not json"; + Assert.Equal("this is not json", _sut.FormatLine(line)); +} + +[Fact] +public void FormatLine_MessageStartAndDelta_ReturnsNull() +{ + var start = """{"type":"stream_event","event":{"type":"message_start","message":{"id":"msg_xxx"}}}"""; + var delta = """{"type":"stream_event","event":{"type":"message_delta","delta":{"stop_reason":"end_turn"},"usage":{"output_tokens":50}}}"""; + Assert.Null(_sut.FormatLine(start)); + Assert.Null(_sut.FormatLine(delta)); +} +``` + +- [ ] **Step 2: Run tests to verify new tests fail** + +Run: `dotnet test tests/ClaudeDo.Ui.Tests --filter "StreamLineFormatterTests" -v quiet` +Expected: Several failures — `FormatLine_ToolUseStart`, `FormatLine_Result`, `FormatLine_ApiRetry` return null instead of expected strings. + +- [ ] **Step 3: Extend StreamLineFormatter to handle all event types** + +Update the `FormatLine` method's switch expression in `StreamLineFormatter.cs`: + +```csharp +return type switch +{ + "stream_event" => HandleStreamEvent(root), + "result" => HandleResult(root), + "system" => HandleSystem(root), + "assistant" => null, + _ => null, +}; +``` + +Add `HandleStreamEvent` case for `content_block_start`: + +```csharp +private static string? HandleStreamEvent(JsonElement root) +{ + if (!root.TryGetProperty("event", out var evt)) return null; + if (!evt.TryGetProperty("type", out var evtTypeProp)) return null; + var evtType = evtTypeProp.GetString(); + + return evtType switch + { + "content_block_start" => HandleBlockStart(evt), + "content_block_delta" => HandleDelta(evt), + "content_block_stop" => "\n", + _ => null, + }; +} +``` + +Add new methods: + +```csharp +private static string? HandleBlockStart(JsonElement evt) +{ + if (!evt.TryGetProperty("content_block", out var block)) return null; + if (!block.TryGetProperty("type", out var blockType)) return null; + + if (blockType.GetString() == "tool_use" && + block.TryGetProperty("name", out var name)) + { + return $"\n[Tool: {name.GetString()}]\n"; + } + return null; +} + +private static string? HandleResult(JsonElement root) +{ + if (root.TryGetProperty("result", out var resultProp)) + { + var text = resultProp.GetString(); + if (text is not null) + return $"\n--- Result ---\n{text}\n"; + } + return null; +} + +private static string? HandleSystem(JsonElement root) +{ + if (root.TryGetProperty("subtype", out var subtype) && + subtype.GetString() == "api_retry") + { + return "\n[Retrying API call...]\n"; + } + return null; +} +``` + +- [ ] **Step 4: Run tests to verify all pass** + +Run: `dotnet test tests/ClaudeDo.Ui.Tests --filter "StreamLineFormatterTests" -v quiet` +Expected: 11 passed. + +- [ ] **Step 5: Commit** + +```bash +git add src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs +git commit -m "feat(ui): complete StreamLineFormatter with tool use, result, system events" +``` + +--- + +### Task 4: StreamLineFormatter — FormatFile method (TDD) + +**Files:** +- Modify: `src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs` +- Modify: `tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs` + +- [ ] **Step 1: Write failing test for FormatFile** + +Append to `StreamLineFormatterTests.cs`: + +```csharp +[Fact] +public void FormatFile_ParsesAllLinesAndReturnsFormattedText() +{ + var dir = Path.Combine(Path.GetTempPath(), "claudedo_test_" + Guid.NewGuid().ToString("N")[..8]); + Directory.CreateDirectory(dir); + var filePath = Path.Combine(dir, "test.ndjson"); + try + { + 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_stop","index":0}}""", + """{"type":"stream_event","event":{"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"t1","name":"Edit","input":{}}}}""", + """{"type":"stream_event","event":{"type":"content_block_stop","index":1}}""", + """{"type":"result","result":"Done.","session_id":"s1"}""", + }; + File.WriteAllLines(filePath, lines); + + var result = _sut.FormatFile(filePath); + Assert.Contains("Hello", result); + Assert.Contains("[Tool: Edit]", result); + Assert.Contains("--- Result ---", result); + Assert.Contains("Done.", result); + } + finally + { + Directory.Delete(dir, true); + } +} + +[Fact] +public void FormatFile_TrimsLargeContent() +{ + var dir = Path.Combine(Path.GetTempPath(), "claudedo_test_" + Guid.NewGuid().ToString("N")[..8]); + Directory.CreateDirectory(dir); + var filePath = Path.Combine(dir, "large.ndjson"); + try + { + // Generate enough text deltas to exceed 50k chars + var lines = new List(); + for (int i = 0; i < 600; i++) + { + var chunk = new string('x', 100); + lines.Add($$"""{"type":"stream_event","event":{"type":"content_block_delta","index":0,"delta":{"type":"text_delta","text":"{{chunk}}\n"}}}"""); + } + File.WriteAllLines(filePath, lines); + + var result = _sut.FormatFile(filePath); + Assert.True(result.Length <= 50_000 + 200); // some tolerance for trimming at newline boundary + } + finally + { + Directory.Delete(dir, true); + } +} +``` + +- [ ] **Step 2: Run tests to verify they fail** + +Run: `dotnet test tests/ClaudeDo.Ui.Tests --filter "FormatFile" -v quiet` +Expected: Build error — `FormatFile` method does not exist. + +- [ ] **Step 3: Implement FormatFile and trimming** + +Add to `StreamLineFormatter.cs`: + +```csharp +private const int MaxLength = 50_000; + +public string FormatFile(string filePath) +{ + var sb = new System.Text.StringBuilder(); + foreach (var line in File.ReadLines(filePath)) + { + var formatted = FormatLine(line); + if (formatted is not null) + sb.Append(formatted); + } + return Trim(sb.ToString()); +} + +public static string Trim(string text) +{ + if (text.Length <= MaxLength) return text; + var trimStart = text.Length - MaxLength; + var newlineAfter = text.IndexOf('\n', trimStart); + if (newlineAfter >= 0 && newlineAfter < trimStart + 200) + trimStart = newlineAfter + 1; + return text[trimStart..]; +} +``` + +Add `using System.Text;` to the top of the file. + +- [ ] **Step 4: Run tests to verify all pass** + +Run: `dotnet test tests/ClaudeDo.Ui.Tests --filter "StreamLineFormatterTests" -v quiet` +Expected: 13 passed. + +- [ ] **Step 5: Commit** + +```bash +git add src/ClaudeDo.Ui/Helpers/StreamLineFormatter.cs tests/ClaudeDo.Ui.Tests/Helpers/StreamLineFormatterTests.cs +git commit -m "feat(ui): add FormatFile and text trimming to StreamLineFormatter" +``` + +--- + +### Task 5: TaskDetailViewModel — replace LiveLines with LiveText + +**Files:** +- Modify: `src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs` + +- [ ] **Step 1: Replace LiveLines with LiveText and wire formatter** + +In `TaskDetailViewModel.cs`, make these changes: + +1. Add using at top: +```csharp +using ClaudeDo.Ui.Helpers; +``` + +2. Replace the LiveLines property (line 40) and MaxLiveLines constant (line 47): + +Remove: +```csharp +public ObservableCollection LiveLines { get; } = new(); +``` +and: +```csharp +private const int MaxLiveLines = 500; +``` + +Add: +```csharp +[ObservableProperty] private string _liveText = ""; +private StreamLineFormatter _formatter = new(); +``` + +3. Update `LoadAsync` (line 69) — replace `LiveLines.Clear()` with: +```csharp +LiveText = ""; +_formatter = new StreamLineFormatter(); +``` + +4. Update `Clear` method — replace `LiveLines.Clear()` with: +```csharp +LiveText = ""; +_formatter = new StreamLineFormatter(); +``` + +5. Update `OnTaskMessage` (lines 259-265): + +Replace entire method: +```csharp +private void OnTaskMessage(string taskId, string line) +{ + if (taskId != _taskId) return; + var formatted = _formatter.FormatLine(line); + if (formatted is not null) + { + LiveText += formatted; + if (LiveText.Length > 50_000) + LiveText = StreamLineFormatter.Trim(LiveText); + } +} +``` + +- [ ] **Step 2: Verify build** + +Run: `dotnet build src/ClaudeDo.Ui` +Expected: Build succeeded. + +- [ ] **Step 3: Commit** + +```bash +git add src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs +git commit -m "refactor(ui): replace LiveLines with LiveText + StreamLineFormatter" +``` + +--- + +### Task 6: TaskDetailView — TextBox replaces ItemsControl + auto-scroll + +**Files:** +- Modify: `src/ClaudeDo.Ui/Views/TaskDetailView.axaml` +- Modify: `src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs` + +- [ ] **Step 1: Replace ItemsControl with TextBox in TaskDetailView.axaml** + +Replace lines 107-122 (the "Live Output" section heading through the Border/ItemsControl): + +Old: +```xml + + + + + + + + + + + + +``` + +New: +```xml + + + + + + +``` + +- [ ] **Step 2: Add auto-scroll in code-behind** + +In `TaskDetailView.axaml.cs`, add an `OnDataContextChanged` override and property-change handler: + +Add using: +```csharp +using System.ComponentModel; +``` + +Add after the `FocusTitle` method: + +```csharp +protected override void OnDataContextChanged(EventArgs e) +{ + base.OnDataContextChanged(e); + if (DataContext is TaskDetailViewModel vm) + { + vm.PropertyChanged += OnViewModelPropertyChanged; + } +} + +private void OnViewModelPropertyChanged(object? sender, PropertyChangedEventArgs e) +{ + if (e.PropertyName == nameof(TaskDetailViewModel.LiveText)) + { + var scroll = this.FindControl("LiveOutputScroll"); + scroll?.ScrollToEnd(); + } +} +``` + +- [ ] **Step 3: Verify build** + +Run: `dotnet build src/ClaudeDo.Ui` +Expected: Build succeeded. + +- [ ] **Step 4: Commit** + +```bash +git add src/ClaudeDo.Ui/Views/TaskDetailView.axaml src/ClaudeDo.Ui/Views/TaskDetailView.axaml.cs +git commit -m "feat(ui): replace ItemsControl with TextBox for formatted live output" +``` + +--- + +### Task 7: Log reload from disk + +**Files:** +- Modify: `src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs` + +- [ ] **Step 1: Add log reload to LoadAsync** + +In `TaskDetailViewModel.cs`, in `LoadAsync`, after `LogPath = task.LogPath;` (around line 81), add: + +```csharp +// Load historical log for completed tasks +if (task.LogPath is not null + && task.Status is Data.Models.TaskStatus.Done or Data.Models.TaskStatus.Failed + && File.Exists(task.LogPath)) +{ + _formatter = new StreamLineFormatter(); + LiveText = _formatter.FormatFile(task.LogPath); +} +``` + +Add `using System.IO;` at the top if not already present. + +- [ ] **Step 2: Verify build** + +Run: `dotnet build src/ClaudeDo.Ui` +Expected: Build succeeded. + +- [ ] **Step 3: Commit** + +```bash +git add src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs +git commit -m "feat(ui): reload formatted log from disk for completed tasks" +``` + +--- + +### Task 8: WorkerClient — RunNowRequestedEvent + +**Files:** +- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs` + +- [ ] **Step 1: Add RunNowRequestedEvent and fire it in RunNowAsync** + +In `WorkerClient.cs`: + +Add event declaration after the existing events (after line 44): +```csharp +public event Action? RunNowRequestedEvent; +``` + +Update `RunNowAsync` method (lines 163-166): +```csharp +public async Task RunNowAsync(string taskId) +{ + RunNowRequestedEvent?.Invoke(taskId); + await _hub.InvokeAsync("RunNow", taskId); +} +``` + +- [ ] **Step 2: Verify build** + +Run: `dotnet build src/ClaudeDo.Ui` +Expected: Build succeeded. + +- [ ] **Step 3: Commit** + +```bash +git add src/ClaudeDo.Ui/Services/WorkerClient.cs +git commit -m "feat(ui): add RunNowRequestedEvent for optimistic UI feedback" +``` + +--- + +### Task 9: TaskItemViewModel — IsStarting state + +**Files:** +- Modify: `src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs` + +- [ ] **Step 1: Add IsStarting property and update CanRunNow** + +In `TaskItemViewModel.cs`: + +Add property after line 16: +```csharp +[ObservableProperty] private bool _isStarting; +``` + +Update `CanRunNow` (line 83-84): +```csharp +private bool CanRunNow() => + _canRunNow() && Status != TaskStatus.Running && !IsStarting; +``` + +Add method to set starting state (after `Refresh` method): +```csharp +public void SetStarting() +{ + IsStarting = true; + StatusText = "starting..."; + RunNowCommand.NotifyCanExecuteChanged(); +} + +public void ClearStarting() +{ + IsStarting = false; + RunNowCommand.NotifyCanExecuteChanged(); +} +``` + +Update `Refresh` method — add after `OnPropertyChanged(nameof(IsRunning))` (line 68): +```csharp +IsStarting = false; +``` + +- [ ] **Step 2: Verify build** + +Run: `dotnet build src/ClaudeDo.Ui` +Expected: Build succeeded. + +- [ ] **Step 3: Commit** + +```bash +git add src/ClaudeDo.Ui/ViewModels/TaskItemViewModel.cs +git commit -m "feat(ui): add IsStarting state to TaskItemViewModel" +``` + +--- + +### Task 10: TaskListViewModel — wire RunNowRequested to TaskItemViewModels + +**Files:** +- Modify: `src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs` + +- [ ] **Step 1: Subscribe to RunNowRequestedEvent and TaskStartedEvent** + +In `TaskListViewModel.cs` constructor, after the existing `worker.PropertyChanged` subscription (after line 57): + +```csharp +worker.RunNowRequestedEvent += taskId => +{ + var item = Tasks.FirstOrDefault(t => t.Id == taskId); + item?.SetStarting(); +}; + +worker.TaskStartedEvent += (_, taskId, _) => +{ + var item = Tasks.FirstOrDefault(t => t.Id == taskId); + item?.ClearStarting(); +}; +``` + +- [ ] **Step 2: Verify build** + +Run: `dotnet build src/ClaudeDo.Ui` +Expected: Build succeeded. + +- [ ] **Step 3: Commit** + +```bash +git add src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs +git commit -m "feat(ui): wire RunNowRequested to TaskItemViewModel starting state" +``` + +--- + +### Task 11: TaskDetailViewModel — start feedback + +**Files:** +- Modify: `src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs` + +- [ ] **Step 1: Subscribe to RunNowRequestedEvent and TaskStartedEvent** + +In `TaskDetailViewModel.cs` constructor, after `worker.TaskUpdatedEvent += OnTaskUpdated;` (line 63): + +```csharp +worker.RunNowRequestedEvent += OnRunNowRequested; +worker.TaskStartedEvent += OnTaskStarted; +``` + +Add the handler methods before `OnTaskMessage`: + +```csharp +private void OnRunNowRequested(string taskId) +{ + if (taskId != _taskId) return; + StatusText = "starting..."; + LiveText = ""; + _formatter = new StreamLineFormatter(); +} + +private void OnTaskStarted(string slot, string taskId, DateTime startedAt) +{ + if (taskId != _taskId) return; + StatusText = "running"; +} +``` + +- [ ] **Step 2: Verify build** + +Run: `dotnet build src/ClaudeDo.Ui` +Expected: Build succeeded. + +- [ ] **Step 3: Commit** + +```bash +git add src/ClaudeDo.Ui/ViewModels/TaskDetailViewModel.cs +git commit -m "feat(ui): add optimistic start feedback to TaskDetailViewModel" +``` + +--- + +### Task 12: TaskListView — starting state visual + +**Files:** +- Modify: `src/ClaudeDo.Ui/Views/TaskListView.axaml` + +- [ ] **Step 1: Add starting indicator next to running indicator** + +In `TaskListView.axaml`, find the running indicator Ellipse (the orange dot visible when `IsRunning`). After it, add a similar indicator for the starting state. The exact location depends on the layout, but it should be adjacent to the existing status indicators. + +Find the `IsRunning` Ellipse and add a sibling for `IsStarting`: + +```xml + + +``` + +Use a gold/yellow color (`#FFD700`) to distinguish "starting" from "running" (orange). + +- [ ] **Step 2: Verify build** + +Run: `dotnet build src/ClaudeDo.Ui` +Expected: Build succeeded. + +- [ ] **Step 3: Commit** + +```bash +git add src/ClaudeDo.Ui/Views/TaskListView.axaml +git commit -m "feat(ui): add starting state indicator in task list" +``` + +--- + +### Task 13: Modal theming — ListEditorView + +**Files:** +- Modify: `src/ClaudeDo.Ui/Views/ListEditorView.axaml` + +- [ ] **Step 1: Apply theme resources to ListEditorView** + +In `ListEditorView.axaml`, update the `` element — add `Background`: + +```xml + +``` + +Add `Foreground="{StaticResource TextSecondaryBrush}"` to each label TextBlock ("Name", "Working Directory", "Default Commit Type"). + +Style the Save button with accent: +```xml +