refactor(worker): remove MessageParser (replaced by StreamAnalyzer)
This commit is contained in:
29
src/ClaudeDo.App/CLAUDE.md
Normal file
29
src/ClaudeDo.App/CLAUDE.md
Normal 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)
|
||||
41
src/ClaudeDo.Data/CLAUDE.md
Normal file
41
src/ClaudeDo.Data/CLAUDE.md
Normal 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
49
src/ClaudeDo.Ui/CLAUDE.md
Normal 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
|
||||
@@ -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}"
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>());
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user