From 09e0772673a5e58ec574a81aa4e8f7f76762ae48 Mon Sep 17 00:00:00 2001 From: mika kuns Date: Thu, 6 Aug 2026 08:25:36 +0200 Subject: [PATCH] =?UTF-8?q?=EF=BB=BFfeat(claude-do):=20=E2=80=9EClaude=20H?= =?UTF-8?q?elp=20Me"-Button:=20Claude-Session=20zur=20Setup-Fehlersuch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Button, der aus „Problem erkannt" ein „Problem gelöst" macht: startet eine interaktive Claude-Session, die dem Nutzer beim Einrichten hilft. ## Warum externes Terminal Der ConPTY-Stack (`PtyTerminalSession`, `ConPtyPaneView`) liegt in `ClaudeDo.Ui` und ist Avalonia — der Installer ist WPF und referenziert nur Data/Releases/Localization. Beim Fresh Install sind `app\`/`worker\` außerdem noch n ClaudeDo-Task: 4e196058-38a3-404f-9862-4cb0e90195da --- docs/open.md | 19 +- src/ClaudeDo.Installer/App.xaml.cs | 2 + src/ClaudeDo.Installer/CLAUDE.md | 35 +-- .../Core/ClaudeHelpLauncher.cs | 182 +++++++++++++++ .../Core/Interfaces/IProcessLauncher.cs | 9 + .../Core/ProcessLauncher.cs | 9 + .../SystemCheckPage/SystemCheckPageView.xaml | 16 +- .../SystemCheckPageViewModel.cs | 50 +++- src/ClaudeDo.Localization/locales/de.json | 9 + src/ClaudeDo.Localization/locales/en.json | 9 + .../Core/ClaudeHelpLauncherTests.cs | 215 ++++++++++++++++++ .../Core/FakeProcessLauncher.cs | 22 ++ .../Core/TestClaudeHelpLauncher.cs | 11 + .../PageResolverTests.cs | 5 +- .../SystemCheckPageViewModelTests.cs | 67 +++++- .../WizardViewModelTests.cs | 5 +- 16 files changed, 630 insertions(+), 35 deletions(-) create mode 100644 src/ClaudeDo.Installer/Core/ClaudeHelpLauncher.cs create mode 100644 src/ClaudeDo.Installer/Core/Interfaces/IProcessLauncher.cs create mode 100644 src/ClaudeDo.Installer/Core/ProcessLauncher.cs create mode 100644 tests/ClaudeDo.Installer.Tests/Core/ClaudeHelpLauncherTests.cs create mode 100644 tests/ClaudeDo.Installer.Tests/Core/FakeProcessLauncher.cs create mode 100644 tests/ClaudeDo.Installer.Tests/Core/TestClaudeHelpLauncher.cs diff --git a/docs/open.md b/docs/open.md index 007b2ecd..2e57bcdf 100644 --- a/docs/open.md +++ b/docs/open.md @@ -149,16 +149,17 @@ 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`. -**Zusätzliche Lücke, unabhängig vom Merge:** die zwei Folge-Tasks „Claude Help Me"-Button -und Diagnose-Sektion (Config-Modus/`SettingsWindow`) sind **nicht implementiert** — beide -liefen ins selbe Merge-Problem und wurden ohne jede Code-Änderung als `Blocked` beendet. Die -folgenden Punkte, die diese zwei Features beträfen, können also noch nicht geprüft werden und -brauchen zuerst eine neue Umsetzungsrunde: +**Update 2026-08-06:** der „Claude Help Me"-Button ist jetzt implementiert +(`Core/ClaudeHelpLauncher.cs` + `SystemCheckPageViewModel`/-View), gebaut + unit-getestet +(`ClaudeDo.Installer.Tests`/`ClaudeDo.Localization.Tests` grün). Die Diagnose-Sektion +(Config-Modus/`SettingsWindow`) bleibt **nicht implementiert** — eigener Folge-Task. -- [ ] „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.** +- [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). - [ ] Diagnose-Sektion im Config-Modus zeigt die echten installierten Pfade/Ports, und der laufende Worker auf 47821 gilt nicht als Konflikt — **nicht umsetzbar, Feature existiert nicht.** diff --git a/src/ClaudeDo.Installer/App.xaml.cs b/src/ClaudeDo.Installer/App.xaml.cs index ef35f7da..4cb440fd 100644 --- a/src/ClaudeDo.Installer/App.xaml.cs +++ b/src/ClaudeDo.Installer/App.xaml.cs @@ -128,7 +128,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 47a360e5..59118972 100644 --- a/src/ClaudeDo.Installer/CLAUDE.md +++ b/src/ClaudeDo.Installer/CLAUDE.md @@ -128,16 +128,11 @@ 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). +> **Merge status (2026-08-06): `Checks/`, `SystemCheckPage`, `ExecutableResolver`, and the +> "Claude Help Me" button below are on this branch.** The 2026-08-05 note about two unmerged +> task branches applied to an earlier state; the "Claude Help Me" button follow-up has since +> landed (see below). The Config-mode Diagnose section in `SettingsWindow` is still **not** +> implemented — see `docs/open.md`. `Checks/` holds one `IEnvironmentCheck` per concern, run in parallel by `EnvironmentCheckService.RunAllAsync`: @@ -169,8 +164,18 @@ 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). -**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, and a Diagnose section -in `SettingsWindow` (Config mode) that re-runs the same checks against the installed configuration. -Both were speced as follow-up tasks; both blocked before writing any code because their prerequisite -(this section) wasn't on `main` yet. +**"Claude Help Me" button** (`Core/ClaudeHelpLauncher.cs`) — a second footer button next to +"Recheck", 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. + +**Not implemented:** a Diagnose section in `SettingsWindow` (Config mode) that re-runs the same +checks against the installed configuration — still speced as a follow-up, no code yet. 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 459163af..642b8180 100644 --- a/src/ClaudeDo.Installer/Pages/SystemCheckPage/SystemCheckPageView.xaml +++ b/src/ClaudeDo.Installer/Pages/SystemCheckPage/SystemCheckPageView.xaml @@ -90,10 +90,18 @@ - - -