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 | | 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 | | | `TaskRunner` Worktree-vs-Sandbox-Branch | In `PrepareRunDirectoryAsync` ausgelagert (kleiner Helper, kein Strategy-Pattern — bei 2 Pfaden Over-Engineering). `RunAsync` liest jetzt linear. | |
| `App.axaml.cs:13` `public static ServiceProvider Services` | Service-Locator-Antipattern, toleriert weil nur in `OnFrameworkInitializationCompleted` genutzt | | | `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;
using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Markup.Xaml; using Avalonia.Markup.Xaml;
@@ -10,7 +11,12 @@ namespace ClaudeDo.App;
public partial class App : Application 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() public override void Initialize()
{ {
@@ -21,16 +27,19 @@ public partial class App : Application
{ {
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop) if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
{ {
var services = _services
?? throw new InvalidOperationException("App was constructed without a service provider.");
FocusClearing.Install(); FocusClearing.Install();
desktop.MainWindow = new MainWindow desktop.MainWindow = new MainWindow
{ {
DataContext = Services.GetRequiredService<IslandsShellViewModel>(), DataContext = services.GetRequiredService<IslandsShellViewModel>(),
}; };
// Kick off the SignalR retry loop — reconnects indefinitely if the worker // Kick off the SignalR retry loop — reconnects indefinitely if the worker
// is not up yet, or goes down and comes back. // is not up yet, or goes down and comes back.
_ = Services.GetRequiredService<WorkerClient>().StartAsync(); _ = services.GetRequiredService<WorkerClient>().StartAsync();
} }
base.OnFrameworkInitializationCompleted(); base.OnFrameworkInitializationCompleted();

View File

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

View File

@@ -64,29 +64,14 @@ public sealed class TaskRunner
} }
// Determine working directory: worktree or sandbox. // Determine working directory: worktree or sandbox.
WorktreeContext? wtCtx = null; var prep = await PrepareRunDirectoryAsync(task, list, ct);
string runDir; if (prep.FailureReason is not null)
if (list.WorkingDir is not null)
{ {
try await MarkFailed(task.Id, task.Title, slot, prep.FailureReason);
{ return;
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);
} }
var wtCtx = prep.WtCtx;
var runDir = prep.RunDir!;
var resolvedConfig = await ResolveConfigAsync(task, listConfig, null, ct); var resolvedConfig = await ResolveConfigAsync(task, listConfig, null, ct);
@@ -216,6 +201,30 @@ public sealed class TaskRunner
await _broadcaster.TaskUpdated(taskId); 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( private async Task<RunResult> RunOnceAsync(
string taskId, string taskTitle, string slot, string runDir, ClaudeRunConfig config, string taskId, string taskTitle, string slot, string runDir, ClaudeRunConfig config,
int runNumber, bool isRetry, string prompt, CancellationToken ct) int runNumber, bool isRetry, string prompt, CancellationToken ct)