feat(settings): claude_bin im Dateien-Tab setzbar, mit Auflösungs-Check

Bisher nur per Hand in worker.config.json. Neuer Abschnitt CLAUDE CLI mit
eigenem Save-Button (die Datei gehoert dem Worker, nicht app_settings) und einer
Zeile, wohin der Wert per ExecutableResolver tatsaechlich aufloest — 'nicht im
PATH gefunden' war bisher erst am fehlgeschlagenen Run zu sehen. WorkerConfig ist
DI-Singleton und ClaudeProcess loest pro Spawn auf, also greift die Aenderung ab
dem naechsten Run ohne Neustart.

SaveOnlineInbox/SaveClaudeBin teilen jetzt ein SaveKey(), damit beide dieselbe
read-modify-write-Semantik haben (alle anderen Keys bleiben unberuehrt).
Die zugehoerigen Locale-Keys sind im vorigen Commit mitgelaufen.
This commit is contained in:
mika kuns
2026-08-27 10:39:25 +02:00
parent 31d53b0033
commit 13b81cac68
9 changed files with 124 additions and 1 deletions
+5
View File
@@ -105,6 +105,11 @@ public record ListConfigDto(string? Model, string? SystemPrompt, string? AgentPa
public record SeedResultDto(int Copied, int Skipped);
/// <param name="Value">What worker.config.json holds (a command name or a full path).</param>
/// <param name="ResolvedPath">Where it actually resolves to right now, or null if nothing resolves —
/// the whole point of surfacing this setting is telling those two cases apart.</param>
public record ClaudeBinDto(string Value, string? ResolvedPath);
public record OnlineInboxStateDto(
bool Enabled,
string ApiBaseUrl,
@@ -194,6 +194,9 @@ public interface IWorkerClient : INotifyPropertyChanged
Task<(bool Ok, string? Error)> SetWorktreeStateAsync(string taskId, WorktreeState newState);
Task<ForceRemoveResultDto?> ForceRemoveWorktreeAsync(string taskId);
Task<ClaudeBinDto?> GetClaudeBinAsync();
Task<ClaudeBinDto?> SetClaudeBinAsync(string? value);
Task<OnlineInboxStateDto?> GetOnlineInboxStateAsync();
Task SetOnlineInboxConfigAsync(OnlineInboxConfigInputDto input);
Task SetOnlineInboxAuthAsync(string refreshToken);
+6
View File
@@ -664,6 +664,12 @@ public partial class WorkerClient : ObservableObject, IAsyncDisposable, IWorkerC
await InvokeTimedAsync("QueuePlanningSubtasksAsync", () => _hub.InvokeAsync("QueuePlanningSubtasksAsync", parentTaskId, ct));
}
public Task<ClaudeBinDto?> GetClaudeBinAsync()
=> TryInvokeAsync<ClaudeBinDto>("GetClaudeBin");
public Task<ClaudeBinDto?> SetClaudeBinAsync(string? value)
=> TryInvokeAsync<ClaudeBinDto>("SetClaudeBin", value);
public Task<OnlineInboxStateDto?> GetOnlineInboxStateAsync()
=> TryInvokeAsync<OnlineInboxStateDto>("GetOnlineInboxState");
@@ -12,6 +12,12 @@ public sealed partial class FilesSettingsTabViewModel : ViewModelBase
private readonly IWorkerClient _worker;
public OperationStatus RestoreOp { get; } = new();
public OperationStatus ClaudeBinOp { get; } = new();
// worker.config.json's claude_bin: a command name or a full path to the CLI. Saved on its own
// (not with the modal's Save button) because the worker owns the file, not app_settings.
[ObservableProperty] private string _claudeBin = "";
[ObservableProperty] private string _claudeBinResolvedHint = "";
[ObservableProperty] private string _statusMessage = "";
[ObservableProperty] private bool _hasCustomizedPrompts;
@@ -37,6 +43,34 @@ public sealed partial class FilesSettingsTabViewModel : ViewModelBase
};
}
public async Task LoadClaudeBinAsync()
{
var dto = await _worker.GetClaudeBinAsync();
if (dto is null) return;
ClaudeBin = dto.Value;
ApplyResolvedHint(dto);
}
[RelayCommand]
private async Task SaveClaudeBin()
{
StatusMessage = "";
using var op = ClaudeBinOp.Begin(Loc.T("ops.claudeBin.saving"));
var dto = await _worker.SetClaudeBinAsync(ClaudeBin);
if (dto is null)
{
StatusMessage = Loc.T("vm.filesTab.workerOffline");
return;
}
ClaudeBin = dto.Value;
ApplyResolvedHint(dto);
}
private void ApplyResolvedHint(ClaudeBinDto dto) =>
ClaudeBinResolvedHint = dto.ResolvedPath is null
? Loc.T("settings.files.claudeBinUnresolved")
: Loc.T("settings.files.claudeBinResolved", dto.ResolvedPath);
private bool CanRestoreDefaultAgents() => !RestoreOp.IsRunning;
[RelayCommand(CanExecute = nameof(CanRestoreDefaultAgents))]
+9 -1
View File
@@ -96,6 +96,14 @@ public sealed class WorkerConfig
/// Reads the existing JSON, replaces the <c>online_inbox</c> node, and writes back indented.
/// </summary>
public void SaveOnlineInbox(string? path = null)
=> SaveKey("online_inbox", JsonSerializer.SerializeToNode(OnlineInbox, InboxSerializerOpts), path);
/// <summary>Persists ONLY <c>claude_bin</c>, same read-modify-write as
/// <see cref="SaveOnlineInbox"/> — every other field in the file stays untouched.</summary>
public void SaveClaudeBin(string? path = null)
=> SaveKey("claude_bin", JsonValue.Create(ClaudeBin), path);
private static void SaveKey(string key, JsonNode? value, string? path)
{
path ??= DefaultConfigPath;
@@ -103,7 +111,7 @@ public sealed class WorkerConfig
? JsonNode.Parse(File.ReadAllText(path)) as JsonObject ?? new JsonObject()
: new JsonObject();
root["online_inbox"] = JsonSerializer.SerializeToNode(OnlineInbox, InboxSerializerOpts);
root[key] = value;
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
File.WriteAllText(path, root.ToJsonString(WriteOpts));
+17
View File
@@ -1,6 +1,7 @@
using System.Globalization;
using System.Reflection;
using ClaudeDo.Data;
using ClaudeDo.Data.Environment;
using ClaudeDo.Data.Models;
using ClaudeDo.Data.Repositories;
using ClaudeDo.Worker.Agents;
@@ -1000,6 +1001,22 @@ public sealed class WorkerHub : Microsoft.AspNetCore.SignalR.Hub
return ids.Count;
}
public ClaudeBinDto GetClaudeBin() => DescribeClaudeBin();
/// Blank resets to the plain "claude" command. WorkerConfig is a DI singleton and
/// ClaudeProcess resolves it per spawn, so the change applies to the next run without a
/// restart — but it is persisted too, since Load() re-reads the file on startup.
public ClaudeBinDto SetClaudeBin(string? value)
{
_cfg.ClaudeBin = string.IsNullOrWhiteSpace(value) ? "claude" : value.Trim();
_cfg.SaveClaudeBin();
_ = _broadcaster.WorkerLog($"claude_bin set to '{_cfg.ClaudeBin}'", WorkerLogLevel.Info, DateTime.UtcNow);
return DescribeClaudeBin();
}
private ClaudeBinDto DescribeClaudeBin() =>
new(_cfg.ClaudeBin, ExecutableResolver.Resolve(_cfg.ClaudeBin)?.Path);
#pragma warning disable CA1416 // ClaudeDo.Worker is Windows-only; DPAPI calls are safe here.
public OnlineInboxStateDto GetOnlineInboxState()
{