refactor(worker): remove MessageParser (replaced by StreamAnalyzer)

This commit is contained in:
Mika Kuns
2026-04-14 14:12:21 +02:00
parent 03728c8e4a
commit c1c4c75979
26 changed files with 3978 additions and 88 deletions

View File

@@ -0,0 +1,29 @@
# ClaudeDo.App
Desktop entry point for the ClaudeDo application. Configures DI, initializes the database, and launches the Avalonia window.
## Responsibility
- `Program.cs` — STA thread, DI container registration (repositories, services, viewmodels), schema init, Avalonia builder
- `App.axaml` / `App.axaml.cs` — Avalonia application lifecycle, main window creation, static `ServiceProvider` accessor
- `ViewLocator.cs` — reflection-based IDataTemplate that maps ViewModels to Views by naming convention
## Dependencies
- Avalonia 12.0.0 (Desktop, Fluent theme, Inter fonts)
- CommunityToolkit.Mvvm 8.4.1
- Microsoft.Extensions.DependencyInjection 8.0.1
- Microsoft.AspNetCore.SignalR.Client 8.0.11
- Microsoft.Data.Sqlite 8.0.11
- Project references: ClaudeDo.Data, ClaudeDo.Ui
## DI Registration Pattern
- **Singletons**: SqliteConnectionFactory, all Repositories, WorkerClient, MainWindowViewModel, TaskListViewModel, TaskDetailViewModel, StatusBarViewModel
- **Transients**: TaskEditorViewModel, ListEditorViewModel (created per dialog)
## Notes
- This project owns the composition root — all wiring happens here
- ViewLocator resolves `FooViewModel` -> `FooView` by replacing "ViewModel" with "View" in the type name
- AvaloniaUI diagnostics are conditionally included (DEBUG only)

View File

@@ -0,0 +1,41 @@
# ClaudeDo.Data
Shared data layer: models, repositories, SQLite infrastructure, and git operations.
## Models
- **TaskEntity** — Id, ListId, Title, Description, Status (Manual|Queued|Running|Done|Failed), ScheduledFor, Result, LogPath, timestamps, CommitType
- **ListEntity** — Id, Name, WorkingDir, DefaultCommitType, CreatedAt
- **TagEntity** — Id (autoincrement), Name (unique)
- **WorktreeEntity** — TaskId (PK, 1:1 with task), Path, BranchName, BaseCommit, HeadCommit, DiffStat, State (Active|Merged|Discarded|Kept)
## Repositories
All repositories use raw parameterized SQL via `Microsoft.Data.Sqlite`. Each method opens its own connection — no Unit of Work.
- **TaskRepository** — CRUD, status transitions (`MarkRunningAsync`, `MarkDoneAsync`, `MarkFailedAsync`), `GetNextQueuedAgentTaskAsync` (queue polling), `GetEffectiveTagsAsync` (union of task + list tags), `FlipAllRunningToFailedAsync`
- **ListRepository** — CRUD, tag junction management
- **TagRepository** — `GetOrCreateAsync` (idempotent)
- **WorktreeRepository** — CRUD, `UpdateHeadAsync`, `SetStateAsync`
## Infrastructure
- **SqliteConnectionFactory** — creates connections, applies WAL mode once, enforces foreign keys via PRAGMA
- **SchemaInitializer** — applies embedded `schema/schema.sql` idempotently (IF NOT EXISTS, INSERT OR IGNORE)
- **Paths** — expands `~` and `%USERPROFILE%`, resolves relative paths. App root: `~/.todo-app`
- **AppSettings** — loads `~/.todo-app/ui.config.json` (DbPath, SignalRUrl)
## Git
- **GitService** — async wrapper around git CLI (ProcessStartInfo, no shell). Operations: worktree add/remove, add all, commit (stdin for message), merge ff-only, rev-parse, diff-stat, has-changes, is-git-repo
## Schema
6 tables: `lists`, `tasks`, `tags`, `list_tags`, `task_tags`, `worktrees`. See `schema/schema.sql`. Seed data: tags "agent" and "manual".
## Conventions
- Enum <-> string mapping via explicit `ToDb()`/`FromDb()` static methods on each enum
- Primary keys are `init`-only strings (GUIDs assigned at creation)
- Nullable fields use `DBNull.Value` checks
- All methods are async with CancellationToken where applicable

49
src/ClaudeDo.Ui/CLAUDE.md Normal file
View File

@@ -0,0 +1,49 @@
# ClaudeDo.Ui
Avalonia UI layer: views, viewmodels, converters, and the SignalR client.
## Pattern
MVVM with CommunityToolkit.Mvvm source generators:
- `[ObservableProperty]` for bindable properties
- `[RelayCommand]` for commands (supports async and CanExecute)
- All ViewModels inherit `ViewModelBase` (extends `ObservableObject`)
## Views
- **MainWindow** — 3-column DockPanel layout (lists | tasks | detail) with GridSplitter, status bar at bottom
- **TaskListView** — ListBox of tasks with add/edit/delete toolbar
- **TaskDetailView** — Task info, live log output, worktree section (merge/keep/discard)
- **TaskEditorView** — Modal dialog for task create/edit
- **ListEditorView** — Modal dialog for list create/edit
- **StatusBarView** — Connection status indicator, active task display
All views use compiled bindings (`x:DataType`).
## ViewModels
- **MainWindowViewModel** — root coordinator; manages list collection, selected list, dialog creation via `Func<T>` factories
- **TaskListViewModel** — manages task collection for selected list; handles CRUD, "Run Now"
- **TaskDetailViewModel** — displays task details, streams live log, controls worktree operations
- **TaskItemViewModel** / **ListItemViewModel** — lightweight display VMs
- **TaskEditorViewModel** / **ListEditorViewModel** — dialog VMs with validation
- **StatusBarViewModel** — connection state and active tasks
## Services
- **WorkerClient** — SignalR client connecting to `http://127.0.0.1:47821/hub`. Auto-reconnect with exponential backoff. Methods: StartAsync, RunNowAsync, CancelTaskAsync, WakeQueueAsync. Events: TaskStarted, TaskFinished, TaskMessage, TaskUpdated, WorktreeUpdated
## Converters
- **StatusColorConverter** — task status string -> color (Queued=Blue, Running=Orange, Done=Green, Failed=Red, Manual=Gray)
- **ConnectionColorConverter** — connection state -> color (Online=Green, Offline=Red)
## Dialog Pattern
Editor dialogs use `TaskCompletionSource<bool>` — the dialog sets the result on save/cancel, and the caller awaits the TCS.
## Notes
- Context menus are on both list items and task items
- Right-click selects the item before showing the context menu
- "Run Now" CanExecute re-evaluates when worker connection state changes

View File

@@ -12,8 +12,10 @@
<TextBox Text="{Binding Name}" PlaceholderText="List name..."/>
<TextBlock Text="Working Directory" FontWeight="SemiBold"/>
<TextBox Text="{Binding WorkingDir}" PlaceholderText="(optional) Absolute path to git repo"/>
<!-- TODO: folder picker button using IStorageProvider -->
<DockPanel>
<Button DockPanel.Dock="Right" Content="Browse..." Click="OnBrowseFolder" Margin="8,0,0,0" VerticalAlignment="Center"/>
<TextBox Text="{Binding WorkingDir}" PlaceholderText="(optional) Absolute path to git repo"/>
</DockPanel>
<TextBlock Text="Default Commit Type" FontWeight="SemiBold"/>
<ComboBox ItemsSource="{Binding CommitTypes}"

View File

@@ -1,4 +1,9 @@
using System;
using System.IO;
using Avalonia.Controls;
using Avalonia.Interactivity;
using Avalonia.Platform.Storage;
using ClaudeDo.Ui.ViewModels;
namespace ClaudeDo.Ui.Views;
@@ -8,4 +13,28 @@ public partial class ListEditorView : Window
{
InitializeComponent();
}
private async void OnBrowseFolder(object? sender, RoutedEventArgs e)
{
var vm = DataContext as ListEditorViewModel;
var startPath = !string.IsNullOrWhiteSpace(vm?.WorkingDir) && Directory.Exists(vm.WorkingDir)
? vm.WorkingDir
: Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
var startLocation = await StorageProvider.TryGetFolderFromPathAsync(new Uri(startPath));
var result = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
{
Title = "Select Working Directory",
SuggestedStartLocation = startLocation,
AllowMultiple = false,
});
if (result.Count > 0)
{
var path = result[0].TryGetLocalPath();
if (path is not null && vm is not null)
vm.WorkingDir = path;
}
}
}

View File

@@ -20,6 +20,7 @@ builder.Services.AddSingleton<TagRepository>();
builder.Services.AddSingleton<ListRepository>();
builder.Services.AddSingleton<TaskRepository>();
builder.Services.AddSingleton<WorktreeRepository>();
builder.Services.AddSingleton<TaskRunRepository>();
builder.Services.AddHostedService<StaleTaskRecovery>();
builder.Services.AddSignalR();
@@ -28,8 +29,14 @@ builder.Services.AddSingleton<IClaudeProcess, ClaudeProcess>();
builder.Services.AddSingleton<HubBroadcaster>();
builder.Services.AddSingleton<GitService>();
builder.Services.AddSingleton<WorktreeManager>();
builder.Services.AddSingleton<ClaudeArgsBuilder>();
builder.Services.AddSingleton<TaskRunner>();
// Agent file management.
var agentsDir = Path.Combine(ClaudeDo.Data.Paths.AppDataRoot(), "agents");
Directory.CreateDirectory(agentsDir);
builder.Services.AddSingleton(new AgentFileService(agentsDir));
// QueueService: singleton + hosted service (same instance).
builder.Services.AddSingleton<QueueService>();
builder.Services.AddHostedService(sp => sp.GetRequiredService<QueueService>());

View File

@@ -1,33 +0,0 @@
using System.Text.Json;
namespace ClaudeDo.Worker.Runner;
public static class MessageParser
{
public static bool TryExtractResult(string ndjsonLine, out string? result)
{
result = null;
if (string.IsNullOrWhiteSpace(ndjsonLine))
return false;
try
{
using var doc = JsonDocument.Parse(ndjsonLine);
var root = doc.RootElement;
if (root.TryGetProperty("type", out var typeProp) &&
typeProp.GetString() == "result" &&
root.TryGetProperty("result", out var resultProp))
{
result = resultProp.GetString();
return true;
}
}
catch (JsonException)
{
// Malformed JSON — not a result line.
}
return false;
}
}