From 8930b564e4f7d5693ad841d24bd2ff26bc9ae333 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Wed, 26 Aug 2026 13:55:40 +0200 Subject: [PATCH] refactor(ipc): declare the SignalR wire DTOs once in ClaudeDo.Data MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit WorkerHub and IWorkerClient each carried their own copy of every record and had already drifted — MergePreviewDto lost its verify fields on the client side, two records disagreed on their name. They now live in Data/Wire.cs and reach both sides via a item, so a hub signature change is a compile error instead of a silently dropped JSON field. --- src/ClaudeDo.Data/Wire.cs | 200 ++++++++++++++++++ src/ClaudeDo.Ui/CLAUDE.md | 9 +- src/ClaudeDo.Ui/ClaudeDo.Ui.csproj | 6 +- .../Services/Interfaces/IWorkerClient.cs | 2 +- src/ClaudeDo.Ui/Services/WorkerClient.cs | 181 +--------------- src/ClaudeDo.Worker/CLAUDE.md | 5 +- src/ClaudeDo.Worker/ClaudeDo.Worker.csproj | 4 + src/ClaudeDo.Worker/Hub/WorkerHub.cs | 174 +-------------- .../ClaudeDo.Ui.Tests.csproj | 1 + tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs | 2 +- .../MissionControlViewModelTests.cs | 4 +- .../ClaudeDo.Worker.Tests.csproj | 1 + .../UiVm/TasksIslandViewModelPlanningTests.cs | 2 +- 13 files changed, 233 insertions(+), 358 deletions(-) create mode 100644 src/ClaudeDo.Data/Wire.cs diff --git a/src/ClaudeDo.Data/Wire.cs b/src/ClaudeDo.Data/Wire.cs new file mode 100644 index 00000000..653373ef --- /dev/null +++ b/src/ClaudeDo.Data/Wire.cs @@ -0,0 +1,200 @@ +using ClaudeDo.Data.Models; +using TaskStatus = ClaudeDo.Data.Models.TaskStatus; + +namespace ClaudeDo.Data.Wire; + +// The SignalR contract between WorkerHub (server) and WorkerClient (client). Both used to +// declare their own copy of every record in this file; they drifted (MergePreviewDto lost its +// verify fields on the client side, two records disagreed on their name). One definition, so +// a hub signature change is a compile error instead of a silently dropped JSON field. +// +// Imported project-wide via a item in the Ui, Worker +// and their test projects — that's why nothing here needs a per-file using. + +public record ActiveTaskDto(string Slot, string TaskId, DateTime StartedAt); + +public record WorkerBuildInfoDto(string? BuildSha); + +public record AppSettingsDto( + string DefaultClaudeInstructions, + string DefaultModel, + int DefaultMaxTurns, + string DefaultPermissionMode, + int MaxParallelExecutions, + string WorktreeStrategy, + string? CentralWorktreeRoot, + bool WorktreeAutoCleanupEnabled, + int WorktreeAutoCleanupDays, + string? ReportExcludedPaths, + int StandupWeekday, + int DailyPrepMaxTasks, + List? SessionSkills = null, + List? ModelPresets = null, + int UsageGateFiveHourPct = 80, + int UsageGateSevenDayPct = 90, + int MaxTurnsCeiling = 80, + // Throttle stages per bucket — dragged on the usage-monitor gauges, not typed in Settings. + int UsageThrottleFiveHourSoftPct = 50, + int UsageThrottleFiveHourHardPct = 65, + int UsageThrottleSevenDaySoftPct = 50, + int UsageThrottleSevenDayHardPct = 65, + bool AutoContinueOnUsageLimit = false); + +public record ModelPresetDto(string Model, string Effort, int MaxTurns); + +public record SessionSkillDto( + string Name, + string Description, + string SourceUrl, + string PinnedRef, + DateTimeOffset AddedAt); + +public record WorktreeCleanupDto(int Removed); + +public record WorktreeResetDto(int Removed, int TasksAffected, bool Blocked, int RunningTasks); + +public record WorktreeOverviewDto( + string TaskId, + string TaskTitle, + TaskStatus TaskStatus, + string ListId, + string ListName, + string Path, + string BranchName, + string BaseCommit, + WorktreeState State, + string? DiffStat, + DateTime CreatedAt, + bool PathExistsOnDisk); + +public record ForceRemoveResultDto(bool Removed, string? Reason); + +public record PlanningMergeConflictStateDto(string PlanningTaskId, string SubtaskId); + +public record PendingQuestionDto(string TaskId, string QuestionId, string Question); + +public record MergeResultDto(string Status, IReadOnlyList ConflictFiles, string? ErrorMessage); + +public record BaseDirtyWarningDto(int ModifiedCount, int UntrackedCount); + +public record SetTaskStatusResultDto(BaseDirtyWarningDto? BaseDirty); + +public record MergePreviewDto( + string Status, IReadOnlyList ConflictFiles, int ChangedFileCount, + int? VerifyExitCode = null, long? VerifyDurationMs = null, string? VerifyOutputTail = null); + +public record MergeTargetsDto( + string DefaultBranch, IReadOnlyList LocalBranches, string DefaultCommitMessage); + +public record MergeConflictDocumentsDto(string TaskId, IReadOnlyList Files); + +public record ConflictDocumentDto(string Path, bool IsBinary, IReadOnlyList Segments); + +public record MergeSegmentDto(bool IsConflict, string Text, string Ours, string? Base, string Theirs); + +public record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType, bool IsManual = false, bool FindingsTracked = false); + +public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List? SessionSkills = null, string? VerifyCommand = null); + +public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List? SessionSkills = null); + +public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List? SessionSkills = null, string? VerifyCommand = null); + +public record SeedResultDto(int Copied, int Skipped); + +public record OnlineInboxStateDto( + bool Enabled, + string ApiBaseUrl, + string Authority, + string ClientId, + string Scopes, + string RedirectUri, + bool SignedIn, + int PollIntervalSeconds); + +public record OnlineInboxConfigInputDto( + bool Enabled, + string ApiBaseUrl, + int PollIntervalSeconds, + string Authority, + string ClientId, + string Scopes, + string RedirectUri); + +public record UsageLimitDto( + string Kind, + string Group, + double Percent, + string Severity, + DateTimeOffset? ResetsAt, + string? ScopeModelDisplayName, + bool IsActive); + +public record UsageSnapshotDto( + double? FiveHourPercent, + DateTimeOffset? FiveHourResetsAt, + double? SevenDayPercent, + DateTimeOffset? SevenDayResetsAt, + IReadOnlyList Limits, + int FiveHourThresholdPct, + int SevenDayThresholdPct, + bool IsGateBlocked, + string? GateReason, + DateTime? FetchedAtUtc, + bool IsStale, + string? LastError, + int ConfiguredSlots, + int EffectiveSlots, + string? ThrottleBucket, + // Throttle stages per bucket, drawn (and dragged) on the usage-monitor gauges. Defaults match + // the DB defaults so an older worker that doesn't send them yet still yields sane markers. + int ThrottleFiveHourSoftPct = 50, + int ThrottleFiveHourHardPct = 65, + int ThrottleSevenDaySoftPct = 50, + int ThrottleSevenDayHardPct = 65); + +public record ModelUsageRowDto( + DateOnly Date, + string Model, + string Scope, + long InputTokens, + long OutputTokens, + long CacheReadTokens, + long CacheCreationTokens, + int Messages, + double? CostUsd = null); + +public record TaskUsageRowDto( + string TaskId, + string TaskTitle, + string ListId, + string ListName, + string? Model, + int Runs, + long TokensIn, + long TokensOut, + double? CostUsd = null, + int? Retries = null, + bool? Productive = null, + bool? OneShot = null); + +public record TokenTrackerDashboardDto(bool Ok, string? Url, string? Error); + +public record TokenTrackerStatusDto( + bool Installed, + string? Version, + bool NodeOk, + string? NodeVersion, + DateTime? LastFetchedUtc, + string? LastError, + int? FormatVersion, + int SessionCount); + +// What an embedded ConPTY terminal (UI process) needs to start a real `claude` process for +// a task's worktree, with the same setup as an autonomous run (session-skills seeded onto +// disk, the same run environment variables) plus the --resume-vs-fresh-start choice. +public sealed record LaunchSpec( + string Cwd, + string Exe, + IReadOnlyList Args, + IReadOnlyDictionary Env); diff --git a/src/ClaudeDo.Ui/CLAUDE.md b/src/ClaudeDo.Ui/CLAUDE.md index 41d06265..48ec46fe 100644 --- a/src/ClaudeDo.Ui/CLAUDE.md +++ b/src/ClaudeDo.Ui/CLAUDE.md @@ -84,7 +84,7 @@ new editor boilerplate there rather than copying it a third time. ## Services -- **WorkerClient / IWorkerClient** — SignalR client on `http://127.0.0.1:47821/hub`, auto-reconnect with exponential backoff. The surface **tracks `WorkerHub`** — treat `src/ClaudeDo.Worker/Hub/WorkerHub.cs` as the canonical method list rather than duplicating it here. Events mirror `HubBroadcaster`. Lifecycle (`StartAsync`/`StopAsync`) and a few admin methods live only on the concrete `WorkerClient`. +- **WorkerClient / IWorkerClient** — SignalR client on `http://127.0.0.1:47821/hub`, auto-reconnect with exponential backoff. The surface **tracks `WorkerHub`** — treat `src/ClaudeDo.Worker/Hub/WorkerHub.cs` as the canonical method list rather than duplicating it here. The wire DTOs are **not** declared here: they live once in `ClaudeDo.Data/Wire.cs` and reach both sides via a `` item in the csproj (Ui, Worker, and both test projects), so a hub signature change is a compile error instead of a silently dropped JSON field. Add new ones there, never a local copy. Events mirror `HubBroadcaster`. Lifecycle (`StartAsync`/`StopAsync`) and a few admin methods live only on the concrete `WorkerClient`. - **INotesApi / WorkerNotesApi** — daily-note CRUD; UI DTO `DailyNoteDto(Id, Date, Text, SortOrder)`. - **IPrimeScheduleApi** — prime-schedule CRUD. - **UpdateCheckService** — polls releases; `LastCheckStatus`/`LatestVersion`/`CheckNowAsync` feed the shell's update banner. @@ -95,8 +95,11 @@ new editor boilerplate there rather than copying it a third time. ## Converters In `Converters/` — grep rather than list: log-level brush, dot brush, -status equality, icon key, strike/italic/opacity toggles, null→bool, -uppercase. +status equality, icon key, `KeepLastNumber`. The one-expression ones +(strike/italic/opacity toggles, uppercase, not-a-header-row) share +`SimpleConverters.cs`; put new trivial ones there instead of a new file. +There is no null→bool converter — bind +`Converter={x:Static conv:ObjectConverters.IsNotNull}` (Avalonia ships it). ## Dialog Pattern diff --git a/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj b/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj index 57689421..5492f80a 100644 --- a/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj +++ b/src/ClaudeDo.Ui/ClaudeDo.Ui.csproj @@ -1,4 +1,4 @@ - + @@ -31,6 +31,10 @@ true + + + + diff --git a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs index 62b9cce8..bfbb19c8 100644 --- a/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/Interfaces/IWorkerClient.cs @@ -63,7 +63,7 @@ public interface IWorkerClient : INotifyPropertyChanged string? LastApproveTarget { get; } - IReadOnlyList GetActiveTasks(); + IReadOnlyList GetActiveTasks(); Task WakeQueueAsync(); Task RunNowAsync(string taskId); diff --git a/src/ClaudeDo.Ui/Services/WorkerClient.cs b/src/ClaudeDo.Ui/Services/WorkerClient.cs index c0a3ab0d..d83b5413 100644 --- a/src/ClaudeDo.Ui/Services/WorkerClient.cs +++ b/src/ClaudeDo.Ui/Services/WorkerClient.cs @@ -10,7 +10,6 @@ using Microsoft.Extensions.DependencyInjection; namespace ClaudeDo.Ui.Services; -public record ActiveTask(string Slot, string TaskId, DateTime StartedAt); public sealed record WorkerLogEntry(string Message, WorkerLogLevel Level, DateTime TimestampUtc); sealed class IndefiniteRetryPolicy : IRetryPolicy @@ -41,7 +40,7 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC [ObservableProperty] private bool _isReconnecting; - public ObservableCollection ActiveTasks { get; } = new(); + public ObservableCollection ActiveTasks { get; } = new(); public event Action? TaskStartedEvent; public event Action? TaskFinishedEvent; @@ -77,7 +76,7 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC public string? LastApproveTarget { get; private set; } - public IReadOnlyList GetActiveTasks() => ActiveTasks.ToList(); + public IReadOnlyList GetActiveTasks() => ActiveTasks.ToList(); public WorkerClient(string signalRUrl) { @@ -118,7 +117,7 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC { Dispatcher.UIThread.Post(() => { - ActiveTasks.Add(new ActiveTask(slot, taskId, startedAt)); + ActiveTasks.Add(new ActiveTaskDto(slot, taskId, startedAt)); TaskStartedEvent?.Invoke(slot, taskId, startedAt); }); }); @@ -417,7 +416,7 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC { ActiveTasks.Clear(); foreach (var a in active) - ActiveTasks.Add(new ActiveTask(a.Slot, a.TaskId, a.StartedAt)); + ActiveTasks.Add(new ActiveTaskDto(a.Slot, a.TaskId, a.StartedAt)); }); } catch (HubException) @@ -712,177 +711,5 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC => await FinalizePlanningSessionAsync(taskId, queueAgentTasks, ct); async Task IWorkerClient.GetPendingDraftCountAsync(string taskId, CancellationToken ct) => await GetPendingDraftCountAsync(taskId, ct); - - // DTOs for deserializing hub responses - private sealed class ActiveTaskDto - { - public string Slot { get; set; } = ""; - public string TaskId { get; set; } = ""; - public DateTime StartedAt { get; set; } - } } -public sealed record AppSettingsDto( - string DefaultClaudeInstructions, - string DefaultModel, - int DefaultMaxTurns, - string DefaultPermissionMode, - int MaxParallelExecutions, - string WorktreeStrategy, - string? CentralWorktreeRoot, - bool WorktreeAutoCleanupEnabled, - int WorktreeAutoCleanupDays, - string? ReportExcludedPaths, - int StandupWeekday, - int DailyPrepMaxTasks, - List? SessionSkills = null, - List? ModelPresets = null, - int UsageGateFiveHourPct = 80, - int UsageGateSevenDayPct = 90, - int MaxTurnsCeiling = 80, - // Throttle stages per bucket — dragged on the usage-monitor gauges, not typed in Settings. - int UsageThrottleFiveHourSoftPct = 50, - int UsageThrottleFiveHourHardPct = 65, - int UsageThrottleSevenDaySoftPct = 50, - int UsageThrottleSevenDayHardPct = 65, - bool AutoContinueOnUsageLimit = false); - -// Per-model run defaults (effort + turn budget) edited in Settings → General. -public sealed record ModelPresetDto(string Model, string Effort, int MaxTurns); - -public sealed record SessionSkillDto( - string Name, - string Description, - string SourceUrl, - string PinnedRef, - DateTimeOffset AddedAt); - -public sealed record WorktreeCleanupDto(int Removed); -public sealed record WorktreeResetDto(int Removed, int TasksAffected, bool Blocked, int RunningTasks); -public record MergeResultDto(string Status, IReadOnlyList ConflictFiles, string? ErrorMessage); -public record BaseDirtyWarningDto(int ModifiedCount, int UntrackedCount); -public record SetTaskStatusResultDto(BaseDirtyWarningDto? BaseDirty); -public record MergePreviewDto(string Status, IReadOnlyList ConflictFiles, int ChangedFileCount); -public record MergeTargetsDto( - string DefaultBranch, IReadOnlyList LocalBranches, string DefaultCommitMessage); -public record MergeConflictDocumentsDto(string TaskId, IReadOnlyList Files); -public record ConflictDocumentDto(string Path, bool IsBinary, IReadOnlyList Segments); -public record MergeSegmentDto(bool IsConflict, string Text, string Ours, string? Base, string Theirs); -public sealed record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType, bool IsManual = false, bool FindingsTracked = false); -public sealed record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List? SessionSkills = null, string? VerifyCommand = null); -public sealed record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List? SessionSkills = null); -public sealed record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List? SessionSkills = null, string? VerifyCommand = null); -public sealed record SeedResultDto(int Copied, int Skipped); - -public sealed record WorktreeOverviewDto( - string TaskId, - string TaskTitle, - ClaudeDo.Data.Models.TaskStatus TaskStatus, - string ListId, - string ListName, - string Path, - string BranchName, - string BaseCommit, - WorktreeState State, - string? DiffStat, - DateTime CreatedAt, - bool PathExistsOnDisk); - -public sealed record LaunchSpec( - string Cwd, - string Exe, - IReadOnlyList Args, - IReadOnlyDictionary Env); - -public sealed record ForceRemoveResultDto(bool Removed, string? Reason); -public sealed record PlanningMergeConflictStateDto(string PlanningTaskId, string SubtaskId); -public sealed record PendingQuestionDto(string TaskId, string QuestionId, string Question); -public sealed record WorkerBuildInfoDto(string? BuildSha); - -public sealed record OnlineInboxStateDto( - bool Enabled, - string ApiBaseUrl, - string Authority, - string ClientId, - string Scopes, - string RedirectUri, - bool SignedIn, - int PollIntervalSeconds); - -public sealed record OnlineInboxConfigInputDto( - bool Enabled, - string ApiBaseUrl, - int PollIntervalSeconds, - string Authority, - string ClientId, - string Scopes, - string RedirectUri); - -public sealed record UsageLimitDto( - string Kind, - string Group, - double Percent, - string Severity, - DateTimeOffset? ResetsAt, - string? ScopeModelDisplayName, - bool IsActive); - -public sealed record UsageSnapshotDto( - double? FiveHourPercent, - DateTimeOffset? FiveHourResetsAt, - double? SevenDayPercent, - DateTimeOffset? SevenDayResetsAt, - IReadOnlyList Limits, - int FiveHourThresholdPct, - int SevenDayThresholdPct, - bool IsGateBlocked, - string? GateReason, - DateTime? FetchedAtUtc, - bool IsStale, - string? LastError, - int ConfiguredSlots, - int EffectiveSlots, - string? ThrottleBucket, - // Throttle stages per bucket, drawn (and dragged) on the usage-monitor gauges. Defaults match - // the DB defaults so an older worker that doesn't send them yet still yields sane markers. - int ThrottleFiveHourSoftPct = 50, - int ThrottleFiveHourHardPct = 65, - int ThrottleSevenDaySoftPct = 50, - int ThrottleSevenDayHardPct = 65); - -public sealed record ModelUsageRowDto( - DateOnly Date, - string Model, - string Scope, - long InputTokens, - long OutputTokens, - long CacheReadTokens, - long CacheCreationTokens, - int Messages, - double? CostUsd = null); - -public sealed record TaskUsageRowDto( - string TaskId, - string TaskTitle, - string ListId, - string ListName, - string? Model, - int Runs, - long TokensIn, - long TokensOut, - double? CostUsd = null, - int? Retries = null, - bool? Productive = null, - bool? OneShot = null); - -public sealed record TokenTrackerDashboardDto(bool Ok, string? Url, string? Error); - -public sealed record TokenTrackerStatusDto( - bool Installed, - string? Version, - bool NodeOk, - string? NodeVersion, - DateTime? LastFetchedUtc, - string? LastError, - int? FormatVersion, - int SessionCount); diff --git a/src/ClaudeDo.Worker/CLAUDE.md b/src/ClaudeDo.Worker/CLAUDE.md index 5b5036e2..2fa0ef39 100644 --- a/src/ClaudeDo.Worker/CLAUDE.md +++ b/src/ClaudeDo.Worker/CLAUDE.md @@ -177,7 +177,10 @@ Claude then calls `get_daily_prep_candidates`, picks an effort-aware subset capp ## SignalR Hub -`WorkerHub` is the canonical method list — grep it rather than trusting a doc inventory. +`WorkerHub` is the canonical method list — grep it rather than trusting a doc inventory. Its wire +DTOs live in `ClaudeDo.Data/Wire.cs` (shared with `IWorkerClient` via a `` item), **not** in +`WorkerHub.cs` — don't re-declare one locally, that's how `MergePreviewDto` lost its verify fields +on the client side. Groups: execution · review/merge · conflict resolver · planning sessions · interactive ConPTY launch specs · worktrees · agents/settings/lists · reports/notes/prep · diagnostics · usage. `IWorkerClient` in `ClaudeDo.Ui` mirrors it. diff --git a/src/ClaudeDo.Worker/ClaudeDo.Worker.csproj b/src/ClaudeDo.Worker/ClaudeDo.Worker.csproj index 9c7497d6..c95498cf 100644 --- a/src/ClaudeDo.Worker/ClaudeDo.Worker.csproj +++ b/src/ClaudeDo.Worker/ClaudeDo.Worker.csproj @@ -27,6 +27,10 @@ ClaudeTaskWorker.ico + + + + diff --git a/src/ClaudeDo.Worker/Hub/WorkerHub.cs b/src/ClaudeDo.Worker/Hub/WorkerHub.cs index c1bb25ba..d10bc8cb 100644 --- a/src/ClaudeDo.Worker/Hub/WorkerHub.cs +++ b/src/ClaudeDo.Worker/Hub/WorkerHub.cs @@ -28,174 +28,6 @@ using Microsoft.EntityFrameworkCore; namespace ClaudeDo.Worker.Hub; -public record ActiveTaskDto(string Slot, string TaskId, DateTime StartedAt); - -/// Git SHA the running worker was built from. Null when the build wasn't stamped -/// (e.g. a local dev build without the SourceRevisionId target). -public record WorkerBuildInfoDto(string? BuildSha); - -public record AppSettingsDto( - string DefaultClaudeInstructions, - string DefaultModel, - int DefaultMaxTurns, - string DefaultPermissionMode, - int MaxParallelExecutions, - string WorktreeStrategy, - string? CentralWorktreeRoot, - bool WorktreeAutoCleanupEnabled, - int WorktreeAutoCleanupDays, - string? ReportExcludedPaths, - int StandupWeekday, - int DailyPrepMaxTasks, - List? SessionSkills = null, - List? ModelPresets = null, - int UsageGateFiveHourPct = 80, - int UsageGateSevenDayPct = 90, - int MaxTurnsCeiling = 80, - // Throttle stages per bucket — dragged on the usage-monitor gauges, not typed in Settings. - int UsageThrottleFiveHourSoftPct = 50, - int UsageThrottleFiveHourHardPct = 65, - int UsageThrottleSevenDaySoftPct = 50, - int UsageThrottleSevenDayHardPct = 65, - bool AutoContinueOnUsageLimit = false); - -// Per-model run defaults (effort + turn budget) edited in Settings -> General. -public record ModelPresetDto(string Model, string Effort, int MaxTurns); - -public record SessionSkillDto( - string Name, - string Description, - string SourceUrl, - string PinnedRef, - DateTimeOffset AddedAt); - -public record WorktreeCleanupDto(int Removed); -public record WorktreeResetDto(int Removed, int TasksAffected, bool Blocked, int RunningTasks); - -public record WorktreeOverviewDto( - string TaskId, - string TaskTitle, - ClaudeDo.Data.Models.TaskStatus TaskStatus, - string ListId, - string ListName, - string Path, - string BranchName, - string BaseCommit, - WorktreeState State, - string? DiffStat, - DateTime CreatedAt, - bool PathExistsOnDisk); - -public record ForceRemoveResultDto(bool Removed, string? Reason); -public record PlanningMergeConflictStateDto(string PlanningTaskId, string SubtaskId); -public record PendingQuestionDto(string TaskId, string QuestionId, string Question); -public record MergeResultDto(string Status, IReadOnlyList ConflictFiles, string? ErrorMessage); -public record BaseDirtyWarningDto(int ModifiedCount, int UntrackedCount); -public record SetTaskStatusResultDto(BaseDirtyWarningDto? BaseDirty); -// Verify* fields are always null on this path today -- the UI's live mergeability indicator never -// requests a verify run (that would mean an unrequested build on every preview poll); they exist so -// MergePreviewDto matches TaskMergeService.MergePreviewResult should a caller opt in later. -public record MergePreviewDto( - string Status, IReadOnlyList ConflictFiles, int ChangedFileCount, - int? VerifyExitCode = null, long? VerifyDurationMs = null, string? VerifyOutputTail = null); -public record MergeTargetsDto( - string DefaultBranch, IReadOnlyList LocalBranches, string DefaultCommitMessage); -public record MergeConflictDocumentsDto(string TaskId, IReadOnlyList Files); -public record ConflictDocumentDto(string Path, bool IsBinary, IReadOnlyList Segments); -public record MergeSegmentDto(bool IsConflict, string Text, string Ours, string? Base, string Theirs); -public record UpdateListDto(string Id, string Name, string? WorkingDir, string DefaultCommitType, bool IsManual = false, bool FindingsTracked = false); -public record UpdateListConfigDto(string ListId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List? SessionSkills = null, string? VerifyCommand = null); -public record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List? SessionSkills = null); -public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List? SessionSkills = null, string? VerifyCommand = null); -public record SeedResultDto(int Copied, int Skipped); - -public record OnlineInboxStateDto( - bool Enabled, - string ApiBaseUrl, - string Authority, - string ClientId, - string Scopes, - string RedirectUri, - bool SignedIn, - int PollIntervalSeconds); - -public record OnlineInboxConfigInput( - bool Enabled, - string ApiBaseUrl, - int PollIntervalSeconds, - string Authority, - string ClientId, - string Scopes, - string RedirectUri); - -public record UsageLimitDto( - string Kind, - string Group, - double Percent, - string Severity, - DateTimeOffset? ResetsAt, - string? ScopeModelDisplayName, - bool IsActive); - -public record UsageSnapshotDto( - double? FiveHourPercent, - DateTimeOffset? FiveHourResetsAt, - double? SevenDayPercent, - DateTimeOffset? SevenDayResetsAt, - IReadOnlyList Limits, - int FiveHourThresholdPct, - int SevenDayThresholdPct, - bool IsGateBlocked, - string? GateReason, - DateTime? FetchedAtUtc, - bool IsStale, - string? LastError, - int ConfiguredSlots, - int EffectiveSlots, - string? ThrottleBucket, - // Throttle stages per bucket, so the usage monitor can draw (and drag) them on each gauge. - int ThrottleFiveHourSoftPct, - int ThrottleFiveHourHardPct, - int ThrottleSevenDaySoftPct, - int ThrottleSevenDayHardPct); - -public record ModelUsageRowDto( - DateOnly Date, - string Model, - string Scope, - long InputTokens, - long OutputTokens, - long CacheReadTokens, - long CacheCreationTokens, - int Messages, - double? CostUsd = null); - -public record TaskUsageRowDto( - string TaskId, - string TaskTitle, - string ListId, - string ListName, - string? Model, - int Runs, - long TokensIn, - long TokensOut, - double? CostUsd = null, - int? Retries = null, - bool? Productive = null, - bool? OneShot = null); - -public record TokenTrackerDashboardDto(bool Ok, string? Url, string? Error); - -public record TokenTrackerStatusDto( - bool Installed, - string? Version, - bool NodeOk, - string? NodeVersion, - DateTime? LastFetchedUtc, - string? LastError, - int? FormatVersion, - int SessionCount); - public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub { private static readonly string Version = @@ -240,7 +72,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub private readonly Runner.PendingQuestionRegistry _pendingQuestions; private readonly LogRingBuffer? _logBuffer; private readonly ISessionSkillRegistry _skillRegistry; - private readonly IInteractiveLaunchSpecService? _interactiveLaunchSpec; + private readonly InteractiveLaunchSpecService? _interactiveLaunchSpec; private readonly WorktreeManager? _worktreeManager; private readonly Data.Git.GitService? _git; private readonly UsageSnapshotBuilder? _usageSnapshotBuilder; @@ -275,7 +107,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub Runner.PendingQuestionRegistry pendingQuestions, ISessionSkillRegistry skillRegistry, LogRingBuffer? logBuffer = null, - IInteractiveLaunchSpecService? interactiveLaunchSpec = null, + InteractiveLaunchSpecService? interactiveLaunchSpec = null, WorktreeManager? worktreeManager = null, Data.Git.GitService? git = null, UsageSnapshotBuilder? usageSnapshotBuilder = null, @@ -1162,7 +994,7 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub _onlineInboxConfig.PollIntervalSeconds); } - public void SetOnlineInboxConfig(OnlineInboxConfigInput input) + public void SetOnlineInboxConfig(OnlineInboxConfigInputDto input) { _onlineInboxConfig.Enabled = input.Enabled; _onlineInboxConfig.ApiBaseUrl = input.ApiBaseUrl ?? ""; diff --git a/tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj b/tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj index 3cfb7ab3..bfe36982 100644 --- a/tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj +++ b/tests/ClaudeDo.Ui.Tests/ClaudeDo.Ui.Tests.csproj @@ -8,6 +8,7 @@ + diff --git a/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs b/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs index e13c30ea..69ff19a1 100644 --- a/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs +++ b/tests/ClaudeDo.Ui.Tests/StubWorkerClient.cs @@ -45,7 +45,7 @@ public abstract class StubWorkerClient : IWorkerClient public int ClearMyDayCalls { get; private set; } public int RunDailyPrepNowCalls { get; private set; } - public virtual IReadOnlyList GetActiveTasks() => System.Array.Empty(); + public virtual IReadOnlyList GetActiveTasks() => System.Array.Empty(); public void RaiseTaskStarted(string slot, string taskId, DateTime startedAt) => TaskStartedEvent?.Invoke(slot, taskId, startedAt); public void RaiseTaskFinished(string slot, string taskId, string status, DateTime finishedAt) => TaskFinishedEvent?.Invoke(slot, taskId, status, finishedAt); diff --git a/tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlViewModelTests.cs b/tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlViewModelTests.cs index 70435e0b..6eb06163 100644 --- a/tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlViewModelTests.cs +++ b/tests/ClaudeDo.Ui.Tests/ViewModels/MissionControlViewModelTests.cs @@ -94,8 +94,8 @@ public class MissionControlViewModelTests : IDisposable private sealed class SeededFakeWorker : StubWorkerClient { - public override IReadOnlyList GetActiveTasks() - => new[] { new ActiveTask("slot-1", "seed1", DateTime.UtcNow) }; + public override IReadOnlyList GetActiveTasks() + => new[] { new ActiveTaskDto("slot-1", "seed1", DateTime.UtcNow) }; } // ── acceptance criterion (b): RefreshQueueAsync returns Running rows first ─ diff --git a/tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj b/tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj index bfa4f500..c7d07e4d 100644 --- a/tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj +++ b/tests/ClaudeDo.Worker.Tests/ClaudeDo.Worker.Tests.csproj @@ -19,6 +19,7 @@ + diff --git a/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs b/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs index 025b4f99..6bd31701 100644 --- a/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs +++ b/tests/ClaudeDo.Worker.Tests/UiVm/TasksIslandViewModelPlanningTests.cs @@ -163,7 +163,7 @@ sealed class FakeWorkerClient : IWorkerClient public Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input) => Task.CompletedTask; public Task SetOnlineInboxAuthAsync(string refreshToken) => Task.CompletedTask; public Task ClearOnlineInboxAuthAsync() => Task.CompletedTask; - public IReadOnlyList GetActiveTasks() => System.Array.Empty(); + public IReadOnlyList GetActiveTasks() => System.Array.Empty(); public Task GetUsageSnapshotAsync() => Task.FromResult(null); public Task RefreshUsageAsync() => Task.FromResult(null);