feat(ui): PowerUse-Toggle im Usage-Monitor, Opt-in pro Liste
Der Toggle sitzt unter den Gauges im Usage-Monitor, weil "das restliche Fenster ausnutzen" eine Entscheidung ist, die man beim Blick auf das restliche Fenster trifft. Er speichert sofort per Read-modify-write, gleiche Disziplin wie SaveStagesAsync, damit er keine Felder ueberschreibt, die er nicht besitzt. Zwei Stellen, die das Opt-in still ausgeschaltet haetten: - WorkerHub.UpdateListConfig loescht die Config-Zeile, wenn alle Felder leer sind. PowerUseAllowed muss in diese Leer-Pruefung, sonst verschwindet das Opt-in sofort wieder, wenn es das einzige gesetzte Feld ist. - SettingsModalViewModel baut das AppSettingsDto positional neu auf. Ohne Round-trip haette jedes Speichern der Settings PowerUseEnabled auf den Record-Default false zurueckgesetzt. Gleiches Muster wie _throttleStages. Das Handoff-Budget ist als Spinner in Settings -> General editierbar, weil es auch fuer normale (interaktive) Handler-Runs gilt und nicht an PowerUse haengt. Das Addendum selbst ist ueber Settings -> Files anpassbar.
This commit is contained in:
@@ -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.<key>" 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();
|
||||
|
||||
@@ -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<CustomizedPromptRowViewModel> CustomizedPrompts { get; } = new();
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -100,6 +100,45 @@ public sealed partial class UsageMonitorModalViewModel : ViewModelBase
|
||||
[ObservableProperty]
|
||||
private IReadOnlyList<UsageGaugeRowViewModel> _gaugeRows = Array.Empty<UsageGaugeRowViewModel>();
|
||||
|
||||
/// <summary>
|
||||
/// 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.
|
||||
/// </summary>
|
||||
[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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user