refactor(ipc): declare the SignalR wire DTOs once in ClaudeDo.Data

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 <Using> item, so a hub signature change is a compile
error instead of a silently dropped JSON field.
This commit is contained in:
mika kuns
2026-08-26 13:55:40 +02:00
parent 39d8241360
commit 8930b564e4
13 changed files with 233 additions and 358 deletions
@@ -63,7 +63,7 @@ public interface IWorkerClient : INotifyPropertyChanged
string? LastApproveTarget { get; }
IReadOnlyList<ActiveTask> GetActiveTasks();
IReadOnlyList<ActiveTaskDto> GetActiveTasks();
Task WakeQueueAsync();
Task RunNowAsync(string taskId);
+4 -177
View File
@@ -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<ActiveTask> ActiveTasks { get; } = new();
public ObservableCollection<ActiveTaskDto> ActiveTasks { get; } = new();
public event Action<string, string, DateTime>? TaskStartedEvent;
public event Action<string, string, string, DateTime>? TaskFinishedEvent;
@@ -77,7 +76,7 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
public string? LastApproveTarget { get; private set; }
public IReadOnlyList<ActiveTask> GetActiveTasks() => ActiveTasks.ToList();
public IReadOnlyList<ActiveTaskDto> 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<int> 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<string>? SessionSkills = null,
List<ModelPresetDto>? 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<string> ConflictFiles, string? ErrorMessage);
public record BaseDirtyWarningDto(int ModifiedCount, int UntrackedCount);
public record SetTaskStatusResultDto(BaseDirtyWarningDto? BaseDirty);
public record MergePreviewDto(string Status, IReadOnlyList<string> ConflictFiles, int ChangedFileCount);
public record MergeTargetsDto(
string DefaultBranch, IReadOnlyList<string> LocalBranches, string DefaultCommitMessage);
public record MergeConflictDocumentsDto(string TaskId, IReadOnlyList<ConflictDocumentDto> Files);
public record ConflictDocumentDto(string Path, bool IsBinary, IReadOnlyList<MergeSegmentDto> 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<string>? SessionSkills = null, string? VerifyCommand = null);
public sealed record UpdateTaskAgentSettingsDto(string TaskId, string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? SessionSkills = null);
public sealed record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPath, int? MaxTurns = null, List<string>? 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<string> Args,
IReadOnlyDictionary<string, string> 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<UsageLimitDto> 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);