diff --git a/docs/open.md b/docs/open.md index 7a6500b0..5cc8d5df 100644 --- a/docs/open.md +++ b/docs/open.md @@ -140,33 +140,38 @@ verifiziert**: ## Offene Verifikation (2026-08-05, Environment Checks / SystemCheckPage) -**Voraussetzung, bevor irgendeiner der Punkte unten geprüft werden kann:** die Branches -`claudedo/06aca9b3afec4b939f59b627bfe21737` (Checks + SystemCheckPage) und -`claudedo/40272c0bb3b14562b59c022d09c382b6` (ExecutableResolver-Wiring in -`ClaudeDo.Worker`) müssen erst gemerged werden — sie lagen bei dieser Abschluss-Session -noch nicht auf `main`. Build/Test-Nachweis unten stammt aus einer lokalen Scratch-Integration -beider Branches, nicht aus `main` selbst. Details → `installer-preflight` in -`docs/explore-notes/README.md` und den neuen Abschnitt „Environment Checks" in -`src/ClaudeDo.Installer/CLAUDE.md`. +Checks + SystemCheckPage (`claudedo/06aca9b3…`) und das ExecutableResolver-Wiring im Worker +(`claudedo/40272c0b…`) sind seit 2026-08-06 auf `main` gemerged; die frühere „erst mergen"- +Voraussetzung ist erledigt. Details → `installer-preflight` in `docs/explore-notes/README.md` +und der Abschnitt „Environment Checks" in `src/ClaudeDo.Installer/CLAUDE.md`. -**Update (2026-08-06):** die Diagnose-Sektion (Config-Modus/`SettingsWindow`) ist implementiert -— `Pages/DiagnosePage/` + geteilte `Checks/CheckListViewModel.cs`/`Checks/CheckListView.xaml` -(auch von `SystemCheckPage` genutzt, keine zweite Implementierung). Unit-getestet -(`tests/ClaudeDo.Installer.Tests/Pages/DiagnosePage/DiagnosePageViewModelTests.cs`), **aber -nicht visuell verifiziert** — siehe Punkt unten. Der „Claude Help Me"-Button bleibt offen (sein -Footer-Slot ist in `CheckListView.xaml` reserviert): +**Update (2026-08-06):** beide Folge-Features sind jetzt implementiert und auf `main`. -- [ ] „Claude Help Me" öffnet ein Terminal mit laufender Claude-Session, und die Session hat - den Diagnose-Report tatsächlich gelesen — **nicht umsetzbar, Feature existiert nicht.** -- [ ] Der Help-Me-Button ist korrekt deaktiviert, wenn `claude` nicht im PATH ist, mit - verständlichem Tooltip — **nicht umsetzbar, Feature existiert nicht.** +- Diagnose-Sektion (Config-Modus/`SettingsWindow`): `Pages/DiagnosePage/` + geteilte + `Checks/CheckListViewModel.cs`/`Checks/CheckListView.xaml` (auch von `SystemCheckPage` + genutzt, keine zweite Implementierung). Unit-getestet + (`tests/ClaudeDo.Installer.Tests/Pages/DiagnosePage/DiagnosePageViewModelTests.cs`). +- „Claude Help Me"-Button: `Core/ClaudeHelpLauncher.cs` + `SystemCheckPageViewModel`/-View, + unit-getestet (`tests/ClaudeDo.Installer.Tests/Core/ClaudeHelpLauncherTests.cs`). + +Beides **nicht visuell verifiziert**: + +- [x] Help-Me-Button ist deaktiviert, wenn `claude-cli` nicht `Ok` ist oder `claude-auth` + `Failed` ist (bleibt aktiv bei `Unknown`), mit erklärendem Tooltip — unit-getestet. +- [ ] „Claude Help Me" öffnet tatsächlich ein Terminal mit laufender Claude-Session, und die + Session hat den Diagnose-Report gelesen — **nicht verifiziert** (der eigentliche + Terminal-Start/`wt.exe`-Zusammenspiel und die Session-Qualität sind nur über die + injizierte `IProcessLauncher`-Fake getestet, nie mit einem echten Terminal/CLI). +- [ ] Platzierung des Help-Me-Buttons: er sitzt seit dem Merge der Diagnose-Sektion in einer + **eigenen Zeile unter** dem geteilten Check-Listen-Footer (der reservierte Slot *im* + Footer entfiel mit der Extraktion nach `CheckListView.xaml`). Optisch prüfen, ob das + so bleiben soll oder ob der Button in den geteilten Footer gehört. - [ ] Diagnose-Sektion im Config-Modus: Öffnen von SettingsWindow löst keinen Prüflauf aus, Klick auf „Erneut prüfen" schon; zeigt die echten installierten Pfade/Ports (nicht die InstallContext-Defaults), und der laufende Worker auf dem konfigurierten SignalR-Port gilt nicht als Konflikt — **unit-verifiziert, visueller Durchlauf noch offen.** -Sobald die beiden Branches oben gemerged sind, sind folgende Punkte real prüfbar (gebaut + -unit-getestet gegen die Scratch-Integration, aber **nicht visuell verifiziert**): +Weitere Punkte, gebaut + unit-getestet auf `main`, aber **nicht visuell verifiziert**: - [ ] SystemCheckPage: Layout, Icon-/Farbwirkung der vier Status (Ok grün / Warnung orange / Fehler rot / Unbekannt grau — `StatusGreenBrush`/`StatusOrangeBrush`/`StatusRedBrush`/ diff --git a/src/ClaudeDo.Installer/App.xaml.cs b/src/ClaudeDo.Installer/App.xaml.cs index 7251f103..8465d71c 100644 --- a/src/ClaudeDo.Installer/App.xaml.cs +++ b/src/ClaudeDo.Installer/App.xaml.cs @@ -129,7 +129,9 @@ public partial class App : Application // Environment checks — stateless, so their infrastructure is shared; ClaudeCliLookup is // rebuilt per EnvironmentCheckService instance so a re-check doesn't reuse a stale result. sc.AddSingleton(); + sc.AddSingleton(); sc.AddSingleton(); + sc.AddSingleton(); sc.AddTransient>(sp => () => { var processRunner = sp.GetRequiredService(); diff --git a/src/ClaudeDo.Installer/CLAUDE.md b/src/ClaudeDo.Installer/CLAUDE.md index 90de96da..9a02c039 100644 --- a/src/ClaudeDo.Installer/CLAUDE.md +++ b/src/ClaudeDo.Installer/CLAUDE.md @@ -128,17 +128,6 @@ The Apps & Features uninstall string and "Rerun Installer" both point at ` **Merge status (2026-08-05): not yet on `main`.** The `Checks/` folder, `SystemCheckPage`, -> and `ExecutableResolver` described below exist only on unmerged task branches -> (`claudedo/06aca9b3afec4b939f59b627bfe21737` for the Installer side, -> `claudedo/40272c0bb3b14562b59c022d09c382b6` for the `ClaudeDo.Worker` wiring). Build/test -> verification for this section was done against a local scratch integration of both, not -> against this repo's actual `main`. Merge them (or re-derive equivalent commits) before trusting -> this section against the checked-out code. See `docs/open.md` for the outstanding gap this -> leaves (`Checks/` and `SystemCheckPage` are real, but the "Claude Help Me" button and the -> Config-mode Diagnose section described as follow-ups were never implemented — both follow-up -> tasks blocked on this same missing merge and shipped no code). - `Checks/` holds one `IEnvironmentCheck` per concern, run in parallel by `EnvironmentCheckService.RunAllAsync`: | Check | Severity | What it verifies | @@ -173,6 +162,20 @@ on page entry (`LoadAsync`, guarded against double-entry). "Next" is disabled vi subscribes to `PropertyChanged` on the current page so a live recheck can flip it back. A "Recheck" button re-runs `EnvironmentCheckService.RunAllAsync` (disabled while already running). +**"Claude Help Me" button** (`Core/ClaudeHelpLauncher.cs`) — a second button below the shared +check-list footer, enabled only when `claude-cli` is `Ok` and `claude-auth` is not `Failed` (`Unknown` +stays enabled — an indeterminate login state shouldn't block the one feature that could help +diagnose it). `BuildReportAsync` renders all check results (Id/Severity/Status/Message table, +plus the full `Detail` of any `Failed` check) and system info (OS, `dotnet --list-runtimes`, +resolved `git`/`claude` messages, planned install dir/ports) into +`%TEMP%\claudedo-setup-diagnose.md` — English and hardcoded (an AI assistant reads it, not the +user) and deliberately excludes credentials/tokens/env-var dumps. `LaunchTerminal` then opens +`wt.exe -d %TEMP% cmd.exe /k ` (or `cmd.exe /k ` if `wt.exe` +isn't resolvable) via the injectable `IProcessLauncher`, pointing the initial prompt at that +report file. Errors from either step surface as `ClaudeHelpError` on the page, never an +exception. It reads the last report via `CheckListViewModel.LastReport`, so it re-evaluates on +every check run. + `DiagnosePage` (`Pages/DiagnosePage/`) hosts the same `CheckListView` in `SettingsWindow` (Config mode only, `ShowInSettings = true` / `ShowInWizard = false`, `Order = 5` — after UiSettings). Nothing here blocks navigation and checks do **not** auto-run on load — only on a "Recheck" click. @@ -180,8 +183,5 @@ Unlike the wizard, its `InstallContext` is built from the **installed** configur (`InstallerWorkerConfig.Load()` for `ClaudeBin`/`SignalRPort`, the shared `InstallContext` for `InstallDirectory`/`ExternalMcpPort`), refreshed on every `LoadAsync()` — not the wizard-default `InstallContext` the DI container hands out, which is only populated once a page's `ApplyAsync` -(i.e. Save) runs. - -**Not implemented (see merge-status note above):** a "Claude Help Me" button that launches an -external terminal with a live `claude` session for setup troubleshooting. Its footer slot is -reserved in `CheckListView.xaml`; still blocked on the same follow-up as before. +(i.e. Save) runs. `DiagnosePage` intentionally has **no** "Claude Help Me" button — that one is +wizard-only. diff --git a/src/ClaudeDo.Installer/Checks/CheckListViewModel.cs b/src/ClaudeDo.Installer/Checks/CheckListViewModel.cs index a2696852..c782c6e6 100644 --- a/src/ClaudeDo.Installer/Checks/CheckListViewModel.cs +++ b/src/ClaudeDo.Installer/Checks/CheckListViewModel.cs @@ -58,6 +58,10 @@ public partial class CheckListViewModel : ObservableObject public ObservableCollection Rows { get; } = []; + /// Report of the most recent run, null before the first one. Hosts that need the raw + /// results (the wizard's "Claude Help Me" button) read it instead of running the checks again. + public EnvironmentCheckReport? LastReport { get; private set; } + [ObservableProperty] private bool _isRunning; [ObservableProperty] private bool _hasRun; [ObservableProperty] private bool _hasBlockingError; @@ -88,6 +92,8 @@ public partial class CheckListViewModel : ObservableObject foreach (var result in report.Results) Rows.Add(new CheckRowViewModel(result)); + LastReport = report; + OnPropertyChanged(nameof(LastReport)); HasBlockingError = report.HasBlockingError; Summary = BuildSummary(report); HasRun = true; diff --git a/src/ClaudeDo.Installer/Core/ClaudeHelpLauncher.cs b/src/ClaudeDo.Installer/Core/ClaudeHelpLauncher.cs new file mode 100644 index 00000000..e9bf576f --- /dev/null +++ b/src/ClaudeDo.Installer/Core/ClaudeHelpLauncher.cs @@ -0,0 +1,182 @@ +using System.Diagnostics; +using System.IO; +using System.Runtime.InteropServices; +using System.Text; +using ClaudeDo.Data.Environment; +using ClaudeDo.Installer.Checks; +using ClaudeDo.Installer.Core.Interfaces; + +namespace ClaudeDo.Installer.Core; + +public sealed record ClaudeHelpLaunchResult(bool Success, string? ErrorMessage); + +/// +/// Builds a setup-diagnose report from the environment check results and opens an interactive +/// claude session pointed at it, so a stuck user can get live help finishing setup. +/// +public sealed class ClaudeHelpLauncher +{ + public const string ReportFileName = "claudedo-setup-diagnose.md"; + + private readonly IProcessRunner _processRunner; + private readonly IProcessLauncher _processLauncher; + private readonly string? _pathOverride; + private readonly string? _pathExtOverride; + + public ClaudeHelpLauncher( + IProcessRunner processRunner, + IProcessLauncher processLauncher, + string? pathOverride = null, + string? pathExtOverride = null) + { + _processRunner = processRunner; + _processLauncher = processLauncher; + _pathOverride = pathOverride; + _pathExtOverride = pathExtOverride; + } + + public async Task BuildReportAsync(EnvironmentCheckReport report, InstallContext ctx, CancellationToken ct) + { + var sb = new StringBuilder(); + + sb.AppendLine("# ClaudeDo setup diagnose"); + sb.AppendLine(); + sb.AppendLine( + "ClaudeDo is a desktop task manager that runs the `claude` CLI autonomously in git " + + "worktrees. The person you're talking to is stuck partway through the setup wizard. " + + "Explain the likely cause of each failing check below in plain language, propose the " + + "concrete command(s) to fix it, and ask a clarifying question if something here is " + + "ambiguous."); + sb.AppendLine(); + + sb.AppendLine("## Check results"); + sb.AppendLine(); + sb.AppendLine("| Id | Severity | Status | Message |"); + sb.AppendLine("|---|---|---|---|"); + foreach (var result in report.Results) + { + sb.AppendLine($"| {result.Id} | {result.Severity} | {result.Status} | {EscapeCell(result.Message)} |"); + } + sb.AppendLine(); + + var failedDetails = report.Results + .Where(r => r.Status == CheckStatus.Failed && !string.IsNullOrWhiteSpace(r.Detail)) + .ToList(); + if (failedDetails.Count > 0) + { + sb.AppendLine("## Details of failed checks"); + foreach (var result in failedDetails) + { + sb.AppendLine(); + sb.AppendLine($"### {result.Id}"); + sb.AppendLine("```"); + sb.AppendLine(result.Detail); + sb.AppendLine("```"); + } + sb.AppendLine(); + } + + sb.AppendLine("## System info"); + sb.AppendLine(); + sb.AppendLine($"- Windows: {RuntimeInformation.OSDescription}"); + sb.AppendLine($"- Architecture: {RuntimeInformation.OSArchitecture}"); + sb.AppendLine($"- git: {FindMessage(report, GitCheck.CheckId)}"); + sb.AppendLine($"- claude: {FindMessage(report, ClaudeCliCheck.CheckId)}"); + sb.AppendLine($"- Planned install directory: {ctx.InstallDirectory}"); + sb.AppendLine($"- Planned ports: SignalR {ctx.SignalRPort}, MCP {ctx.ExternalMcpPort}"); + sb.AppendLine("- `dotnet --list-runtimes`:"); + sb.AppendLine("```"); + sb.AppendLine(await RunDotnetListRuntimesAsync(ct)); + sb.AppendLine("```"); + sb.AppendLine(); + + // No secrets: never include credentials.json content, tokens, or a raw environment + // variable dump here — only resolved paths and version numbers, since this file exists + // to be read by an AI assistant, not to stay private. + sb.AppendLine( + "No secrets are included above (no credentials file contents, no tokens, no raw " + + "environment variable dump) — only resolved paths and version numbers."); + sb.AppendLine(); + sb.AppendLine("Please start with the blocking errors above."); + + return sb.ToString(); + } + + public async Task LaunchAsync(EnvironmentCheckReport report, InstallContext ctx, CancellationToken ct) + { + string reportPath; + try + { + var content = await BuildReportAsync(report, ctx, ct); + reportPath = Path.Combine(Path.GetTempPath(), ReportFileName); + await File.WriteAllTextAsync(reportPath, content, ct); + } + catch (Exception ex) + { + return new ClaudeHelpLaunchResult(false, ex.Message); + } + + return LaunchTerminal(reportPath, ctx); + } + + public ClaudeHelpLaunchResult LaunchTerminal(string reportPath, InstallContext ctx) + { + var claude = ExecutableResolver.Resolve(ctx.ClaudeBin, _pathOverride, _pathExtOverride); + if (claude is null) + { + return new ClaudeHelpLaunchResult(false, $"'{ctx.ClaudeBin}' was not found on PATH."); + } + + var tempDir = Path.GetTempPath(); + var promptText = $"Lies {reportPath} und hilf mir, mein ClaudeDo-Setup zum Laufen zu bringen."; + var claudeCommand = $"{Quote(claude.Path)} {Quote(promptText)}"; + + var wt = ExecutableResolver.Resolve("wt", _pathOverride, _pathExtOverride); + var startInfo = wt is not null + ? new ProcessStartInfo(wt.Path) + { + Arguments = $"-d {QuoteDirectory(tempDir)} cmd.exe /k {claudeCommand}", + WorkingDirectory = tempDir, + UseShellExecute = false, + } + : new ProcessStartInfo("cmd.exe") + { + Arguments = $"/k {claudeCommand}", + WorkingDirectory = tempDir, + UseShellExecute = false, + }; + + try + { + _processLauncher.Start(startInfo); + return new ClaudeHelpLaunchResult(true, null); + } + catch (Exception ex) + { + return new ClaudeHelpLaunchResult(false, ex.Message); + } + } + + private async Task RunDotnetListRuntimesAsync(CancellationToken ct) + { + try + { + var (_, output) = await _processRunner.RunAsync("dotnet", "--list-runtimes", null, ct); + return output.Trim(); + } + catch (Exception ex) + { + return $"(could not run 'dotnet --list-runtimes': {ex.Message})"; + } + } + + private static string? FindMessage(EnvironmentCheckReport report, string checkId) => + report.Results.FirstOrDefault(r => r.Id == checkId)?.Message; + + private static string EscapeCell(string value) => + value.Replace("|", "\\|").Replace("\r", " ").Replace("\n", " "); + + private static string Quote(string value) => value.Contains(' ') ? $"\"{value}\"" : value; + + private static string QuoteDirectory(string directory) => Quote(directory.TrimEnd('\\', '/')); +} diff --git a/src/ClaudeDo.Installer/Core/Interfaces/IProcessLauncher.cs b/src/ClaudeDo.Installer/Core/Interfaces/IProcessLauncher.cs new file mode 100644 index 00000000..add7a1fa --- /dev/null +++ b/src/ClaudeDo.Installer/Core/Interfaces/IProcessLauncher.cs @@ -0,0 +1,9 @@ +using System.Diagnostics; + +namespace ClaudeDo.Installer.Core.Interfaces; + +/// Starts a detached process without waiting for it to exit or capturing its output. +public interface IProcessLauncher +{ + void Start(ProcessStartInfo startInfo); +} diff --git a/src/ClaudeDo.Installer/Core/ProcessLauncher.cs b/src/ClaudeDo.Installer/Core/ProcessLauncher.cs new file mode 100644 index 00000000..6c5889d3 --- /dev/null +++ b/src/ClaudeDo.Installer/Core/ProcessLauncher.cs @@ -0,0 +1,9 @@ +using System.Diagnostics; +using ClaudeDo.Installer.Core.Interfaces; + +namespace ClaudeDo.Installer.Core; + +public sealed class ProcessLauncher : IProcessLauncher +{ + public void Start(ProcessStartInfo startInfo) => Process.Start(startInfo); +} diff --git a/src/ClaudeDo.Installer/Pages/SystemCheckPage/SystemCheckPageView.xaml b/src/ClaudeDo.Installer/Pages/SystemCheckPage/SystemCheckPageView.xaml index 31cb04c1..ca3b4314 100644 --- a/src/ClaudeDo.Installer/Pages/SystemCheckPage/SystemCheckPageView.xaml +++ b/src/ClaudeDo.Installer/Pages/SystemCheckPage/SystemCheckPageView.xaml @@ -13,6 +13,7 @@ + @@ -23,5 +24,16 @@ + + + + +