feat(claude-do): „Claude Help Me"-Button: Claude-Session zur Setup-Fehlersuch
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
This commit is contained in:
@@ -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<IProcessRunner, ProcessRunnerAdapter>();
|
||||
sc.AddSingleton<IProcessLauncher, ProcessLauncher>();
|
||||
sc.AddSingleton<IPortOwnerResolver, NetstatPortOwnerResolver>();
|
||||
sc.AddSingleton<ClaudeHelpLauncher>();
|
||||
sc.AddTransient<Func<EnvironmentCheckService>>(sp => () =>
|
||||
{
|
||||
var processRunner = sp.GetRequiredService<IProcessRunner>();
|
||||
|
||||
@@ -128,16 +128,11 @@ The Apps & Features uninstall string and "Rerun Installer" both point at `<Insta
|
||||
|
||||
## Environment Checks
|
||||
|
||||
> **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 <claude> <prompt>` (or `cmd.exe /k <claude> <prompt>` 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.
|
||||
|
||||
@@ -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);
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
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<string> 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<ClaudeHelpLaunchResult> 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<string> 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('\\', '/'));
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace ClaudeDo.Installer.Core.Interfaces;
|
||||
|
||||
/// <summary>Starts a detached process without waiting for it to exit or capturing its output.</summary>
|
||||
public interface IProcessLauncher
|
||||
{
|
||||
void Start(ProcessStartInfo startInfo);
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -90,10 +90,18 @@
|
||||
</StackPanel>
|
||||
|
||||
<!-- Footer -->
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" HorizontalAlignment="Right" Margin="0,12,0,0">
|
||||
<!-- Reserved space: a "Claude Help Me" button lands here in a follow-up task. -->
|
||||
<Button Content="{loc:Tr installer.systemCheck.recheck}"
|
||||
Command="{Binding RunChecksCommand}"/>
|
||||
<StackPanel Grid.Row="3" Margin="0,12,0,0">
|
||||
<TextBlock Text="{Binding ClaudeHelpError}" FontSize="12" TextWrapping="Wrap"
|
||||
Foreground="{StaticResource ErrorBrush}" Margin="0,0,0,8"
|
||||
Visibility="{Binding ClaudeHelpError, Converter={StaticResource NullToCollapsedConverter}}"/>
|
||||
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right">
|
||||
<Button Content="{loc:Tr installer.systemCheck.claudeHelp.button}"
|
||||
ToolTip="{Binding ClaudeHelpTooltip}"
|
||||
Command="{Binding StartClaudeHelpCommand}"
|
||||
Margin="0,0,8,0"/>
|
||||
<Button Content="{loc:Tr installer.systemCheck.recheck}"
|
||||
Command="{Binding RunChecksCommand}"/>
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</UserControl>
|
||||
|
||||
@@ -51,8 +51,10 @@ public partial class SystemCheckPageViewModel : ObservableObject, IInstallerPage
|
||||
{
|
||||
private readonly InstallContext _context;
|
||||
private readonly Func<EnvironmentCheckService> _checkServiceFactory;
|
||||
private readonly ClaudeHelpLauncher _claudeHelpLauncher;
|
||||
private SystemCheckPageView? _view;
|
||||
private bool _hasStarted;
|
||||
private EnvironmentCheckReport? _lastReport;
|
||||
|
||||
public string Title => TrExtension.Localizer?["installer.systemCheck.title"] ?? "System Check";
|
||||
public string Icon => "";
|
||||
@@ -67,13 +69,40 @@ public partial class SystemCheckPageViewModel : ObservableObject, IInstallerPage
|
||||
[ObservableProperty] private bool _hasRun;
|
||||
[ObservableProperty] private bool _hasBlockingError;
|
||||
[ObservableProperty] private string _summary = string.Empty;
|
||||
[ObservableProperty] private string? _claudeHelpError;
|
||||
|
||||
public bool BlocksNavigation => IsRunning || HasBlockingError;
|
||||
|
||||
public SystemCheckPageViewModel(InstallContext context, Func<EnvironmentCheckService> checkServiceFactory)
|
||||
public bool ClaudeCliOk =>
|
||||
_lastReport?.Results.FirstOrDefault(r => r.Id == ClaudeCliCheck.CheckId)?.Status == CheckStatus.Ok;
|
||||
|
||||
public bool ClaudeAuthFailed =>
|
||||
_lastReport?.Results.FirstOrDefault(r => r.Id == ClaudeAuthCheck.CheckId)?.Status == CheckStatus.Failed;
|
||||
|
||||
public bool CanStartClaudeHelp => ClaudeCliOk && !ClaudeAuthFailed;
|
||||
|
||||
public string ClaudeHelpTooltip
|
||||
{
|
||||
get
|
||||
{
|
||||
var loc = TrExtension.Localizer;
|
||||
if (!ClaudeCliOk)
|
||||
return loc?["installer.systemCheck.claudeHelp.tooltip.cliMissing"] ?? "The Claude CLI was not found.";
|
||||
if (ClaudeAuthFailed)
|
||||
return loc?["installer.systemCheck.claudeHelp.tooltip.notLoggedIn"] ?? "Claude is not logged in.";
|
||||
return loc?["installer.systemCheck.claudeHelp.tooltip.ready"]
|
||||
?? "Start an interactive Claude session to help troubleshoot your setup.";
|
||||
}
|
||||
}
|
||||
|
||||
public SystemCheckPageViewModel(
|
||||
InstallContext context,
|
||||
Func<EnvironmentCheckService> checkServiceFactory,
|
||||
ClaudeHelpLauncher claudeHelpLauncher)
|
||||
{
|
||||
_context = context;
|
||||
_checkServiceFactory = checkServiceFactory;
|
||||
_claudeHelpLauncher = claudeHelpLauncher;
|
||||
}
|
||||
|
||||
partial void OnIsRunningChanged(bool value)
|
||||
@@ -115,9 +144,14 @@ public partial class SystemCheckPageViewModel : ObservableObject, IInstallerPage
|
||||
foreach (var result in report.Results)
|
||||
Rows.Add(new CheckRowViewModel(result));
|
||||
|
||||
_lastReport = report;
|
||||
HasBlockingError = report.HasBlockingError;
|
||||
Summary = BuildSummary(report);
|
||||
HasRun = true;
|
||||
|
||||
OnPropertyChanged(nameof(CanStartClaudeHelp));
|
||||
OnPropertyChanged(nameof(ClaudeHelpTooltip));
|
||||
StartClaudeHelpCommand.NotifyCanExecuteChanged();
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -127,6 +161,20 @@ public partial class SystemCheckPageViewModel : ObservableObject, IInstallerPage
|
||||
|
||||
private bool CanRunChecks() => !IsRunning;
|
||||
|
||||
[RelayCommand(CanExecute = nameof(CanStartClaudeHelp))]
|
||||
private async Task StartClaudeHelpAsync()
|
||||
{
|
||||
if (_lastReport is null) return;
|
||||
|
||||
ClaudeHelpError = null;
|
||||
var result = await _claudeHelpLauncher.LaunchAsync(_lastReport, _context, CancellationToken.None);
|
||||
if (!result.Success)
|
||||
{
|
||||
ClaudeHelpError = TrExtension.Localizer?.Get("installer.systemCheck.claudeHelp.error", result.ErrorMessage ?? "")
|
||||
?? $"Could not start the Claude session: {result.ErrorMessage}";
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildSummary(EnvironmentCheckReport report)
|
||||
{
|
||||
var loc = TrExtension.Localizer;
|
||||
|
||||
Reference in New Issue
Block a user