diff --git a/docs/explore-notes/conpty-sessions.md b/docs/explore-notes/conpty-sessions.md index d88df85e..1f1c0141 100644 --- a/docs/explore-notes/conpty-sessions.md +++ b/docs/explore-notes/conpty-sessions.md @@ -121,7 +121,7 @@ per call and `Register` clamps to min 1, so 0 can never mean "unbounded"). Past `wait`/`merge` request is silently coerced to its `_final` counterpart — check the result's `coerced` flag — and any handoff after a `merge_final` round throws. -**PowerUse.** When `AppSettingsEntity.PowerUseEnabled` (toggled on the usage monitor) **and** the +**PowerUse.** When `AppSettingsEntity.PowerUseEnabled` (Settings → Execution) **and** the list's own `ListConfigEntity.PowerUseAllowed` are both set, `InteractiveLaunchSpecService. ComposeHandlerPromptAsync` appends `PromptKind.PowerUseAddendum` to whichever handler prompt the phase resolved to. The addendum is written as an *override* of the base prompt's interactive rules diff --git a/docs/explore-notes/usage-monitoring.md b/docs/explore-notes/usage-monitoring.md index f775d108..b5841d62 100644 --- a/docs/explore-notes/usage-monitoring.md +++ b/docs/explore-notes/usage-monitoring.md @@ -254,6 +254,7 @@ degrades instead of throwing. Worker config: `usage_poll_interval_active_seconds `usage_poll_interval_idle_seconds`. The gate percentages are editable in **two** places that both write the same `app_settings` row: -Settings → General (typed) and the usage-monitor gauges (dragged). The throttle stages are +Settings → Execution (typed) and the usage-monitor gauges (dragged). The throttle stages are gauge-only — `SettingsModalViewModel` therefore carries them load→save verbatim so saving Settings -can't reset a dragged value. +can't reset a dragged value. PowerUse (switch, time window, handoff %) used to be usage-monitor-only +and is now typed on the same Settings tab, so it needs no such round-trip. diff --git a/src/ClaudeDo.Data/CLAUDE.md b/src/ClaudeDo.Data/CLAUDE.md index 411b203d..c13a8c6d 100644 --- a/src/ClaudeDo.Data/CLAUDE.md +++ b/src/ClaudeDo.Data/CLAUDE.md @@ -39,10 +39,10 @@ Beyond the basics it carries: | `ReportExcludedPaths` | `report_excluded_paths` | null | JSON array of excluded path prefixes. | | `StandupWeekday` | `standup_weekday` | Wednesday | int `DayOfWeek`. | | `NextTaskNumber` | `next_task_number` | 1 | Persistent counter for `TaskEntity.Number` allocation. Incremented atomically in `TaskNumberAllocator.AddWithNumberAsync` on every task insert. **Monotonic only:** the counter itself advances across all tasks (no per-list numbering), and it never resets. A deleted task leaves a gap. | -| `PowerUseEnabled` | `power_use_enabled` | false | PowerUse master switch, toggled on the **usage monitor** (not Settings). ⚠️ Never sufficient alone — a handler run is in PowerUse only when `list_config.power_use_allowed` is also set. | +| `PowerUseEnabled` | `power_use_enabled` | false | PowerUse master switch, toggled in **Settings → Execution**. ⚠️ Never sufficient alone — a handler run is in PowerUse only when `list_config.power_use_allowed` is also set. | | `ListHandlerMaxHandoffs` | `list_handler_max_handoffs` | 4 | Cap on a list-handler run's wait↔merge handoff chain (`HandoffRoundTracker`). 4 = two merge rounds. Independent of PowerUse — applies to interactive runs too. Clamped to min 1 in `WorkerHub.UpdateAppSettings` and again in `Register`, because 0 would remove the only bound on a runaway chain. | | `PowerUseWindowStart` / `PowerUseWindowEnd` | `power_use_window_*` | null | Optional local-time window (`HH:mm`) narrowing when PowerUse applies. Both null (or only one set) = round the clock, i.e. byte-identical to the pre-window behaviour; end before start wraps over midnight. Semantics live in the pure `PowerUseWindow`; checked in `InteractiveLaunchSpecService.ComposeHandlerPromptAsync` **per phase** at prompt-build time, so a chain handing off past the end time drops the addendum from there on. Unparseable input is stored as null, not rejected. | -| `PowerUseHandoffPct` | `power_use_handoff_pct` | 95 | 5h-usage percentage at which a PowerUse handler session should hand off. Clamped 50..99. Stored + shown on the usage monitor; the consumer is the handoff prompt. | +| `PowerUseHandoffPct` | `power_use_handoff_pct` | 95 | 5h-usage percentage at which a PowerUse handler session should hand off. Clamped 50..99. Typed in Settings → Execution; the consumer is the handoff prompt. | All four usage percentages are clamped 0..100 by `AppSettingsRepository.UpdateAsync`. Gate/throttle semantics → [usage-monitoring](../../docs/explore-notes/usage-monitoring.md). diff --git a/src/ClaudeDo.Data/Models/AppSettingsEntity.cs b/src/ClaudeDo.Data/Models/AppSettingsEntity.cs index 6b86f378..42cd6ed4 100644 --- a/src/ClaudeDo.Data/Models/AppSettingsEntity.cs +++ b/src/ClaudeDo.Data/Models/AppSettingsEntity.cs @@ -62,8 +62,7 @@ public sealed class AppSettingsEntity public bool AutoContinueOnUsageLimit { get; set; } // Master switch for PowerUse: list-handler runs go unattended (never ask the user) and harvest - // new work when a round would otherwise starve. Toggled from the usage monitor, because it is a - // deliberate "spend the remaining window" decision rather than a normal default. Off by default, + // new work when a round would otherwise starve. Toggled in Settings → Execution. Off by default, // and on its own it changes nothing — a run is only in PowerUse when the LIST also opted in via // ListConfigEntity.PowerUseAllowed, so leaving this on cannot make an unrelated list's handler // run unattended. Composition lives in InteractiveLaunchSpecService. diff --git a/src/ClaudeDo.Data/Wire.cs b/src/ClaudeDo.Data/Wire.cs index 5d119d62..456f0469 100644 --- a/src/ClaudeDo.Data/Wire.cs +++ b/src/ClaudeDo.Data/Wire.cs @@ -37,13 +37,12 @@ public record AppSettingsDto( int UsageThrottleSevenDaySoftPct = 50, int UsageThrottleSevenDayHardPct = 65, bool AutoContinueOnUsageLimit = false, - // PowerUse master switch — toggled on the usage monitor, not typed in Settings. Only takes - // effect for a list that also set PowerUseAllowed. + // PowerUse master switch — Settings → Execution. Only takes effect for a list that also set + // PowerUseAllowed. bool PowerUseEnabled = false, int ListHandlerMaxHandoffs = 4, // PowerUse time window (`HH:mm`, local, null = round the clock) + the 5h-usage percentage at - // which a PowerUse handler session should hand off. Also usage-monitor-owned, so Settings has - // to round-trip them too. + // which a PowerUse handler session should hand off. Same Settings tab. string? PowerUseWindowStart = null, string? PowerUseWindowEnd = null, int PowerUseHandoffPct = 95); diff --git a/src/ClaudeDo.Localization/locales/de.json b/src/ClaudeDo.Localization/locales/de.json index c5f0498a..e47a2eb6 100644 --- a/src/ClaudeDo.Localization/locales/de.json +++ b/src/ClaudeDo.Localization/locales/de.json @@ -43,6 +43,13 @@ "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.", + "powerUseHeading": "PowerUse", + "powerUse": "PowerUse aktivieren", + "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.", + "powerUseWindow": "Nur zwischen", + "powerUseWindowPlaceholder": "HH:mm", + "powerUseHandoffPct": "Übergabe bei (% von 5h)", + "powerUseWindowHint": "Beide Zeiten leer = rund um die Uhr. Das Fenster wird bei jedem Start einer Handler-Phase geprüft — eine Kette, die über das Ende hinausläuft, macht ohne PowerUse weiter.", "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}%", @@ -332,6 +339,7 @@ "failed": "Fehlgeschlagen", "roadblock": "Blockade gemeldet", "openSession": "Sitzung öffnen", + "homeFolder": "Home (Schnellsitzung)", "browseForFolder": "Ordner wählen…", "focusMode": "Fokus", "overviewMode": "Übersicht", @@ -407,7 +415,7 @@ "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.", + "powerUseAllowedHint": "Erlaubt einem List-Handler-Run auf dieser Liste, unbeaufsichtigt zu laufen, sobald PowerUse in Einstellungen → Ausführung 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.", @@ -510,18 +518,18 @@ "regenerate": "Neu erstellen", "emptyStateHint": "Noch kein Bericht für diesen Zeitraum. Klicke auf „Erstellen“." }, + "cheatSheets": { + "title": "CHEAT SHEETS", + "windowTitle": "Cheat Sheets", + "pin": "Anpinnen", + "pinTooltip": "Fenster über den anderen halten", + "missing": "Cheat Sheet {0} nicht gefunden." + }, "usageMonitor": { "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.", - "powerUseWindow": "Nur zwischen", - "powerUseWindowPlaceholder": "HH:mm", - "powerUseHandoffPct": "Übergabe bei (% von 5h)", - "powerUseWindowHint": "Beide Zeiten leer = rund um die Uhr. Das Fenster wird bei jedem Start einer Handler-Phase geprüft — eine Kette, die über das Ende hinausläuft, macht ohne PowerUse weiter.", - "refresh": "Jetzt aktualisieren", - "refreshHint": "Abruf alle 5 Min., während ein Task läuft, sonst alle 15 Min.", + "refreshHint": "Jetzt aktualisieren — Abruf alle 5 Min., während ein Task läuft, sonst alle 15 Min.", "staleFormat": "Werte veraltet (Stand {0})", "staleGateHint": "Das Gate greift in diesem Zustand nicht.", "gateBlockedFormat": "Warteschlange pausiert — {0}", @@ -710,6 +718,9 @@ "abort": "Merge abbrechen" }, "controls": { + "markdown": { + "copy": "Befehl kopieren" + }, "datePicker": { "today": "Heute", "tomorrow": "Morgen", @@ -729,6 +740,13 @@ "worktrees": "Worktrees…", "weeklyReport": "Wochenbericht…", "usageMonitor": "Usage Monitor…", + "cheatSheets": "Cheat Sheets", + "cheatSheetGit": "Git…", + "cheatSheetCommits": "Conventional Commits…", + "cheatSheetClaude": "Claude CLI…", + "cheatSheetClaudeConfig": "Claude-Konfiguration…", + "cheatSheetDotnet": "dotnet CLI…", + "cheatSheetPowershell": "PowerShell…", "about": "Über…", "addRepos": "Repos als Listen hinzufügen…" }, @@ -765,7 +783,6 @@ "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 3b9b3c3e..5f0398fb 100644 --- a/src/ClaudeDo.Localization/locales/en.json +++ b/src/ClaudeDo.Localization/locales/en.json @@ -43,6 +43,13 @@ "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.", + "powerUseHeading": "PowerUse", + "powerUse": "Enable 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.", + "powerUseWindow": "Only between", + "powerUseWindowPlaceholder": "HH:mm", + "powerUseHandoffPct": "Hand off at (% of 5h)", + "powerUseWindowHint": "Leave both times empty for round the clock. A window is checked whenever a handler phase starts, so a chain that runs past the end time continues without PowerUse.", "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}%", @@ -332,6 +339,7 @@ "failed": "Failed", "roadblock": "Roadblock reported", "openSession": "Open session", + "homeFolder": "Home (quick session)", "browseForFolder": "Choose folder…", "focusMode": "Focus", "overviewMode": "Overview", @@ -407,7 +415,7 @@ "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.", + "powerUseAllowedHint": "Lets a list-handler run on this list go unattended when PowerUse is switched on in Settings → Execution: 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.", @@ -510,18 +518,18 @@ "regenerate": "Regenerate", "emptyStateHint": "No report for this range yet. Click “Generate”." }, + "cheatSheets": { + "title": "CHEAT SHEETS", + "windowTitle": "Cheat Sheets", + "pin": "Pin", + "pinTooltip": "Keep this window on top of the others", + "missing": "Cheat sheet {0} not found." + }, "usageMonitor": { "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.", - "powerUseWindow": "Only between", - "powerUseWindowPlaceholder": "HH:mm", - "powerUseHandoffPct": "Hand off at (% of 5h)", - "powerUseWindowHint": "Leave both times empty for round the clock. A window is checked whenever a handler phase starts, so a chain that runs past the end time continues without PowerUse.", - "refresh": "Refresh now", - "refreshHint": "Polled every 5 min while a task runs, otherwise every 15 min.", + "refreshHint": "Refresh now — polled every 5 min while a task runs, otherwise every 15 min.", "staleFormat": "Values stale (as of {0})", "staleGateHint": "The gate does not apply while values are stale.", "gateBlockedFormat": "Queue paused — {0}", @@ -710,6 +718,9 @@ "abort": "Abort merge" }, "controls": { + "markdown": { + "copy": "Copy command" + }, "datePicker": { "today": "Today", "tomorrow": "Tomorrow", @@ -729,6 +740,13 @@ "worktrees": "Worktrees…", "weeklyReport": "Weekly Report…", "usageMonitor": "Usage Monitor…", + "cheatSheets": "Cheat Sheets", + "cheatSheetGit": "Git…", + "cheatSheetCommits": "Conventional Commits…", + "cheatSheetClaude": "Claude CLI…", + "cheatSheetClaudeConfig": "Claude configuration…", + "cheatSheetDotnet": "dotnet CLI…", + "cheatSheetPowershell": "PowerShell…", "about": "About…", "addRepos": "Add repos as lists…" }, @@ -765,7 +783,6 @@ "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/Modals/ListSettingsModalViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs index 0406759c..80f4c7b9 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/ListSettingsModalViewModel.cs @@ -49,8 +49,8 @@ public sealed partial class ListSettingsModalViewModel : ViewModelBase // 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. + // harvests new work, longer handoff chain) when this AND the global switch in Settings → + // Execution 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). diff --git a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/GeneralSettingsTabViewModel.cs b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/GeneralSettingsTabViewModel.cs index ee22c4b3..7fb528cd 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/Settings/GeneralSettingsTabViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/Settings/GeneralSettingsTabViewModel.cs @@ -30,6 +30,15 @@ public sealed partial class GeneralSettingsTabViewModel : ViewModelBase // 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; + // PowerUse master switch: a handler run spends the rest of the window unattended. On its own it + // changes nothing — the run's LIST must also opt in (list settings → PowerUse). + [ObservableProperty] private bool _powerUseEnabled; + // Optional local-time window (HH:mm) narrowing when PowerUse applies; both blank = round the + // clock. Unparseable input is normalized to null on save, same as the worker does. + [ObservableProperty] private string? _powerUseWindowStart; + [ObservableProperty] private string? _powerUseWindowEnd; + // 5h-usage percentage at which a PowerUse handler session hands off. Clamped 50..99. + [ObservableProperty] private int _powerUseHandoffPct = 95; // 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 aadfb141..65f1f4b5 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/SettingsModalViewModel.cs @@ -1,5 +1,6 @@ using System.Linq; using ClaudeDo.Data; +using ClaudeDo.Data.Models; using ClaudeDo.Localization; using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; @@ -130,14 +131,6 @@ 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; - - // Ditto for the PowerUse time window and handoff threshold — usage-monitor-owned, invisible - // here, but they must survive a Settings save. - private (string? Start, string? End, int HandoffPct) _powerUseWindow = (null, null, 95); - public async Task LoadAsync() { IsBusy = true; @@ -156,11 +149,10 @@ public sealed partial class SettingsModalViewModel : ViewModelBase 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; - _powerUseWindow = (dto.PowerUseWindowStart, dto.PowerUseWindowEnd, dto.PowerUseHandoffPct); + General.PowerUseEnabled = dto.PowerUseEnabled; + General.PowerUseWindowStart = dto.PowerUseWindowStart; + General.PowerUseWindowEnd = dto.PowerUseWindowEnd; + General.PowerUseHandoffPct = dto.PowerUseHandoffPct; _throttleStages = ( dto.UsageThrottleFiveHourSoftPct, dto.UsageThrottleFiveHourHardPct, dto.UsageThrottleSevenDaySoftPct, dto.UsageThrottleSevenDayHardPct); @@ -228,11 +220,13 @@ public sealed partial class SettingsModalViewModel : ViewModelBase _throttleStages.SevenSoft, _throttleStages.SevenHard, General.AutoContinueOnUsageLimit, - _powerUseEnabled, + General.PowerUseEnabled, General.ListHandlerMaxHandoffs, - _powerUseWindow.Start, - _powerUseWindow.End, - _powerUseWindow.HandoffPct); + // The worker stores an unparseable time as null; normalize here too so the box + // shows what was actually saved instead of a typo that looks like a live window. + PowerUseWindow.Normalize(General.PowerUseWindowStart), + PowerUseWindow.Normalize(General.PowerUseWindowEnd), + Math.Clamp(General.PowerUseHandoffPct, 50, 99)); 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 92bbb23e..180b738c 100644 --- a/src/ClaudeDo.Ui/ViewModels/Modals/UsageMonitorModalViewModel.cs +++ b/src/ClaudeDo.Ui/ViewModels/Modals/UsageMonitorModalViewModel.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using ClaudeDo.Data.Models; using ClaudeDo.Ui.Localization; using ClaudeDo.Ui.Services; using CommunityToolkit.Mvvm.ComponentModel; @@ -101,77 +100,6 @@ 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; - - /// Optional local-time window (HH:mm) narrowing when PowerUse applies. Both - /// blank = round the clock. Committed on focus loss / Enter, not per keystroke. - [ObservableProperty] private string? _powerUseWindowStart; - [ObservableProperty] private string? _powerUseWindowEnd; - - /// 5h-usage percentage at which a PowerUse handler session should hand off. - [ObservableProperty] private int _powerUseHandoffPct = 95; - - // 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; - _ = SavePowerUse(); - } - - /// Read-modify-write, same discipline as SaveStagesAsync: never clobber a field this modal - /// doesn't own. All four PowerUse fields go in one write, so committing one box can't drop a - /// value the user just typed into a sibling box. - [RelayCommand] - private async Task SavePowerUse() - { - if (_loadingPowerUse) return; - try - { - var settings = await _worker.GetAppSettingsAsync(); - if (settings is null) - { - ErrorReported?.Invoke(Loc.T("vm.usageMonitor.powerUseSaveFailed", Loc.T("vm.settingsModal.workerOffline"))); - return; - } - - // The worker stores an unparseable time as null; normalize here too so the box shows - // what was actually saved instead of a typo that looks like a live window. - var start = PowerUseWindow.Normalize(PowerUseWindowStart); - var end = PowerUseWindow.Normalize(PowerUseWindowEnd); - var pct = Math.Clamp(PowerUseHandoffPct, 50, 99); - - await _worker.UpdateAppSettingsAsync(settings with - { - PowerUseEnabled = PowerUseEnabled, - PowerUseWindowStart = start, - PowerUseWindowEnd = end, - PowerUseHandoffPct = pct, - }); - - _loadingPowerUse = true; - try - { - PowerUseWindowStart = start; - PowerUseWindowEnd = end; - PowerUseHandoffPct = pct; - } - finally { _loadingPowerUse = false; } - } - 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; @@ -221,20 +149,6 @@ public sealed partial class UsageMonitorModalViewModel : ViewModelBase _worker.UsageUpdatedEvent -= OnUsageUpdated; _worker.UsageUpdatedEvent += OnUsageUpdated; - var settings = await _worker.GetAppSettingsAsync(); - if (settings is not null) - { - _loadingPowerUse = true; - try - { - PowerUseEnabled = settings.PowerUseEnabled; - PowerUseWindowStart = settings.PowerUseWindowStart; - PowerUseWindowEnd = settings.PowerUseWindowEnd; - PowerUseHandoffPct = settings.PowerUseHandoffPct; - } - finally { _loadingPowerUse = false; } - } - ApplyPresetRange(SelectedPresetDays); await LoadUsageDataAsync(); } diff --git a/src/ClaudeDo.Ui/Views/Controls/ModalShell.axaml b/src/ClaudeDo.Ui/Views/Controls/ModalShell.axaml index 5bf693bd..c44ceb45 100644 --- a/src/ClaudeDo.Ui/Views/Controls/ModalShell.axaml +++ b/src/ClaudeDo.Ui/Views/Controls/ModalShell.axaml @@ -14,19 +14,21 @@ Background="{DynamicResource DeepBrush}" BorderBrush="{DynamicResource LineBrush}" BorderThickness="0,0,0,1"> - + - -