23 tasks covering: StreamLineFormatter (TDD), LiveText display, start feedback, log reload, modal theming, and config editors. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1551 lines
44 KiB
Markdown
1551 lines
44 KiB
Markdown
# 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<string>` 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
|
|
<!-- tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj -->
|
|
<Project Sdk="Microsoft.NET.Sdk">
|
|
<PropertyGroup>
|
|
<TargetFramework>net8.0</TargetFramework>
|
|
<Nullable>enable</Nullable>
|
|
<IsPackable>false</IsPackable>
|
|
</PropertyGroup>
|
|
<ItemGroup>
|
|
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.12.0" />
|
|
<PackageReference Include="xunit" Version="2.9.3" />
|
|
<PackageReference Include="xunit.runner.visualstudio" Version="3.0.1" />
|
|
</ItemGroup>
|
|
<ItemGroup>
|
|
<ProjectReference Include="../../src/ClaudeDo.Ui/ClaudeDo.Ui.csproj" />
|
|
</ItemGroup>
|
|
</Project>
|
|
```
|
|
|
|
- [ ] **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<string>();
|
|
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<string> 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
|
|
<TextBlock Text="Live Output" FontWeight="SemiBold" FontSize="12"
|
|
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,8,0,2"/>
|
|
<Border BorderBrush="{StaticResource BorderSubtleBrush}" BorderThickness="1"
|
|
CornerRadius="6" Padding="6" MaxHeight="200">
|
|
<ScrollViewer>
|
|
<ItemsControl ItemsSource="{Binding LiveLines}">
|
|
<ItemsControl.ItemTemplate>
|
|
<DataTemplate>
|
|
<TextBlock Text="{Binding}" FontFamily="Consolas,Courier New,monospace"
|
|
FontSize="11" TextWrapping="NoWrap"
|
|
Foreground="{StaticResource TextPrimaryBrush}"/>
|
|
</DataTemplate>
|
|
</ItemsControl.ItemTemplate>
|
|
</ItemsControl>
|
|
</ScrollViewer>
|
|
</Border>
|
|
```
|
|
|
|
New:
|
|
```xml
|
|
<TextBlock Text="Live Output" FontWeight="SemiBold" FontSize="12"
|
|
Foreground="{StaticResource TextSecondaryBrush}" Margin="0,8,0,2"/>
|
|
<Border BorderBrush="{StaticResource BorderSubtleBrush}" BorderThickness="1"
|
|
CornerRadius="6" Padding="6" MaxHeight="300">
|
|
<ScrollViewer x:Name="LiveOutputScroll">
|
|
<TextBox x:Name="LiveOutputBox"
|
|
Text="{Binding LiveText, Mode=OneWay}"
|
|
IsReadOnly="True"
|
|
AcceptsReturn="True"
|
|
TextWrapping="NoWrap"
|
|
FontFamily="Consolas,Courier New,monospace"
|
|
FontSize="11"
|
|
Foreground="{StaticResource TextPrimaryBrush}"
|
|
Background="Transparent"
|
|
BorderThickness="0"
|
|
Padding="0"/>
|
|
</ScrollViewer>
|
|
</Border>
|
|
```
|
|
|
|
- [ ] **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<ScrollViewer>("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<string>? 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
|
|
<!-- Starting indicator (pulsing or different shade) -->
|
|
<Ellipse Width="8" Height="8" Fill="#FFD700"
|
|
IsVisible="{Binding IsStarting}"
|
|
HorizontalAlignment="Center" VerticalAlignment="Center"/>
|
|
```
|
|
|
|
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 `<Window>` element — add `Background`:
|
|
|
|
```xml
|
|
<Window xmlns="https://github.com/avaloniaui"
|
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
xmlns:vm="using:ClaudeDo.Ui.ViewModels"
|
|
x:Class="ClaudeDo.Ui.Views.ListEditorView"
|
|
x:DataType="vm:ListEditorViewModel"
|
|
Title="{Binding WindowTitle}"
|
|
Width="450" Height="280"
|
|
WindowStartupLocation="CenterOwner"
|
|
CanResize="False"
|
|
Background="{StaticResource WindowBgBrush}">
|
|
```
|
|
|
|
Add `Foreground="{StaticResource TextSecondaryBrush}"` to each label TextBlock ("Name", "Working Directory", "Default Commit Type").
|
|
|
|
Style the Save button with accent:
|
|
```xml
|
|
<Button Content="Save" Command="{Binding SaveCommand}" IsDefault="True" MinWidth="80"
|
|
Background="{StaticResource AccentBrush}" Foreground="{StaticResource TextPrimaryBrush}"/>
|
|
```
|
|
|
|
- [ ] **Step 2: Verify build**
|
|
|
|
Run: `dotnet build src/ClaudeDo.Ui`
|
|
Expected: Build succeeded.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add src/ClaudeDo.Ui/Views/ListEditorView.axaml
|
|
git commit -m "fix(ui): apply dark theme to ListEditorView modal"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 14: Modal theming — TaskEditorView
|
|
|
|
**Files:**
|
|
- Modify: `src/ClaudeDo.Ui/Views/TaskEditorView.axaml`
|
|
|
|
- [ ] **Step 1: Apply theme resources to TaskEditorView**
|
|
|
|
In `TaskEditorView.axaml`, update the `<Window>` element — add `Background`:
|
|
|
|
```xml
|
|
<Window xmlns="https://github.com/avaloniaui"
|
|
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
|
xmlns:vm="using:ClaudeDo.Ui.ViewModels"
|
|
x:Class="ClaudeDo.Ui.Views.TaskEditorView"
|
|
x:DataType="vm:TaskEditorViewModel"
|
|
Title="{Binding WindowTitle}"
|
|
Width="500" Height="420"
|
|
WindowStartupLocation="CenterOwner"
|
|
CanResize="False"
|
|
Background="{StaticResource WindowBgBrush}">
|
|
```
|
|
|
|
Add `Foreground="{StaticResource TextSecondaryBrush}"` to each label TextBlock ("Title", "Description", "Status", "Commit Type", "Tags (comma-separated)").
|
|
|
|
Style the Save button:
|
|
```xml
|
|
<Button Content="Save" Command="{Binding SaveCommand}" IsDefault="True" MinWidth="80"
|
|
Background="{StaticResource AccentBrush}" Foreground="{StaticResource TextPrimaryBrush}"/>
|
|
```
|
|
|
|
- [ ] **Step 2: Verify build**
|
|
|
|
Run: `dotnet build src/ClaudeDo.Ui`
|
|
Expected: Build succeeded.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add src/ClaudeDo.Ui/Views/TaskEditorView.axaml
|
|
git commit -m "fix(ui): apply dark theme to TaskEditorView modal"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 15: WorkerClient — GetAgentsAsync + AgentInfo DTO
|
|
|
|
**Files:**
|
|
- Modify: `src/ClaudeDo.Ui/Services/WorkerClient.cs`
|
|
|
|
- [ ] **Step 1: Add AgentInfo DTO and hub methods**
|
|
|
|
In `WorkerClient.cs`:
|
|
|
|
Add the DTO record at the bottom of the file (outside the `WorkerClient` class, inside the namespace):
|
|
|
|
```csharp
|
|
public record AgentInfo(string Name, string Description, string Path);
|
|
```
|
|
|
|
Add methods after `WakeQueueAsync` (after line 175):
|
|
|
|
```csharp
|
|
public async Task<List<AgentInfo>> GetAgentsAsync()
|
|
{
|
|
try
|
|
{
|
|
var agents = await _hub.InvokeAsync<List<AgentInfo>>("GetAgents");
|
|
return agents ?? [];
|
|
}
|
|
catch
|
|
{
|
|
return [];
|
|
}
|
|
}
|
|
|
|
public async Task RefreshAgentsAsync()
|
|
{
|
|
await _hub.InvokeAsync("RefreshAgents");
|
|
}
|
|
```
|
|
|
|
- [ ] **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 GetAgentsAsync and AgentInfo DTO to WorkerClient"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 16: ListEditorViewModel — config fields
|
|
|
|
**Files:**
|
|
- Modify: `src/ClaudeDo.Ui/ViewModels/ListEditorViewModel.cs`
|
|
|
|
- [ ] **Step 1: Add config properties and model mapping**
|
|
|
|
In `ListEditorViewModel.cs`:
|
|
|
|
Add using:
|
|
```csharp
|
|
using ClaudeDo.Ui.Services;
|
|
```
|
|
|
|
Add static model mapping after `CommitTypes`:
|
|
|
|
```csharp
|
|
public static string[] ModelDisplayNames { get; } = ["Sonnet", "Opus", "Haiku"];
|
|
|
|
private static readonly Dictionary<string, string> ModelToId = new()
|
|
{
|
|
["Sonnet"] = "claude-sonnet-4-6",
|
|
["Opus"] = "claude-opus-4-6",
|
|
["Haiku"] = "claude-haiku-4-5",
|
|
};
|
|
|
|
private static readonly Dictionary<string, string> IdToModel =
|
|
ModelToId.ToDictionary(kv => kv.Value, kv => kv.Key);
|
|
|
|
public static string ModelIdToDisplay(string? modelId) =>
|
|
modelId is not null && IdToModel.TryGetValue(modelId, out var display) ? display : "Sonnet";
|
|
|
|
public static string? ModelDisplayToId(string display) =>
|
|
ModelToId.TryGetValue(display, out var id) ? id : null;
|
|
```
|
|
|
|
Add config properties after `_windowTitle`:
|
|
|
|
```csharp
|
|
[ObservableProperty] private string _model = "Sonnet";
|
|
[ObservableProperty] private string? _systemPrompt;
|
|
[ObservableProperty] private AgentInfo? _selectedAgent;
|
|
|
|
public List<AgentInfo> AvailableAgents { get; set; } = [];
|
|
```
|
|
|
|
Add a `WorkerClient` field and update constructor — but since the ViewModel is created via factory (`Func<ListEditorViewModel>`), the WorkerClient needs to be injected. Add a method to load agents:
|
|
|
|
```csharp
|
|
public async Task LoadAgentsAsync(WorkerClient worker)
|
|
{
|
|
AvailableAgents = await worker.GetAgentsAsync();
|
|
}
|
|
```
|
|
|
|
Update `InitForEdit` to accept config:
|
|
|
|
```csharp
|
|
public void InitForEdit(ListEntity entity, Data.Models.ListConfigEntity? config)
|
|
{
|
|
_editId = entity.Id;
|
|
_createdAt = entity.CreatedAt;
|
|
Name = entity.Name;
|
|
WorkingDir = entity.WorkingDir;
|
|
DefaultCommitType = entity.DefaultCommitType;
|
|
WindowTitle = $"Edit List: {entity.Name}";
|
|
|
|
if (config is not null)
|
|
{
|
|
Model = ModelIdToDisplay(config.Model);
|
|
SystemPrompt = config.SystemPrompt;
|
|
SelectedAgentPath = config.AgentPath;
|
|
}
|
|
}
|
|
```
|
|
|
|
Add a method to build the config entity for saving:
|
|
|
|
```csharp
|
|
public Data.Models.ListConfigEntity? BuildConfig(string listId)
|
|
{
|
|
var modelId = ModelDisplayToId(Model);
|
|
if (modelId is null && SystemPrompt is null && SelectedAgentPath is null)
|
|
return null;
|
|
|
|
return new Data.Models.ListConfigEntity
|
|
{
|
|
ListId = listId,
|
|
Model = modelId,
|
|
SystemPrompt = string.IsNullOrWhiteSpace(SystemPrompt) ? null : SystemPrompt.Trim(),
|
|
AgentPath = SelectedAgentPath,
|
|
};
|
|
}
|
|
```
|
|
|
|
- [ ] **Step 2: Verify build**
|
|
|
|
Run: `dotnet build src/ClaudeDo.Ui`
|
|
Expected: Build succeeded.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add src/ClaudeDo.Ui/ViewModels/ListEditorViewModel.cs
|
|
git commit -m "feat(ui): add model/prompt/agent config fields to ListEditorViewModel"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 17: ListEditorView — config section XAML
|
|
|
|
**Files:**
|
|
- Modify: `src/ClaudeDo.Ui/Views/ListEditorView.axaml`
|
|
|
|
- [ ] **Step 1: Add config section and increase window height**
|
|
|
|
In `ListEditorView.axaml`, update `Height="280"` to `Height="480"`.
|
|
|
|
Before the Save/Cancel button StackPanel, add a config section:
|
|
|
|
```xml
|
|
<!-- Divider -->
|
|
<Border Height="1" Background="{StaticResource BorderSubtleBrush}" Margin="0,6,0,2"/>
|
|
|
|
<TextBlock Text="Agent Config" FontWeight="Bold" FontSize="13"
|
|
Foreground="{StaticResource TextPrimaryBrush}"/>
|
|
|
|
<TextBlock Text="Model" FontWeight="SemiBold"
|
|
Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
<ComboBox ItemsSource="{Binding ModelDisplayNames}"
|
|
SelectedItem="{Binding Model}"
|
|
MinWidth="150"/>
|
|
|
|
<TextBlock Text="System Prompt" FontWeight="SemiBold"
|
|
Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
<TextBox Text="{Binding SystemPrompt}"
|
|
PlaceholderText="(optional) Additional system instructions..."
|
|
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="50"/>
|
|
|
|
<TextBlock Text="Agent File" FontWeight="SemiBold"
|
|
Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
<ComboBox x:Name="AgentCombo"
|
|
SelectedValue="{Binding SelectedAgentPath}"
|
|
SelectedValueBinding="{Binding Path}"
|
|
DisplayMemberBinding="{Binding Name}"
|
|
MinWidth="150">
|
|
<ComboBox.Items>
|
|
<!-- Populated from code-behind -->
|
|
</ComboBox.Items>
|
|
</ComboBox>
|
|
```
|
|
|
|
Note: The agent ComboBox will need to be populated from code-behind or via `ItemsSource` binding to `AvailableAgents`. Since `AvailableAgents` is a `List<AgentInfo>`, bind it:
|
|
|
|
```xml
|
|
<ComboBox ItemsSource="{Binding AvailableAgents}"
|
|
SelectedValue="{Binding SelectedAgentPath}"
|
|
MinWidth="150">
|
|
<ComboBox.ItemTemplate>
|
|
<DataTemplate>
|
|
<TextBlock Text="{Binding Name}"/>
|
|
</DataTemplate>
|
|
</ComboBox.ItemTemplate>
|
|
</ComboBox>
|
|
```
|
|
|
|
However, since `SelectedValue` binding with a path property requires `SelectedValueBinding`, and Avalonia's ComboBox support differs from WPF, the simpler approach is to bind to a list of agent path strings with display names handled in the ViewModel. Use a list of display strings and map in the VM.
|
|
|
|
Simpler approach — replace `AvailableAgents` with two parallel properties in the VM:
|
|
|
|
Actually, the cleanest Avalonia pattern: use `ItemsSource` and handle selection via `SelectedItem` where items are `AgentInfo` records, and map to path in the VM.
|
|
|
|
Update the ComboBox to:
|
|
```xml
|
|
<ComboBox ItemsSource="{Binding AvailableAgents}"
|
|
SelectedItem="{Binding SelectedAgent}"
|
|
MinWidth="150">
|
|
<ComboBox.ItemTemplate>
|
|
<DataTemplate x:DataType="svc:AgentInfo">
|
|
<TextBlock Text="{Binding Name}"/>
|
|
</DataTemplate>
|
|
</ComboBox.ItemTemplate>
|
|
</ComboBox>
|
|
```
|
|
|
|
This requires adding `xmlns:svc="using:ClaudeDo.Ui.Services"` to the Window element.
|
|
|
|
And in the ViewModel, change `SelectedAgentPath` to `SelectedAgent`:
|
|
|
|
```csharp
|
|
[ObservableProperty] private AgentInfo? _selectedAgent;
|
|
```
|
|
|
|
Map to/from path in `InitForEdit` and `BuildConfig`:
|
|
- `InitForEdit`: `SelectedAgent = AvailableAgents.FirstOrDefault(a => a.Path == config?.AgentPath);`
|
|
- `BuildConfig`: use `SelectedAgent?.Path`
|
|
|
|
Update `ListEditorViewModel` to use `SelectedAgent` instead of `SelectedAgentPath` in `BuildConfig`:
|
|
```csharp
|
|
AgentPath = SelectedAgent?.Path,
|
|
```
|
|
|
|
- [ ] **Step 2: Verify build**
|
|
|
|
Run: `dotnet build src/ClaudeDo.Ui`
|
|
Expected: Build succeeded.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add src/ClaudeDo.Ui/Views/ListEditorView.axaml src/ClaudeDo.Ui/ViewModels/ListEditorViewModel.cs
|
|
git commit -m "feat(ui): add config section to ListEditorView"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 18: Wire ListEditor config loading/saving in MainWindowViewModel
|
|
|
|
**Files:**
|
|
- Modify: `src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs` (or wherever list editor is opened)
|
|
|
|
The list editor is opened from `MainWindowViewModel` (or wherever the "Edit List" action lives). Find where `ListEditorViewModel.InitForEdit` is called and update it to:
|
|
|
|
1. Load config before opening: `var config = await _listRepo.GetConfigAsync(entity.Id);`
|
|
2. Load agents: `await editor.LoadAgentsAsync(_worker);`
|
|
3. Call updated `InitForEdit(entity, config)`
|
|
4. After save, persist config:
|
|
```csharp
|
|
var configEntity = editor.BuildConfig(saved.Id);
|
|
if (configEntity is not null)
|
|
await _listRepo.SetConfigAsync(configEntity);
|
|
```
|
|
|
|
- [ ] **Step 1: Find and update list editor call sites**
|
|
|
|
Search for `InitForEdit` and `InitForCreate` calls for `ListEditorViewModel` in the codebase. Update each call site to load config and agents.
|
|
|
|
For `InitForCreate`: agents still need to be loaded. Add `await editor.LoadAgentsAsync(_worker);` after `InitForCreate()`.
|
|
|
|
For `InitForEdit`: add config loading before init:
|
|
```csharp
|
|
var config = await _listRepo.GetConfigAsync(entity.Id);
|
|
await editor.LoadAgentsAsync(_worker);
|
|
editor.InitForEdit(entity, config);
|
|
```
|
|
|
|
After save, persist config:
|
|
```csharp
|
|
var configEntity = editor.BuildConfig(saved.Id);
|
|
if (configEntity is not null)
|
|
await _listRepo.SetConfigAsync(configEntity);
|
|
```
|
|
|
|
- [ ] **Step 2: Verify build**
|
|
|
|
Run: `dotnet build src/ClaudeDo.Ui`
|
|
Expected: Build succeeded.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add src/ClaudeDo.Ui/ViewModels/MainWindowViewModel.cs
|
|
git commit -m "feat(ui): wire config loading/saving in list editor flow"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 19: TaskEditorViewModel — config override fields
|
|
|
|
**Files:**
|
|
- Modify: `src/ClaudeDo.Ui/ViewModels/TaskEditorViewModel.cs`
|
|
|
|
- [ ] **Step 1: Add config properties with inheritance**
|
|
|
|
In `TaskEditorViewModel.cs`:
|
|
|
|
Add using:
|
|
```csharp
|
|
using ClaudeDo.Ui.Services;
|
|
```
|
|
|
|
Add static model choices (reuse the mapping from ListEditorViewModel):
|
|
|
|
```csharp
|
|
public static string[] ModelChoices { get; } = ["(list default)", "Sonnet", "Opus", "Haiku"];
|
|
```
|
|
|
|
Add config properties after `_windowTitle`:
|
|
|
|
```csharp
|
|
[ObservableProperty] private string _modelChoice = "(list default)";
|
|
[ObservableProperty] private string? _systemPromptOverride;
|
|
[ObservableProperty] private AgentInfo? _selectedAgent;
|
|
public List<AgentInfo> AvailableAgents { get; set; } = [];
|
|
```
|
|
|
|
Add agent loading method:
|
|
|
|
```csharp
|
|
public async Task LoadAgentsAsync(WorkerClient worker)
|
|
{
|
|
AvailableAgents = await worker.GetAgentsAsync();
|
|
}
|
|
```
|
|
|
|
Update `InitForEdit` to load config overrides (add after `TagsInput = ...` line):
|
|
|
|
```csharp
|
|
ModelChoice = entity.Model is not null
|
|
? ListEditorViewModel.ModelIdToDisplay(entity.Model)
|
|
: "(list default)";
|
|
SystemPromptOverride = entity.SystemPrompt;
|
|
SelectedAgent = entity.AgentPath is not null
|
|
? AvailableAgents.FirstOrDefault(a => a.Path == entity.AgentPath)
|
|
: null;
|
|
```
|
|
|
|
Update `Save` to include config on the entity — in the `Save` method, after building the entity, add:
|
|
|
|
```csharp
|
|
entity.Model = ModelChoice != "(list default)"
|
|
? ListEditorViewModel.ModelDisplayToId(ModelChoice)
|
|
: null;
|
|
entity.SystemPrompt = string.IsNullOrWhiteSpace(SystemPromptOverride) ? null : SystemPromptOverride.Trim();
|
|
entity.AgentPath = SelectedAgent?.Path;
|
|
```
|
|
|
|
- [ ] **Step 2: Verify build**
|
|
|
|
Run: `dotnet build src/ClaudeDo.Ui`
|
|
Expected: Build succeeded.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add src/ClaudeDo.Ui/ViewModels/TaskEditorViewModel.cs
|
|
git commit -m "feat(ui): add model/prompt/agent override fields to TaskEditorViewModel"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 20: TaskEditorView — config section XAML
|
|
|
|
**Files:**
|
|
- Modify: `src/ClaudeDo.Ui/Views/TaskEditorView.axaml`
|
|
|
|
- [ ] **Step 1: Add config section and increase height**
|
|
|
|
In `TaskEditorView.axaml`, update `Height="420"` to `Height="600"`.
|
|
|
|
Add `xmlns:svc="using:ClaudeDo.Ui.Services"` to the Window element.
|
|
|
|
Before the Save/Cancel button StackPanel, add:
|
|
|
|
```xml
|
|
<!-- Divider -->
|
|
<Border Height="1" Background="{StaticResource BorderSubtleBrush}" Margin="0,6,0,2"/>
|
|
|
|
<TextBlock Text="Agent Config (overrides)" FontWeight="Bold" FontSize="13"
|
|
Foreground="{StaticResource TextPrimaryBrush}"/>
|
|
|
|
<TextBlock Text="Model" FontWeight="SemiBold"
|
|
Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
<ComboBox ItemsSource="{Binding ModelChoices}"
|
|
SelectedItem="{Binding ModelChoice}"
|
|
MinWidth="150"/>
|
|
|
|
<TextBlock Text="System Prompt" FontWeight="SemiBold"
|
|
Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
<TextBox Text="{Binding SystemPromptOverride}"
|
|
PlaceholderText="(inherits from list)"
|
|
AcceptsReturn="True" TextWrapping="Wrap" MinHeight="50"/>
|
|
|
|
<TextBlock Text="Agent File" FontWeight="SemiBold"
|
|
Foreground="{StaticResource TextSecondaryBrush}"/>
|
|
<ComboBox ItemsSource="{Binding AvailableAgents}"
|
|
SelectedItem="{Binding SelectedAgent}"
|
|
MinWidth="150">
|
|
<ComboBox.ItemTemplate>
|
|
<DataTemplate x:DataType="svc:AgentInfo">
|
|
<TextBlock Text="{Binding Name}"/>
|
|
</DataTemplate>
|
|
</ComboBox.ItemTemplate>
|
|
</ComboBox>
|
|
```
|
|
|
|
- [ ] **Step 2: Verify build**
|
|
|
|
Run: `dotnet build src/ClaudeDo.Ui`
|
|
Expected: Build succeeded.
|
|
|
|
- [ ] **Step 3: Commit**
|
|
|
|
```bash
|
|
git add src/ClaudeDo.Ui/Views/TaskEditorView.axaml
|
|
git commit -m "feat(ui): add config override section to TaskEditorView"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 21: Wire TaskEditor agents loading
|
|
|
|
**Files:**
|
|
- Modify: `src/ClaudeDo.Ui/ViewModels/TaskListViewModel.cs`
|
|
|
|
- [ ] **Step 1: Load agents in AddTask and EditTask**
|
|
|
|
In `TaskListViewModel.cs`:
|
|
|
|
In `AddTask` method (line 130), after `editor.InitForCreate(CurrentListId, defaultCommitType);`, add:
|
|
```csharp
|
|
await editor.LoadAgentsAsync(_worker);
|
|
```
|
|
|
|
In `EditTask` method (line 173), after `editor.InitForEdit(entity, taskTags);`, add:
|
|
```csharp
|
|
await editor.LoadAgentsAsync(_worker);
|
|
```
|
|
|
|
Note: `LoadAgentsAsync` must be called BEFORE `InitForEdit` for the task editor so that `AvailableAgents` is populated when `InitForEdit` tries to find the matching `SelectedAgent`. Reorder:
|
|
|
|
```csharp
|
|
var editor = _editorFactory();
|
|
await editor.LoadAgentsAsync(_worker);
|
|
editor.InitForEdit(entity, taskTags);
|
|
```
|
|
|
|
Same for `AddTask`:
|
|
```csharp
|
|
var editor = _editorFactory();
|
|
await editor.LoadAgentsAsync(_worker);
|
|
editor.InitForCreate(CurrentListId, defaultCommitType);
|
|
```
|
|
|
|
- [ ] **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): load available agents when opening task editor"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 22: TaskRunner — default model fallback
|
|
|
|
**Files:**
|
|
- Modify: `src/ClaudeDo.Worker/Runner/TaskRunner.cs`
|
|
|
|
- [ ] **Step 1: Add Sonnet as default model in config resolution**
|
|
|
|
In `TaskRunner.cs`, find the config resolution (around line 82-83):
|
|
|
|
```csharp
|
|
var resolvedConfig = new ClaudeRunConfig(
|
|
Model: task.Model ?? listConfig?.Model,
|
|
```
|
|
|
|
Change to:
|
|
|
|
```csharp
|
|
var resolvedConfig = new ClaudeRunConfig(
|
|
Model: task.Model ?? listConfig?.Model ?? "claude-sonnet-4-6",
|
|
```
|
|
|
|
- [ ] **Step 2: Verify build**
|
|
|
|
Run: `dotnet build src/ClaudeDo.Worker`
|
|
Expected: Build succeeded.
|
|
|
|
- [ ] **Step 3: Run existing Worker tests**
|
|
|
|
Run: `dotnet test tests/ClaudeDo.Worker.Tests -v quiet`
|
|
Expected: All tests pass.
|
|
|
|
- [ ] **Step 4: Commit**
|
|
|
|
```bash
|
|
git add src/ClaudeDo.Worker/Runner/TaskRunner.cs
|
|
git commit -m "feat(worker): default to claude-sonnet-4-6 when no model configured"
|
|
```
|
|
|
|
---
|
|
|
|
### Task 23: Full build and test verification
|
|
|
|
- [ ] **Step 1: Build entire solution**
|
|
|
|
Run: `dotnet build ClaudeDo.slnx`
|
|
Expected: Build succeeded with 0 errors.
|
|
|
|
- [ ] **Step 2: Run all tests**
|
|
|
|
Run: `dotnet test ClaudeDo.slnx -v quiet`
|
|
Expected: All tests pass (existing Worker tests + new Ui tests).
|
|
|
|
- [ ] **Step 3: Verify no compiler warnings related to changes**
|
|
|
|
Run: `dotnet build ClaudeDo.slnx -warnaserror`
|
|
Expected: Build succeeded (or only pre-existing warnings unrelated to our changes).
|