diff --git a/docs/open.md b/docs/open.md index 631d7ae..a63164d 100644 --- a/docs/open.md +++ b/docs/open.md @@ -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). | ✅ | --- diff --git a/src/ClaudeDo.App/App.axaml.cs b/src/ClaudeDo.App/App.axaml.cs index aaed0c0..09bc866 100644 --- a/src/ClaudeDo.App/App.axaml.cs +++ b/src/ClaudeDo.App/App.axaml.cs @@ -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(), + DataContext = services.GetRequiredService(), }; // Kick off the SignalR retry loop — reconnects indefinitely if the worker // is not up yet, or goes down and comes back. - _ = Services.GetRequiredService().StartAsync(); + _ = services.GetRequiredService().StartAsync(); } base.OnFrameworkInitializationCompleted(); diff --git a/src/ClaudeDo.App/Program.cs b/src/ClaudeDo.App/Program.cs index f4b8056..0c68eb0 100644 --- a/src/ClaudeDo.App/Program.cs +++ b/src/ClaudeDo.App/Program.cs @@ -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() + => ConfigureAppBuilder(AppBuilder.Configure()); + + private static AppBuilder ConfigureAppBuilder(AppBuilder builder) + => builder .UsePlatformDetect() #if DEBUG .WithDeveloperTools() diff --git a/src/ClaudeDo.Worker/Runner/TaskRunner.cs b/src/ClaudeDo.Worker/Runner/TaskRunner.cs index 5c573a8..6646e31 100644 --- a/src/ClaudeDo.Worker/Runner/TaskRunner.cs +++ b/src/ClaudeDo.Worker/Runner/TaskRunner.cs @@ -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 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 RunOnceAsync( string taskId, string taskTitle, string slot, string runDir, ClaudeRunConfig config, int runNumber, bool isRetry, string prompt, CancellationToken ct)