refactor: address code smells (run-dir helper, App DI injection)

- TaskRunner: extract worktree-vs-sandbox selection into
  PrepareRunDirectoryAsync so RunAsync reads linearly (a small helper, not
  a Strategy pattern — overkill for a two-way branch).
- App: drop the public static ServiceProvider locator; inject the provider
  via constructor through AppBuilder.Configure(() => new App(services)).
  Parameterless ctor + BuildAvaloniaApp() retained for the XAML designer.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
mika kuns
2026-06-04 11:33:10 +02:00
parent 72a86fc173
commit 3756b81817
4 changed files with 50 additions and 29 deletions

View File

@@ -208,8 +208,8 @@ Diese Punkte standen 2026-04-30 noch als offen/partial und sind verifiziert fert
| Stelle | Issue | Status |
|---|---|---|
| `TaskRunner.cs:70` `if (list.WorkingDir is not null)` | Inline-Verzweigung Worktree vs. Non-Worktree; Strategy-Pattern erst wenn die Methode wächst | |
| `App.axaml.cs:13` `public static ServiceProvider Services` | Service-Locator-Antipattern, toleriert weil nur in `OnFrameworkInitializationCompleted` genutzt | |
| `TaskRunner` Worktree-vs-Sandbox-Branch | In `PrepareRunDirectoryAsync` ausgelagert (kleiner Helper, kein Strategy-Pattern — bei 2 Pfaden Over-Engineering). `RunAsync` liest jetzt linear. | |
| `App.Services` statischer ServiceProvider | Entfernt: `App` bekommt den Provider per Constructor-Injection über `AppBuilder.Configure(() => new App(services))`; parameterloser Ctor + `BuildAvaloniaApp()` bleiben für den Designer. **UI-Start manuell smoke-testen** (kein headless-Lauf möglich). | |
---

View File

@@ -1,3 +1,4 @@
using System;
using Avalonia;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml;
@@ -10,7 +11,12 @@ namespace ClaudeDo.App;
public partial class App : Application
{
public static ServiceProvider Services { get; set; } = null!;
private readonly IServiceProvider? _services;
// Parameterless ctor is required by the XAML previewer / designer.
public App() { }
public App(IServiceProvider services) => _services = services;
public override void Initialize()
{
@@ -21,16 +27,19 @@ public partial class App : Application
{
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{
var services = _services
?? throw new InvalidOperationException("App was constructed without a service provider.");
FocusClearing.Install();
desktop.MainWindow = new MainWindow
{
DataContext = Services.GetRequiredService<IslandsShellViewModel>(),
DataContext = services.GetRequiredService<IslandsShellViewModel>(),
};
// Kick off the SignalR retry loop — reconnects indefinitely if the worker
// is not up yet, or goes down and comes back.
_ = Services.GetRequiredService<WorkerClient>().StartAsync();
_ = services.GetRequiredService<WorkerClient>().StartAsync();
}
base.OnFrameworkInitializationCompleted();

View File

@@ -35,7 +35,6 @@ sealed class Program
SetCurrentProcessExplicitAppUserModelID("ClaudeDo.App");
var services = BuildServices();
App.Services = services;
using (var scope = services.CreateScope())
{
@@ -45,7 +44,7 @@ sealed class Program
try
{
BuildAvaloniaApp()
ConfigureAppBuilder(AppBuilder.Configure(() => new App(services)))
.StartWithClassicDesktopLifetime(args);
}
finally
@@ -58,8 +57,12 @@ sealed class Program
}
}
// Parameterless entry point required by the XAML previewer / designer.
public static AppBuilder BuildAvaloniaApp()
=> AppBuilder.Configure<App>()
=> ConfigureAppBuilder(AppBuilder.Configure<App>());
private static AppBuilder ConfigureAppBuilder(AppBuilder builder)
=> builder
.UsePlatformDetect()
#if DEBUG
.WithDeveloperTools()

View File

@@ -64,29 +64,14 @@ public sealed class TaskRunner
}
// Determine working directory: worktree or sandbox.
WorktreeContext? wtCtx = null;
string runDir;
if (list.WorkingDir is not null)
var prep = await PrepareRunDirectoryAsync(task, list, ct);
if (prep.FailureReason is not null)
{
try
{
wtCtx = await _wtManager.CreateAsync(task, list, ct);
await _broadcaster.WorkerLog($"Created worktree for \"{task.Title}\"", WorkerLogLevel.Info, DateTime.UtcNow);
runDir = wtCtx.WorktreePath;
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create worktree for task {TaskId}", task.Id);
await MarkFailed(task.Id, task.Title, slot, $"Worktree creation failed: {ex.Message}");
return;
}
}
else
{
runDir = Path.Combine(_cfg.SandboxRoot, task.Id);
Directory.CreateDirectory(runDir);
await MarkFailed(task.Id, task.Title, slot, prep.FailureReason);
return;
}
var wtCtx = prep.WtCtx;
var runDir = prep.RunDir!;
var resolvedConfig = await ResolveConfigAsync(task, listConfig, null, ct);
@@ -216,6 +201,30 @@ public sealed class TaskRunner
await _broadcaster.TaskUpdated(taskId);
}
private readonly record struct RunDirResult(string? RunDir, WorktreeContext? WtCtx, string? FailureReason);
private async Task<RunDirResult> PrepareRunDirectoryAsync(TaskEntity task, ListEntity list, CancellationToken ct)
{
if (list.WorkingDir is not null)
{
try
{
var wtCtx = await _wtManager.CreateAsync(task, list, ct);
await _broadcaster.WorkerLog($"Created worktree for \"{task.Title}\"", WorkerLogLevel.Info, DateTime.UtcNow);
return new RunDirResult(wtCtx.WorktreePath, wtCtx, null);
}
catch (Exception ex)
{
_logger.LogError(ex, "Failed to create worktree for task {TaskId}", task.Id);
return new RunDirResult(null, null, $"Worktree creation failed: {ex.Message}");
}
}
var sandboxDir = Path.Combine(_cfg.SandboxRoot, task.Id);
Directory.CreateDirectory(sandboxDir);
return new RunDirResult(sandboxDir, null, null);
}
private async Task<RunResult> RunOnceAsync(
string taskId, string taskTitle, string slot, string runDir, ClaudeRunConfig config,
int runNumber, bool isRetry, string prompt, CancellationToken ct)