diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json index 4ca377d4..7e1cda5b 100644 --- a/src/ClaudeDo.Localization/locales/de.json +++ b/src/ClaudeDo.Localization/locales/de.json @@ -41,6 +41,8 @@ "usageGateSevenDayPct": "7-Tage-Fenster (%)", "autoContinueOnUsageLimit": "Nach Limit-Reset fortsetzen", "autoContinueOnUsageLimitHint": "Ist ein Run am Nutzungslimit abgebrochen, wird er nach Ablauf des 5-Stunden-Fensters automatisch fortgesetzt. Weckt außerdem die pausierte Queue genau zum Reset-Zeitpunkt statt erst beim nächsten Backstop-Check.", + "listHandlerMaxHandoffs": "Handoff-Budget des List-Handlers", + "listHandlerMaxHandoffsHint": "Wie viele Wait/Merge-Handoffs ein „Let Claude handle it\"-Run verketten darf. 4 entspricht zwei Merge-Runden. Für Läufe, die stundenlang durchlaufen sollen, höher setzen; der Wert begrenzt eine entlaufene Kette und darf daher nicht 0 sein.", "throttleStagesHeading": "Throttle-Stufen", "throttleStagesHint": "Die Parallelität wird vor dem harten Gate begrenzt: soft auf 2 Slots, hard auf 1. Eingestellt wird das durch Ziehen der Marker an den Gauges im Usage-Monitor. Der Monitor schließt diese Einstellungen — vorher speichern, wenn du etwas geändert hast.", "throttleStagesValues": "5h: soft {0}% / hard {1}% · 7d: soft {2}% / hard {3}%", @@ -85,6 +87,7 @@ "retryPrompt": "Wiederholung", "dailyPrepPrompt": "Tagesplanung", "weeklyReportPrompt": "Wochenbericht", + "powerUsePrompt": "PowerUse-Addendum", "viewPrompt": "Ansehen", "customizedSection": "ANGEPASST", "customizedHint": "Diese Prompts weichen vom mitgelieferten Standard ab und werden unverändert beibehalten. Zurücksetzen, um künftige Standard-Verbesserungen zu erhalten.", @@ -398,6 +401,8 @@ "manualListHint": "Neue Aufgaben in dieser Liste sind zunächst manuell: kein Einreihen, Ausführen oder Verfeinern, und die Automatik überspringt sie. Eine handgesteuerte Sitzung kannst du weiterhin öffnen.", "findingsTracked": "Findings-Ordner .claudedo einchecken", "findingsTrackedHint": "Aus: der Ordner bleibt über .git/info/exclude aus git heraus. An: Findings reisen mit dem Repo.", + "powerUseAllowed": "PowerUse für diese Liste erlauben", + "powerUseAllowedHint": "Erlaubt einem List-Handler-Run auf dieser Liste, unbeaufsichtigt zu laufen, sobald PowerUse im Usage-Monitor eingeschaltet ist: er fragt nichts, löst Konflikte selbst und sammelt neue Arbeit. Beide Schalter sind nötig — ein global aktives PowerUse ändert an einer Liste ohne Opt-in also nichts.", "sectionIcon": "ICON", "icon": "Listen-Icon", "iconHint": "Wird in der Seitenleiste neben dem Listennamen angezeigt.", @@ -504,6 +509,8 @@ "title": "USAGE MONITOR", "windowTitle": "Usage Monitor", "noGauges": "Noch keine Nutzungslimits gemeldet.", + "powerUse": "PowerUse", + "powerUseHint": "Das restliche Fenster ausnutzen: ein List-Handler-Run fragt nichts, löst Konflikte selbst, sammelt neue Arbeit und handoffet weiter, solange sein Budget reicht. Wirkt nur auf Listen, die PowerUse in ihren Listen-Einstellungen erlaubt haben.", "refresh": "Jetzt aktualisieren", "refreshHint": "Abruf alle 5 Min., während ein Task läuft, sonst alle 15 Min.", "staleFormat": "Werte veraltet (Stand {0})", @@ -749,6 +756,7 @@ "loadFailed": "Nutzungsdaten konnten nicht geladen werden: {0}", "refreshFailed": "Nutzung konnte nicht aktualisiert werden: {0}", "thresholdSaveFailed": "Grenze konnte nicht gespeichert werden: {0}", + "powerUseSaveFailed": "PowerUse-Schalter konnte nicht gespeichert werden: {0}", "installStarted": "TokenTracker wird installiert — der Fortschritt läuft in der Log-Leiste.", "installFailed": "TokenTracker konnte nicht installiert werden: {0}", "analyticsRefreshFailed": "Die Auswertung konnte nicht aktualisiert werden: {0}", diff --git a/src/ClaudeDo.Localization/locales/en.json b/src/ClaudeDo.Localization/locales/en.json index e58e4780..d1a21582 100644 --- a/src/ClaudeDo.Localization/locales/en.json +++ b/src/ClaudeDo.Localization/locales/en.json @@ -41,6 +41,8 @@ "usageGateSevenDayPct": "7-day window (%)", "autoContinueOnUsageLimit": "Continue on session limit reset", "autoContinueOnUsageLimitHint": "When a run stops because it hit the usage limit, automatically continue it once the 5-hour window resets. Also wakes the paused queue exactly at reset time instead of waiting for the next backstop check.", + "listHandlerMaxHandoffs": "List-handler handoff budget", + "listHandlerMaxHandoffsHint": "How many wait/merge handoffs one \"Let Claude handle it\" run may chain. 4 means two merge rounds. Raise it for runs meant to keep going for hours; it bounds a runaway chain, so it cannot be 0.", "throttleStagesHeading": "Throttle stages", "throttleStagesHint": "Parallelism is capped before the hard gate: soft caps at 2 slots, hard at 1. Edited by dragging the markers on the usage-monitor gauges. Opening the monitor closes these settings — save first if you changed anything.", "throttleStagesValues": "5h: soft {0}% / hard {1}% · 7d: soft {2}% / hard {3}%", @@ -85,6 +87,7 @@ "retryPrompt": "Retry", "dailyPrepPrompt": "Daily prep", "weeklyReportPrompt": "Weekly report", + "powerUsePrompt": "PowerUse addendum", "viewPrompt": "View", "customizedSection": "CUSTOMIZED", "customizedHint": "These prompts differ from the bundled default and are kept as-is. Reset to pick up future default improvements.", @@ -398,6 +401,8 @@ "manualListHint": "New tasks in this list start out manual: no queueing, running or refining, and automation skips them. You can still open a hand-driven session.", "findingsTracked": "Commit the .claudedo findings folder", "findingsTrackedHint": "Off: the folder stays out of git via .git/info/exclude. On: findings travel with the repo.", + "powerUseAllowed": "Allow PowerUse for this list", + "powerUseAllowedHint": "Lets a list-handler run on this list go unattended when PowerUse is switched on in the usage monitor: it asks nothing, resolves conflicts itself, and harvests new work. Both switches are needed, so leaving PowerUse on globally never changes a list that hasn't opted in.", "sectionIcon": "ICON", "icon": "List icon", "iconHint": "Shown next to the list name in the sidebar.", @@ -504,6 +509,8 @@ "title": "USAGE MONITOR", "windowTitle": "Usage Monitor", "noGauges": "No usage limits reported yet.", + "powerUse": "PowerUse", + "powerUseHint": "Spend the rest of the window: a list-handler run asks nothing, resolves conflicts itself, harvests new work, and keeps handing off for as long as its budget allows. Only affects lists that allowed PowerUse in their list settings.", "refresh": "Refresh now", "refreshHint": "Polled every 5 min while a task runs, otherwise every 15 min.", "staleFormat": "Values stale (as of {0})", @@ -749,6 +756,7 @@ "loadFailed": "Couldn't load usage data: {0}", "refreshFailed": "Couldn't refresh usage: {0}", "thresholdSaveFailed": "Couldn't save the threshold: {0}", + "powerUseSaveFailed": "Couldn't save the PowerUse switch: {0}", "installStarted": "Installing TokenTracker — watch the log strip for progress.", "installFailed": "Couldn't install TokenTracker: {0}", "analyticsRefreshFailed": "Couldn't refresh the analytics: {0}", diff --git a/src/ClaudeDo.Ui/ViewModels/Agent/AgentConfigEditorViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Agent/AgentConfigEditorViewModel.cs index 8511e379..b8c6bb20 100644 --- a/src/ClaudeDo.Ui/ViewModels/Agent/AgentConfigEditorViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Agent/AgentConfigEditorViewModel.cs @@ -208,11 +208,13 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa catch { } } - // verifyCommand and ticketProjectId are List-only fields owned by ListSettingsModalViewModel - // (not this editor, which is also reused for Task scope); the caller passes them through so the - // single UpdateListConfig call carries the full desired row instead of clobbering it. + // verifyCommand, ticketProjectId and powerUseAllowed are List-only fields owned by + // ListSettingsModalViewModel (not this editor, which is also reused for Task scope); the caller + // passes them through so the single UpdateListConfig call carries the full desired row instead + // of clobbering it. powerUseAllowed stays null-by-default for the same reason ticketProjectId + // does: a caller that doesn't own it must not revoke the list's opt-in by omission. public async System.Threading.Tasks.Task SaveAsync( - string? verifyCommand = null, int? ticketProjectId = null) + string? verifyCommand = null, int? ticketProjectId = null, bool? powerUseAllowed = null) { if (TargetId is null) return; var model = string.IsNullOrWhiteSpace(Model) ? null : Model; @@ -225,7 +227,7 @@ public sealed partial class AgentConfigEditorViewModel : ViewModelBase, IDisposa if (_scope == AgentConfigScope.Task) await _worker.UpdateTaskAgentSettingsAsync(new UpdateTaskAgentSettingsDto(TargetId, model, sp, ap, turns, skills, permission)); else - await _worker.UpdateListConfigAsync(new UpdateListConfigDto(TargetId, model, sp, ap, turns, skills, verifyCommand, permission, ticketProjectId)); + await _worker.UpdateListConfigAsync(new UpdateListConfigDto(TargetId, model, sp, ap, turns, skills, verifyCommand, permission, ticketProjectId, powerUseAllowed)); } private List? SelectedSessionSkillNames() diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs index fe8a48bd..29824cf6 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs @@ -45,6 +45,10 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase // Optional post-merge verification command (build/test), run in WorkingDir after a merge // lands; a non-zero exit keeps the task out of Done instead of silently reporting merged. [ObservableProperty] private string _verifyCommand = ""; + // Per-list opt-in for PowerUse. A handler run on this list only goes unattended (no questions, + // harvests new work, longer handoff chain) when this AND the global switch in the usage monitor + // are both on. Off by default — the global switch alone must never change a list's behaviour. + [ObservableProperty] private bool _powerUseAllowed; // Dropdown hidden entirely when no ticket-system base URL is configured (Settings → Files tab). // Icon key into the UI icon library (IconKeyConverter -> "Icon." StreamGeometry). [ObservableProperty] private string _icon = DefaultIcon; @@ -110,6 +114,7 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase await Agent.LoadForListAsync(listId, ct); var cfg = await _worker.GetListConfigAsync(listId); VerifyCommand = cfg?.VerifyCommand ?? ""; + PowerUseAllowed = cfg?.PowerUseAllowed ?? false; var ticketSettings = TicketSettingsTabViewModel.FeatureEnabled ? await _worker.GetTicketSettingsAsync() @@ -149,9 +154,12 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase // Never null here, or an explicit unlink would silently fail to reach the worker. var ticketProjectId = TicketsAvailable ? (SelectedTicketProject?.Id ?? 0) : (int?)null; + // Always explicit (never null) — this modal owns the checkbox, so unchecking it has to reach + // the worker as a real false rather than a "leave it alone". await Agent.SaveAsync( string.IsNullOrWhiteSpace(VerifyCommand) ? null : VerifyCommand, - ticketProjectId); + ticketProjectId, + PowerUseAllowed); Saved = true; CloseAction?.Invoke(); diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs index fcd050fe..ffbb995f 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/FilesSettingsTabViewModel.cs @@ -30,6 +30,7 @@ public sealed partial class FilesSettingsTabViewModel : ViewModelBase public string RetryPromptPath { get; } = PromptFiles.PathFor(PromptKind.Retry); public string DailyPrepPromptPath { get; } = PromptFiles.PathFor(PromptKind.DailyPrep); public string WeeklyReportPromptPath { get; } = PromptFiles.PathFor(PromptKind.WeeklyReport); + public string PowerUsePromptPath { get; } = PromptFiles.PathFor(PromptKind.PowerUseAddendum); public ObservableCollection CustomizedPrompts { get; } = new(); diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/GeneralSettingsTabViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/GeneralSettingsTabViewModel.cs index e9af7a6b..ee22c4b3 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/GeneralSettingsTabViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/GeneralSettingsTabViewModel.cs @@ -27,6 +27,9 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase // Off by default: auto-continues a usage-limit-failed task once the 5h window resets, and // wakes a gate-blocked queue exactly at that reset time instead of only the 30s backstop. [ObservableProperty] private bool _autoContinueOnUsageLimit; + // Cap on a list-handler run's wait<->merge handoff chain. 4 = two merge rounds (the long-standing + // default); raise it for runs meant to keep going for hours. Bounds a runaway chain, so never 0. + [ObservableProperty] private int _listHandlerMaxHandoffs = 4; // Newline-separated path prefixes excluded from the weekly report. [ObservableProperty] private string _reportExcludedPaths = @"C:\Private"; // 0=Sunday..6=Saturday (System.DayOfWeek); default Wednesday. diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs index 9bc1ade9..b24fa6f7 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs @@ -130,6 +130,10 @@ public sealed partial class SettingsModalViewModel : ViewModelBase // carried through load→save verbatim so saving Settings can never reset a dragged value. private (int FiveSoft, int FiveHard, int SevenSoft, int SevenHard) _throttleStages = (50, 65, 50, 65); + // Same deal: the PowerUse master switch is toggled on the usage monitor. Carried load→save so + // opening and saving Settings can't switch it off behind the user's back. + private bool _powerUseEnabled; + public async Task LoadAsync() { IsBusy = true; @@ -147,6 +151,11 @@ public sealed partial class SettingsModalViewModel : ViewModelBase General.UsageGateFiveHourPct = dto.UsageGateFiveHourPct; General.UsageGateSevenDayPct = dto.UsageGateSevenDayPct; General.AutoContinueOnUsageLimit = dto.AutoContinueOnUsageLimit; + General.ListHandlerMaxHandoffs = dto.ListHandlerMaxHandoffs; + // Owned by the usage monitor, not editable here — but it MUST be round-tripped, + // because SaveAsync rebuilds the whole DTO positionally and would otherwise write + // the record default (false) back and silently switch PowerUse off. + _powerUseEnabled = dto.PowerUseEnabled; _throttleStages = ( dto.UsageThrottleFiveHourSoftPct, dto.UsageThrottleFiveHourHardPct, dto.UsageThrottleSevenDaySoftPct, dto.UsageThrottleSevenDayHardPct); @@ -213,7 +222,9 @@ public sealed partial class SettingsModalViewModel : ViewModelBase _throttleStages.FiveHard, _throttleStages.SevenSoft, _throttleStages.SevenHard, - General.AutoContinueOnUsageLimit); + General.AutoContinueOnUsageLimit, + _powerUseEnabled, + General.ListHandlerMaxHandoffs); await _worker.UpdateAppSettingsAsync(dto); await Prime.SaveAsync(); await OnlineInbox.SaveAsync(); diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/UsageMonitorModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/UsageMonitorModalViewModel.cs index 0d358aa8..d4e93539 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/UsageMonitorModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/UsageMonitorModalViewModel.cs @@ -100,6 +100,45 @@ public sealed partial class UsageMonitorModalViewModel : ViewModelBase [ObservableProperty] private IReadOnlyList _gaugeRows = Array.Empty(); + /// + /// PowerUse master switch. It lives here rather than in Settings because turning it on is a + /// "spend the rest of the window" decision, made while looking at how much window is left. + /// On its own it changes nothing — a list-handler run only goes unattended when its LIST also + /// opted in (list settings → PowerUse), so this can stay on without affecting other lists. + /// + [ObservableProperty] private bool _powerUseEnabled; + + // Set while LoadAsync writes the stored value in, so seeding the property doesn't immediately + // write it straight back to the worker. + private bool _loadingPowerUse; + + partial void OnPowerUseEnabledChanged(bool value) + { + if (_loadingPowerUse) return; + _ = SavePowerUseAsync(value); + } + + /// Read-modify-write, same discipline as SaveStagesAsync: never clobber a field this modal + /// doesn't own. + private async Task SavePowerUseAsync(bool value) + { + try + { + var settings = await _worker.GetAppSettingsAsync(); + if (settings is null) + { + ErrorReported?.Invoke(Loc.T("vm.usageMonitor.powerUseSaveFailed", Loc.T("vm.settingsModal.workerOffline"))); + return; + } + + await _worker.UpdateAppSettingsAsync(settings with { PowerUseEnabled = value }); + } + catch (Exception ex) + { + ErrorReported?.Invoke(Loc.T("vm.usageMonitor.powerUseSaveFailed", ex.Message)); + } + } + public bool IsStale => Snapshot?.IsStale == true; public string? LastError => Snapshot?.LastError; public bool IsGateBlocked => Snapshot?.IsGateBlocked == true; @@ -148,6 +187,15 @@ public sealed partial class UsageMonitorModalViewModel : ViewModelBase Snapshot = snapshot; _worker.UsageUpdatedEvent -= OnUsageUpdated; _worker.UsageUpdatedEvent += OnUsageUpdated; + + var settings = await _worker.GetAppSettingsAsync(); + if (settings is not null) + { + _loadingPowerUse = true; + try { PowerUseEnabled = settings.PowerUseEnabled; } + finally { _loadingPowerUse = false; } + } + ApplyPresetRange(SelectedPresetDays); await LoadUsageDataAsync(); } diff --git a/src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml b/src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml index 0e404dd8..e75ca032 100644 --- a/src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml +++ b/src/ClaudeDo.Ui/Views/Modals/ListSettingsModalView.axaml @@ -103,6 +103,13 @@ Opacity="0.6" FontSize="12" TextWrapping="Wrap"/> + + + + + + + + + + + + + + + + + + +