feat(ui): PowerUse-Einstellungen nach Settings -> Ausfuehrung verschieben
Schalter, Zeitfenster und Handoff-Prozent lagen im Usage-Monitor und mussten in SettingsModalViewModel nur load->save durchgeschleift werden, damit ein Settings-Speichern sie nicht zurueckwirft. Jetzt gehoeren sie dem General-Tab, der Monitor ist wieder nur Anzeige + Threshold-Drag, und "Jetzt aktualisieren" sitzt als Icon in der Titelleiste (neuer ModalShell.HeaderActions-Slot).
This commit is contained in:
@@ -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
|
`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.
|
`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.
|
list's own `ListConfigEntity.PowerUseAllowed` are both set, `InteractiveLaunchSpecService.
|
||||||
ComposeHandlerPromptAsync` appends `PromptKind.PowerUseAddendum` to whichever handler prompt the
|
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
|
phase resolved to. The addendum is written as an *override* of the base prompt's interactive rules
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ degrades instead of throwing. Worker config: `usage_poll_interval_active_seconds
|
|||||||
`usage_poll_interval_idle_seconds`.
|
`usage_poll_interval_idle_seconds`.
|
||||||
|
|
||||||
The gate percentages are editable in **two** places that both write the same `app_settings` row:
|
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
|
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.
|
||||||
|
|||||||
@@ -39,10 +39,10 @@ Beyond the basics it carries:
|
|||||||
| `ReportExcludedPaths` | `report_excluded_paths` | null | JSON array of excluded path prefixes. |
|
| `ReportExcludedPaths` | `report_excluded_paths` | null | JSON array of excluded path prefixes. |
|
||||||
| `StandupWeekday` | `standup_weekday` | Wednesday | int `DayOfWeek`. |
|
| `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. |
|
| `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. |
|
| `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. |
|
| `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`.
|
All four usage percentages are clamped 0..100 by `AppSettingsRepository.UpdateAsync`.
|
||||||
Gate/throttle semantics → [usage-monitoring](../../docs/explore-notes/usage-monitoring.md).
|
Gate/throttle semantics → [usage-monitoring](../../docs/explore-notes/usage-monitoring.md).
|
||||||
|
|||||||
@@ -62,8 +62,7 @@ public sealed class AppSettingsEntity
|
|||||||
public bool AutoContinueOnUsageLimit { get; set; }
|
public bool AutoContinueOnUsageLimit { get; set; }
|
||||||
|
|
||||||
// Master switch for PowerUse: list-handler runs go unattended (never ask the user) and harvest
|
// 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
|
// new work when a round would otherwise starve. Toggled in Settings → Execution. Off by default,
|
||||||
// deliberate "spend the remaining window" decision rather than a normal default. Off by default,
|
|
||||||
// and on its own it changes nothing — a run is only in PowerUse when the LIST also opted in via
|
// 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
|
// ListConfigEntity.PowerUseAllowed, so leaving this on cannot make an unrelated list's handler
|
||||||
// run unattended. Composition lives in InteractiveLaunchSpecService.
|
// run unattended. Composition lives in InteractiveLaunchSpecService.
|
||||||
|
|||||||
@@ -37,13 +37,12 @@ public record AppSettingsDto(
|
|||||||
int UsageThrottleSevenDaySoftPct = 50,
|
int UsageThrottleSevenDaySoftPct = 50,
|
||||||
int UsageThrottleSevenDayHardPct = 65,
|
int UsageThrottleSevenDayHardPct = 65,
|
||||||
bool AutoContinueOnUsageLimit = false,
|
bool AutoContinueOnUsageLimit = false,
|
||||||
// PowerUse master switch — toggled on the usage monitor, not typed in Settings. Only takes
|
// PowerUse master switch — Settings → Execution. Only takes effect for a list that also set
|
||||||
// effect for a list that also set PowerUseAllowed.
|
// PowerUseAllowed.
|
||||||
bool PowerUseEnabled = false,
|
bool PowerUseEnabled = false,
|
||||||
int ListHandlerMaxHandoffs = 4,
|
int ListHandlerMaxHandoffs = 4,
|
||||||
// PowerUse time window (`HH:mm`, local, null = round the clock) + the 5h-usage percentage at
|
// 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
|
// which a PowerUse handler session should hand off. Same Settings tab.
|
||||||
// to round-trip them too.
|
|
||||||
string? PowerUseWindowStart = null,
|
string? PowerUseWindowStart = null,
|
||||||
string? PowerUseWindowEnd = null,
|
string? PowerUseWindowEnd = null,
|
||||||
int PowerUseHandoffPct = 95);
|
int PowerUseHandoffPct = 95);
|
||||||
|
|||||||
@@ -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.",
|
"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",
|
"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.",
|
"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",
|
"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.",
|
"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}%",
|
"throttleStagesValues": "5h: soft {0}% / hard {1}% · 7d: soft {2}% / hard {3}%",
|
||||||
@@ -332,6 +339,7 @@
|
|||||||
"failed": "Fehlgeschlagen",
|
"failed": "Fehlgeschlagen",
|
||||||
"roadblock": "Blockade gemeldet",
|
"roadblock": "Blockade gemeldet",
|
||||||
"openSession": "Sitzung öffnen",
|
"openSession": "Sitzung öffnen",
|
||||||
|
"homeFolder": "Home (Schnellsitzung)",
|
||||||
"browseForFolder": "Ordner wählen…",
|
"browseForFolder": "Ordner wählen…",
|
||||||
"focusMode": "Fokus",
|
"focusMode": "Fokus",
|
||||||
"overviewMode": "Übersicht",
|
"overviewMode": "Übersicht",
|
||||||
@@ -407,7 +415,7 @@
|
|||||||
"findingsTracked": "Findings-Ordner .claudedo einchecken",
|
"findingsTracked": "Findings-Ordner .claudedo einchecken",
|
||||||
"findingsTrackedHint": "Aus: der Ordner bleibt über .git/info/exclude aus git heraus. An: Findings reisen mit dem Repo.",
|
"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",
|
"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",
|
"sectionIcon": "ICON",
|
||||||
"icon": "Listen-Icon",
|
"icon": "Listen-Icon",
|
||||||
"iconHint": "Wird in der Seitenleiste neben dem Listennamen angezeigt.",
|
"iconHint": "Wird in der Seitenleiste neben dem Listennamen angezeigt.",
|
||||||
@@ -510,18 +518,18 @@
|
|||||||
"regenerate": "Neu erstellen",
|
"regenerate": "Neu erstellen",
|
||||||
"emptyStateHint": "Noch kein Bericht für diesen Zeitraum. Klicke auf „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": {
|
"usageMonitor": {
|
||||||
"title": "USAGE MONITOR",
|
"title": "USAGE MONITOR",
|
||||||
"windowTitle": "Usage Monitor",
|
"windowTitle": "Usage Monitor",
|
||||||
"noGauges": "Noch keine Nutzungslimits gemeldet.",
|
"noGauges": "Noch keine Nutzungslimits gemeldet.",
|
||||||
"powerUse": "PowerUse",
|
"refreshHint": "Jetzt aktualisieren — Abruf alle 5 Min., während ein Task läuft, sonst alle 15 Min.",
|
||||||
"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.",
|
|
||||||
"staleFormat": "Werte veraltet (Stand {0})",
|
"staleFormat": "Werte veraltet (Stand {0})",
|
||||||
"staleGateHint": "Das Gate greift in diesem Zustand nicht.",
|
"staleGateHint": "Das Gate greift in diesem Zustand nicht.",
|
||||||
"gateBlockedFormat": "Warteschlange pausiert — {0}",
|
"gateBlockedFormat": "Warteschlange pausiert — {0}",
|
||||||
@@ -710,6 +718,9 @@
|
|||||||
"abort": "Merge abbrechen"
|
"abort": "Merge abbrechen"
|
||||||
},
|
},
|
||||||
"controls": {
|
"controls": {
|
||||||
|
"markdown": {
|
||||||
|
"copy": "Befehl kopieren"
|
||||||
|
},
|
||||||
"datePicker": {
|
"datePicker": {
|
||||||
"today": "Heute",
|
"today": "Heute",
|
||||||
"tomorrow": "Morgen",
|
"tomorrow": "Morgen",
|
||||||
@@ -729,6 +740,13 @@
|
|||||||
"worktrees": "Worktrees…",
|
"worktrees": "Worktrees…",
|
||||||
"weeklyReport": "Wochenbericht…",
|
"weeklyReport": "Wochenbericht…",
|
||||||
"usageMonitor": "Usage Monitor…",
|
"usageMonitor": "Usage Monitor…",
|
||||||
|
"cheatSheets": "Cheat Sheets",
|
||||||
|
"cheatSheetGit": "Git…",
|
||||||
|
"cheatSheetCommits": "Conventional Commits…",
|
||||||
|
"cheatSheetClaude": "Claude CLI…",
|
||||||
|
"cheatSheetClaudeConfig": "Claude-Konfiguration…",
|
||||||
|
"cheatSheetDotnet": "dotnet CLI…",
|
||||||
|
"cheatSheetPowershell": "PowerShell…",
|
||||||
"about": "Über…",
|
"about": "Über…",
|
||||||
"addRepos": "Repos als Listen hinzufügen…"
|
"addRepos": "Repos als Listen hinzufügen…"
|
||||||
},
|
},
|
||||||
@@ -765,7 +783,6 @@
|
|||||||
"loadFailed": "Nutzungsdaten konnten nicht geladen werden: {0}",
|
"loadFailed": "Nutzungsdaten konnten nicht geladen werden: {0}",
|
||||||
"refreshFailed": "Nutzung konnte nicht aktualisiert werden: {0}",
|
"refreshFailed": "Nutzung konnte nicht aktualisiert werden: {0}",
|
||||||
"thresholdSaveFailed": "Grenze konnte nicht gespeichert 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.",
|
"installStarted": "TokenTracker wird installiert — der Fortschritt läuft in der Log-Leiste.",
|
||||||
"installFailed": "TokenTracker konnte nicht installiert werden: {0}",
|
"installFailed": "TokenTracker konnte nicht installiert werden: {0}",
|
||||||
"analyticsRefreshFailed": "Die Auswertung konnte nicht aktualisiert werden: {0}",
|
"analyticsRefreshFailed": "Die Auswertung konnte nicht aktualisiert werden: {0}",
|
||||||
|
|||||||
@@ -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.",
|
"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",
|
"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.",
|
"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",
|
"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.",
|
"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}%",
|
"throttleStagesValues": "5h: soft {0}% / hard {1}% · 7d: soft {2}% / hard {3}%",
|
||||||
@@ -332,6 +339,7 @@
|
|||||||
"failed": "Failed",
|
"failed": "Failed",
|
||||||
"roadblock": "Roadblock reported",
|
"roadblock": "Roadblock reported",
|
||||||
"openSession": "Open session",
|
"openSession": "Open session",
|
||||||
|
"homeFolder": "Home (quick session)",
|
||||||
"browseForFolder": "Choose folder…",
|
"browseForFolder": "Choose folder…",
|
||||||
"focusMode": "Focus",
|
"focusMode": "Focus",
|
||||||
"overviewMode": "Overview",
|
"overviewMode": "Overview",
|
||||||
@@ -407,7 +415,7 @@
|
|||||||
"findingsTracked": "Commit the .claudedo findings folder",
|
"findingsTracked": "Commit the .claudedo findings folder",
|
||||||
"findingsTrackedHint": "Off: the folder stays out of git via .git/info/exclude. On: findings travel with the repo.",
|
"findingsTrackedHint": "Off: the folder stays out of git via .git/info/exclude. On: findings travel with the repo.",
|
||||||
"powerUseAllowed": "Allow PowerUse for this list",
|
"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",
|
"sectionIcon": "ICON",
|
||||||
"icon": "List icon",
|
"icon": "List icon",
|
||||||
"iconHint": "Shown next to the list name in the sidebar.",
|
"iconHint": "Shown next to the list name in the sidebar.",
|
||||||
@@ -510,18 +518,18 @@
|
|||||||
"regenerate": "Regenerate",
|
"regenerate": "Regenerate",
|
||||||
"emptyStateHint": "No report for this range yet. Click “Generate”."
|
"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": {
|
"usageMonitor": {
|
||||||
"title": "USAGE MONITOR",
|
"title": "USAGE MONITOR",
|
||||||
"windowTitle": "Usage Monitor",
|
"windowTitle": "Usage Monitor",
|
||||||
"noGauges": "No usage limits reported yet.",
|
"noGauges": "No usage limits reported yet.",
|
||||||
"powerUse": "PowerUse",
|
"refreshHint": "Refresh now — polled every 5 min while a task runs, otherwise every 15 min.",
|
||||||
"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.",
|
|
||||||
"staleFormat": "Values stale (as of {0})",
|
"staleFormat": "Values stale (as of {0})",
|
||||||
"staleGateHint": "The gate does not apply while values are stale.",
|
"staleGateHint": "The gate does not apply while values are stale.",
|
||||||
"gateBlockedFormat": "Queue paused — {0}",
|
"gateBlockedFormat": "Queue paused — {0}",
|
||||||
@@ -710,6 +718,9 @@
|
|||||||
"abort": "Abort merge"
|
"abort": "Abort merge"
|
||||||
},
|
},
|
||||||
"controls": {
|
"controls": {
|
||||||
|
"markdown": {
|
||||||
|
"copy": "Copy command"
|
||||||
|
},
|
||||||
"datePicker": {
|
"datePicker": {
|
||||||
"today": "Today",
|
"today": "Today",
|
||||||
"tomorrow": "Tomorrow",
|
"tomorrow": "Tomorrow",
|
||||||
@@ -729,6 +740,13 @@
|
|||||||
"worktrees": "Worktrees…",
|
"worktrees": "Worktrees…",
|
||||||
"weeklyReport": "Weekly Report…",
|
"weeklyReport": "Weekly Report…",
|
||||||
"usageMonitor": "Usage Monitor…",
|
"usageMonitor": "Usage Monitor…",
|
||||||
|
"cheatSheets": "Cheat Sheets",
|
||||||
|
"cheatSheetGit": "Git…",
|
||||||
|
"cheatSheetCommits": "Conventional Commits…",
|
||||||
|
"cheatSheetClaude": "Claude CLI…",
|
||||||
|
"cheatSheetClaudeConfig": "Claude configuration…",
|
||||||
|
"cheatSheetDotnet": "dotnet CLI…",
|
||||||
|
"cheatSheetPowershell": "PowerShell…",
|
||||||
"about": "About…",
|
"about": "About…",
|
||||||
"addRepos": "Add repos as lists…"
|
"addRepos": "Add repos as lists…"
|
||||||
},
|
},
|
||||||
@@ -765,7 +783,6 @@
|
|||||||
"loadFailed": "Couldn't load usage data: {0}",
|
"loadFailed": "Couldn't load usage data: {0}",
|
||||||
"refreshFailed": "Couldn't refresh usage: {0}",
|
"refreshFailed": "Couldn't refresh usage: {0}",
|
||||||
"thresholdSaveFailed": "Couldn't save the threshold: {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.",
|
"installStarted": "Installing TokenTracker — watch the log strip for progress.",
|
||||||
"installFailed": "Couldn't install TokenTracker: {0}",
|
"installFailed": "Couldn't install TokenTracker: {0}",
|
||||||
"analyticsRefreshFailed": "Couldn't refresh the analytics: {0}",
|
"analyticsRefreshFailed": "Couldn't refresh the analytics: {0}",
|
||||||
|
|||||||
@@ -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.
|
// lands; a non-zero exit keeps the task out of Done instead of silently reporting merged.
|
||||||
[ObservableProperty] private string _verifyCommand = "";
|
[ObservableProperty] private string _verifyCommand = "";
|
||||||
// Per-list opt-in for PowerUse. A handler run on this list only goes unattended (no questions,
|
// 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
|
// harvests new work, longer handoff chain) when this AND the global switch in Settings →
|
||||||
// are both on. Off by default — the global switch alone must never change a list's behaviour.
|
// Execution are both on. Off by default — the global switch alone must never change a list's behaviour.
|
||||||
[ObservableProperty] private bool _powerUseAllowed;
|
[ObservableProperty] private bool _powerUseAllowed;
|
||||||
// Dropdown hidden entirely when no ticket-system base URL is configured (Settings → Files tab).
|
// 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).
|
// Icon key into the UI icon library (IconKeyConverter -> "Icon.<key>" StreamGeometry).
|
||||||
|
|||||||
@@ -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
|
// 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.
|
// default); raise it for runs meant to keep going for hours. Bounds a runaway chain, so never 0.
|
||||||
[ObservableProperty] private int _listHandlerMaxHandoffs = 4;
|
[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.
|
// Newline-separated path prefixes excluded from the weekly report.
|
||||||
[ObservableProperty] private string _reportExcludedPaths = @"C:\Private";
|
[ObservableProperty] private string _reportExcludedPaths = @"C:\Private";
|
||||||
// 0=Sunday..6=Saturday (System.DayOfWeek); default Wednesday.
|
// 0=Sunday..6=Saturday (System.DayOfWeek); default Wednesday.
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
using System.Linq;
|
using System.Linq;
|
||||||
using ClaudeDo.Data;
|
using ClaudeDo.Data;
|
||||||
|
using ClaudeDo.Data.Models;
|
||||||
using ClaudeDo.Localization;
|
using ClaudeDo.Localization;
|
||||||
using ClaudeDo.Ui.Localization;
|
using ClaudeDo.Ui.Localization;
|
||||||
using ClaudeDo.Ui.Services;
|
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.
|
// 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);
|
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()
|
public async Task LoadAsync()
|
||||||
{
|
{
|
||||||
IsBusy = true;
|
IsBusy = true;
|
||||||
@@ -156,11 +149,10 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
|||||||
General.UsageGateSevenDayPct = dto.UsageGateSevenDayPct;
|
General.UsageGateSevenDayPct = dto.UsageGateSevenDayPct;
|
||||||
General.AutoContinueOnUsageLimit = dto.AutoContinueOnUsageLimit;
|
General.AutoContinueOnUsageLimit = dto.AutoContinueOnUsageLimit;
|
||||||
General.ListHandlerMaxHandoffs = dto.ListHandlerMaxHandoffs;
|
General.ListHandlerMaxHandoffs = dto.ListHandlerMaxHandoffs;
|
||||||
// Owned by the usage monitor, not editable here — but it MUST be round-tripped,
|
General.PowerUseEnabled = dto.PowerUseEnabled;
|
||||||
// because SaveAsync rebuilds the whole DTO positionally and would otherwise write
|
General.PowerUseWindowStart = dto.PowerUseWindowStart;
|
||||||
// the record default (false) back and silently switch PowerUse off.
|
General.PowerUseWindowEnd = dto.PowerUseWindowEnd;
|
||||||
_powerUseEnabled = dto.PowerUseEnabled;
|
General.PowerUseHandoffPct = dto.PowerUseHandoffPct;
|
||||||
_powerUseWindow = (dto.PowerUseWindowStart, dto.PowerUseWindowEnd, dto.PowerUseHandoffPct);
|
|
||||||
_throttleStages = (
|
_throttleStages = (
|
||||||
dto.UsageThrottleFiveHourSoftPct, dto.UsageThrottleFiveHourHardPct,
|
dto.UsageThrottleFiveHourSoftPct, dto.UsageThrottleFiveHourHardPct,
|
||||||
dto.UsageThrottleSevenDaySoftPct, dto.UsageThrottleSevenDayHardPct);
|
dto.UsageThrottleSevenDaySoftPct, dto.UsageThrottleSevenDayHardPct);
|
||||||
@@ -228,11 +220,13 @@ public sealed partial class SettingsModalViewModel : ViewModelBase
|
|||||||
_throttleStages.SevenSoft,
|
_throttleStages.SevenSoft,
|
||||||
_throttleStages.SevenHard,
|
_throttleStages.SevenHard,
|
||||||
General.AutoContinueOnUsageLimit,
|
General.AutoContinueOnUsageLimit,
|
||||||
_powerUseEnabled,
|
General.PowerUseEnabled,
|
||||||
General.ListHandlerMaxHandoffs,
|
General.ListHandlerMaxHandoffs,
|
||||||
_powerUseWindow.Start,
|
// The worker stores an unparseable time as null; normalize here too so the box
|
||||||
_powerUseWindow.End,
|
// shows what was actually saved instead of a typo that looks like a live window.
|
||||||
_powerUseWindow.HandoffPct);
|
PowerUseWindow.Normalize(General.PowerUseWindowStart),
|
||||||
|
PowerUseWindow.Normalize(General.PowerUseWindowEnd),
|
||||||
|
Math.Clamp(General.PowerUseHandoffPct, 50, 99));
|
||||||
await _worker.UpdateAppSettingsAsync(dto);
|
await _worker.UpdateAppSettingsAsync(dto);
|
||||||
await Prime.SaveAsync();
|
await Prime.SaveAsync();
|
||||||
await OnlineInbox.SaveAsync();
|
await OnlineInbox.SaveAsync();
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ using System;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
using ClaudeDo.Data.Models;
|
|
||||||
using ClaudeDo.Ui.Localization;
|
using ClaudeDo.Ui.Localization;
|
||||||
using ClaudeDo.Ui.Services;
|
using ClaudeDo.Ui.Services;
|
||||||
using CommunityToolkit.Mvvm.ComponentModel;
|
using CommunityToolkit.Mvvm.ComponentModel;
|
||||||
@@ -101,77 +100,6 @@ public sealed partial class UsageMonitorModalViewModel : ViewModelBase
|
|||||||
[ObservableProperty]
|
[ObservableProperty]
|
||||||
private IReadOnlyList<UsageGaugeRowViewModel> _gaugeRows = Array.Empty<UsageGaugeRowViewModel>();
|
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;
|
|
||||||
|
|
||||||
/// <summary>Optional local-time window (<c>HH:mm</c>) narrowing when PowerUse applies. Both
|
|
||||||
/// blank = round the clock. Committed on focus loss / Enter, not per keystroke.</summary>
|
|
||||||
[ObservableProperty] private string? _powerUseWindowStart;
|
|
||||||
[ObservableProperty] private string? _powerUseWindowEnd;
|
|
||||||
|
|
||||||
/// <summary>5h-usage percentage at which a PowerUse handler session should hand off.</summary>
|
|
||||||
[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 bool IsStale => Snapshot?.IsStale == true;
|
||||||
public string? LastError => Snapshot?.LastError;
|
public string? LastError => Snapshot?.LastError;
|
||||||
public bool IsGateBlocked => Snapshot?.IsGateBlocked == true;
|
public bool IsGateBlocked => Snapshot?.IsGateBlocked == true;
|
||||||
@@ -221,20 +149,6 @@ public sealed partial class UsageMonitorModalViewModel : ViewModelBase
|
|||||||
_worker.UsageUpdatedEvent -= OnUsageUpdated;
|
_worker.UsageUpdatedEvent -= OnUsageUpdated;
|
||||||
_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);
|
ApplyPresetRange(SelectedPresetDays);
|
||||||
await LoadUsageDataAsync();
|
await LoadUsageDataAsync();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,19 +14,21 @@
|
|||||||
Background="{DynamicResource DeepBrush}"
|
Background="{DynamicResource DeepBrush}"
|
||||||
BorderBrush="{DynamicResource LineBrush}"
|
BorderBrush="{DynamicResource LineBrush}"
|
||||||
BorderThickness="0,0,0,1">
|
BorderThickness="0,0,0,1">
|
||||||
<Grid ColumnDefinitions="*,Auto,Auto" Margin="14,0">
|
<Grid ColumnDefinitions="*,Auto,Auto,Auto" Margin="14,0">
|
||||||
<TextBlock Text="{TemplateBinding Title}"
|
<TextBlock Text="{TemplateBinding Title}"
|
||||||
FontFamily="{DynamicResource MonoFont}"
|
FontFamily="{DynamicResource MonoFont}"
|
||||||
FontSize="{DynamicResource FontSizeMono}"
|
FontSize="{DynamicResource FontSizeMono}"
|
||||||
LetterSpacing="1.4"
|
LetterSpacing="1.4"
|
||||||
Foreground="{DynamicResource TextBrush}"
|
Foreground="{DynamicResource TextBrush}"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
<Button Name="PART_MaxButton" Grid.Column="1" Classes="icon-btn"
|
<ContentPresenter Grid.Column="1" Content="{TemplateBinding HeaderActions}"
|
||||||
|
VerticalAlignment="Center" Margin="0,0,8,0"/>
|
||||||
|
<Button Name="PART_MaxButton" Grid.Column="2" Classes="icon-btn"
|
||||||
IsVisible="{TemplateBinding ShowMaximizeButton}"
|
IsVisible="{TemplateBinding ShowMaximizeButton}"
|
||||||
VerticalAlignment="Center">
|
VerticalAlignment="Center">
|
||||||
<PathIcon Name="PART_MaxIcon" Data="{StaticResource Icon.WinMax}" Width="10" Height="10"/>
|
<PathIcon Name="PART_MaxIcon" Data="{StaticResource Icon.WinMax}" Width="10" Height="10"/>
|
||||||
</Button>
|
</Button>
|
||||||
<Button Grid.Column="2" Classes="icon-btn" Content="✕"
|
<Button Grid.Column="3" Classes="icon-btn" Content="✕"
|
||||||
FontSize="{DynamicResource FontSizeBody}"
|
FontSize="{DynamicResource FontSizeBody}"
|
||||||
Command="{TemplateBinding CloseCommand}"
|
Command="{TemplateBinding CloseCommand}"
|
||||||
VerticalAlignment="Center"/>
|
VerticalAlignment="Center"/>
|
||||||
|
|||||||
@@ -17,6 +17,11 @@ public class ModalShell : ContentControl
|
|||||||
public static readonly StyledProperty<object?> FooterProperty =
|
public static readonly StyledProperty<object?> FooterProperty =
|
||||||
AvaloniaProperty.Register<ModalShell, object?>(nameof(Footer));
|
AvaloniaProperty.Register<ModalShell, object?>(nameof(Footer));
|
||||||
|
|
||||||
|
// Window-level actions rendered in the titlebar, left of maximise/close. Keeps a single
|
||||||
|
// always-visible action (e.g. the usage monitor's refresh) out of the body's vertical flow.
|
||||||
|
public static readonly StyledProperty<object?> HeaderActionsProperty =
|
||||||
|
AvaloniaProperty.Register<ModalShell, object?>(nameof(HeaderActions));
|
||||||
|
|
||||||
public static readonly StyledProperty<ICommand?> CloseCommandProperty =
|
public static readonly StyledProperty<ICommand?> CloseCommandProperty =
|
||||||
AvaloniaProperty.Register<ModalShell, ICommand?>(nameof(CloseCommand));
|
AvaloniaProperty.Register<ModalShell, ICommand?>(nameof(CloseCommand));
|
||||||
|
|
||||||
@@ -27,6 +32,7 @@ public class ModalShell : ContentControl
|
|||||||
|
|
||||||
public string? Title { get => GetValue(TitleProperty); set => SetValue(TitleProperty, value); }
|
public string? Title { get => GetValue(TitleProperty); set => SetValue(TitleProperty, value); }
|
||||||
public object? Footer { get => GetValue(FooterProperty); set => SetValue(FooterProperty, value); }
|
public object? Footer { get => GetValue(FooterProperty); set => SetValue(FooterProperty, value); }
|
||||||
|
public object? HeaderActions { get => GetValue(HeaderActionsProperty); set => SetValue(HeaderActionsProperty, value); }
|
||||||
public ICommand? CloseCommand { get => GetValue(CloseCommandProperty); set => SetValue(CloseCommandProperty, value); }
|
public ICommand? CloseCommand { get => GetValue(CloseCommandProperty); set => SetValue(CloseCommandProperty, value); }
|
||||||
public bool ShowMaximizeButton { get => GetValue(ShowMaximizeButtonProperty); set => SetValue(ShowMaximizeButtonProperty, value); }
|
public bool ShowMaximizeButton { get => GetValue(ShowMaximizeButtonProperty); set => SetValue(ShowMaximizeButtonProperty, value); }
|
||||||
|
|
||||||
|
|||||||
@@ -252,8 +252,8 @@
|
|||||||
<TextBlock Text="{loc:Tr settings.general.autoContinueOnUsageLimitHint}"
|
<TextBlock Text="{loc:Tr settings.general.autoContinueOnUsageLimitHint}"
|
||||||
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
|
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
|
||||||
|
|
||||||
<!-- PowerUse itself is toggled on the usage monitor; only its handoff budget is
|
<!-- Outside the PowerUse section below on purpose: the handoff budget also bounds
|
||||||
typed here, because it also applies to a normal (interactive) handler run. -->
|
a normal (interactive) handler run. -->
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
<TextBlock Classes="eyebrow" Text="{loc:Tr settings.general.listHandlerMaxHandoffs}"/>
|
<TextBlock Classes="eyebrow" Text="{loc:Tr settings.general.listHandlerMaxHandoffs}"/>
|
||||||
<NumericUpDown Value="{Binding General.ListHandlerMaxHandoffs, Mode=TwoWay, Converter={StaticResource KeepLastNumber}}"
|
<NumericUpDown Value="{Binding General.ListHandlerMaxHandoffs, Mode=TwoWay, Converter={StaticResource KeepLastNumber}}"
|
||||||
@@ -264,6 +264,34 @@
|
|||||||
</StackPanel>
|
</StackPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
|
<!-- PowerUse: a master switch plus its time window. Never sufficient on its own —
|
||||||
|
the run's list must also allow PowerUse in its list settings. -->
|
||||||
|
<StackPanel Spacing="4">
|
||||||
|
<TextBlock Classes="field-label" Text="{loc:Tr settings.general.powerUseHeading}"/>
|
||||||
|
<CheckBox IsChecked="{Binding General.PowerUseEnabled, Mode=TwoWay}"
|
||||||
|
Content="{loc:Tr settings.general.powerUse}"/>
|
||||||
|
<TextBlock Text="{loc:Tr settings.general.powerUseHint}"
|
||||||
|
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,6,0,0">
|
||||||
|
<TextBlock Classes="eyebrow" VerticalAlignment="Center"
|
||||||
|
Text="{loc:Tr settings.general.powerUseWindow}"/>
|
||||||
|
<TextBox Width="70" FontSize="12" Padding="6,2"
|
||||||
|
Watermark="{loc:Tr settings.general.powerUseWindowPlaceholder}"
|
||||||
|
Text="{Binding General.PowerUseWindowStart, Mode=TwoWay}"/>
|
||||||
|
<TextBlock Classes="meta" VerticalAlignment="Center" Text="–"/>
|
||||||
|
<TextBox Width="70" FontSize="12" Padding="6,2"
|
||||||
|
Watermark="{loc:Tr settings.general.powerUseWindowPlaceholder}"
|
||||||
|
Text="{Binding General.PowerUseWindowEnd, Mode=TwoWay}"/>
|
||||||
|
<TextBlock Classes="eyebrow" VerticalAlignment="Center" Margin="12,0,0,0"
|
||||||
|
Text="{loc:Tr settings.general.powerUseHandoffPct}"/>
|
||||||
|
<NumericUpDown Width="80" FontSize="12" Padding="6,2" ShowButtonSpinner="False"
|
||||||
|
Minimum="50" Maximum="99" Increment="1" FormatString="0"
|
||||||
|
Value="{Binding General.PowerUseHandoffPct, Mode=TwoWay, Converter={StaticResource KeepLastNumber}}"/>
|
||||||
|
</StackPanel>
|
||||||
|
<TextBlock Text="{loc:Tr settings.general.powerUseWindowHint}"
|
||||||
|
Opacity="0.6" FontSize="12" TextWrapping="Wrap"/>
|
||||||
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Throttle stages live on the usage-monitor gauges (drag + spinner per stage);
|
<!-- Throttle stages live on the usage-monitor gauges (drag + spinner per stage);
|
||||||
echoed read-only here so they aren't invisible on the tab that owns the gate. -->
|
echoed read-only here so they aren't invisible on the tab that owns the gate. -->
|
||||||
<StackPanel Spacing="4">
|
<StackPanel Spacing="4">
|
||||||
|
|||||||
@@ -19,6 +19,18 @@
|
|||||||
</Window.KeyBindings>
|
</Window.KeyBindings>
|
||||||
|
|
||||||
<ctl:ModalShell Title="{loc:Tr modals.usageMonitor.title}" CloseCommand="{Binding CloseCommand}">
|
<ctl:ModalShell Title="{loc:Tr modals.usageMonitor.title}" CloseCommand="{Binding CloseCommand}">
|
||||||
|
<!-- Refresh sits in the titlebar: the worker polls on a slow cadence to avoid the endpoint's
|
||||||
|
429s, so this is the way to get a number now — but it doesn't earn a row in the body. -->
|
||||||
|
<ctl:ModalShell.HeaderActions>
|
||||||
|
<StackPanel Orientation="Horizontal" Spacing="6">
|
||||||
|
<Ellipse Classes="spinner" Width="12" Height="12" VerticalAlignment="Center"
|
||||||
|
IsVisible="{Binding IsRefreshing}"/>
|
||||||
|
<Button Classes="icon-btn" Content="⟳" FontSize="{DynamicResource FontSizeBody}"
|
||||||
|
Command="{Binding RefreshCommand}"
|
||||||
|
IsEnabled="{Binding !IsRefreshing}"
|
||||||
|
ToolTip.Tip="{loc:Tr modals.usageMonitor.refreshHint}"/>
|
||||||
|
</StackPanel>
|
||||||
|
</ctl:ModalShell.HeaderActions>
|
||||||
<DockPanel>
|
<DockPanel>
|
||||||
|
|
||||||
<!-- Stale / gate bands -->
|
<!-- Stale / gate bands -->
|
||||||
@@ -47,18 +59,6 @@
|
|||||||
</Border>
|
</Border>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
|
|
||||||
<!-- Refresh: the worker polls on a slow cadence to avoid the endpoint's 429s -->
|
|
||||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8"
|
|
||||||
Margin="20,12,20,0" VerticalAlignment="Center">
|
|
||||||
<Button Classes="btn" Content="{loc:Tr modals.usageMonitor.refresh}"
|
|
||||||
Command="{Binding RefreshCommand}"
|
|
||||||
IsEnabled="{Binding !IsRefreshing}"/>
|
|
||||||
<Ellipse Classes="spinner" Width="14" Height="14" VerticalAlignment="Center"
|
|
||||||
IsVisible="{Binding IsRefreshing}"/>
|
|
||||||
<TextBlock Classes="meta" VerticalAlignment="Center"
|
|
||||||
Text="{loc:Tr modals.usageMonitor.refreshHint}"/>
|
|
||||||
</StackPanel>
|
|
||||||
|
|
||||||
<!-- Gauges -->
|
<!-- Gauges -->
|
||||||
<ItemsControl DockPanel.Dock="Top" Margin="20,12,20,0" ItemsSource="{Binding GaugeRows}">
|
<ItemsControl DockPanel.Dock="Top" Margin="20,12,20,0" ItemsSource="{Binding GaugeRows}">
|
||||||
<ItemsControl.ItemsPanel>
|
<ItemsControl.ItemsPanel>
|
||||||
@@ -134,42 +134,6 @@
|
|||||||
Text="{loc:Tr modals.usageMonitor.noGauges}"
|
Text="{loc:Tr modals.usageMonitor.noGauges}"
|
||||||
IsVisible="{Binding !GaugeRows.Count}"/>
|
IsVisible="{Binding !GaugeRows.Count}"/>
|
||||||
|
|
||||||
<!-- PowerUse: sits under the gauges on purpose — "spend the rest of the window" is a
|
|
||||||
decision made while looking at how much window is left. Saves immediately on toggle. -->
|
|
||||||
<Border DockPanel.Dock="Top" Margin="20,12,20,0" Padding="12,10"
|
|
||||||
Background="{DynamicResource DeepBrush}" BorderBrush="{DynamicResource LineBrush}"
|
|
||||||
BorderThickness="1" CornerRadius="6">
|
|
||||||
<StackPanel Spacing="4">
|
|
||||||
<CheckBox IsChecked="{Binding PowerUseEnabled, Mode=TwoWay}"
|
|
||||||
Content="{loc:Tr modals.usageMonitor.powerUse}"/>
|
|
||||||
<TextBlock Classes="meta" TextWrapping="Wrap"
|
|
||||||
Text="{loc:Tr modals.usageMonitor.powerUseHint}"/>
|
|
||||||
<!-- Window + handoff threshold. Same commit-on-blur/Enter discipline as the gauge
|
|
||||||
legend boxes: a hub write per keystroke would be one save per typed digit. -->
|
|
||||||
<StackPanel Orientation="Horizontal" Spacing="8" Margin="0,6,0,0">
|
|
||||||
<TextBlock Classes="meta" VerticalAlignment="Center"
|
|
||||||
Text="{loc:Tr modals.usageMonitor.powerUseWindow}"/>
|
|
||||||
<TextBox Width="70" FontSize="12" Padding="6,2"
|
|
||||||
Watermark="{loc:Tr modals.usageMonitor.powerUseWindowPlaceholder}"
|
|
||||||
Text="{Binding PowerUseWindowStart, Mode=TwoWay}"
|
|
||||||
LostFocus="OnPowerUseBoxCommit" KeyDown="OnPowerUseBoxKeyDown"/>
|
|
||||||
<TextBlock Classes="meta" VerticalAlignment="Center" Text="–"/>
|
|
||||||
<TextBox Width="70" FontSize="12" Padding="6,2"
|
|
||||||
Watermark="{loc:Tr modals.usageMonitor.powerUseWindowPlaceholder}"
|
|
||||||
Text="{Binding PowerUseWindowEnd, Mode=TwoWay}"
|
|
||||||
LostFocus="OnPowerUseBoxCommit" KeyDown="OnPowerUseBoxKeyDown"/>
|
|
||||||
<TextBlock Classes="meta" VerticalAlignment="Center" Margin="12,0,0,0"
|
|
||||||
Text="{loc:Tr modals.usageMonitor.powerUseHandoffPct}"/>
|
|
||||||
<NumericUpDown Width="80" FontSize="12" Padding="6,2" ShowButtonSpinner="False"
|
|
||||||
Minimum="50" Maximum="99" Increment="1" FormatString="0"
|
|
||||||
Value="{Binding PowerUseHandoffPct, Mode=TwoWay, Converter={StaticResource KeepLastNumber}}"
|
|
||||||
LostFocus="OnPowerUseBoxCommit" KeyDown="OnPowerUseBoxKeyDown"/>
|
|
||||||
</StackPanel>
|
|
||||||
<TextBlock Classes="meta" TextWrapping="Wrap" Opacity="0.75"
|
|
||||||
Text="{loc:Tr modals.usageMonitor.powerUseWindowHint}"/>
|
|
||||||
</StackPanel>
|
|
||||||
</Border>
|
|
||||||
|
|
||||||
<!-- Period picker -->
|
<!-- Period picker -->
|
||||||
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8" Margin="20,12,20,0">
|
<StackPanel DockPanel.Dock="Top" Orientation="Horizontal" Spacing="8" Margin="20,12,20,0">
|
||||||
<TextBlock Classes="meta" Text="{loc:Tr modals.usageMonitor.period}" VerticalAlignment="Center"/>
|
<TextBlock Classes="meta" Text="{loc:Tr modals.usageMonitor.period}" VerticalAlignment="Center"/>
|
||||||
|
|||||||
@@ -33,19 +33,4 @@ public partial class UsageMonitorModalView : Window
|
|||||||
OnStageBoxCommit(sender, e);
|
OnStageBoxCommit(sender, e);
|
||||||
e.Handled = true;
|
e.Handled = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Persists the PowerUse window / handoff threshold on blur — a save per keystroke
|
|
||||||
/// would be one hub round-trip per typed digit.</summary>
|
|
||||||
private void OnPowerUseBoxCommit(object? sender, RoutedEventArgs e)
|
|
||||||
{
|
|
||||||
if (DataContext is not UsageMonitorModalViewModel vm) return;
|
|
||||||
if (vm.SavePowerUseCommand.CanExecute(null)) vm.SavePowerUseCommand.Execute(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnPowerUseBoxKeyDown(object? sender, KeyEventArgs e)
|
|
||||||
{
|
|
||||||
if (e.Key != Key.Enter) return;
|
|
||||||
OnPowerUseBoxCommit(sender, e);
|
|
||||||
e.Handled = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -607,8 +607,8 @@ public sealed class InteractiveLaunchSpecService
|
|||||||
|
|
||||||
// The list-handler system prompt, plus the PowerUse addendum when this run is in PowerUse.
|
// The list-handler system prompt, plus the PowerUse addendum when this run is in PowerUse.
|
||||||
//
|
//
|
||||||
// PowerUse requires BOTH the global switch (AppSettingsEntity.PowerUseEnabled, toggled on the
|
// PowerUse requires BOTH the global switch (AppSettingsEntity.PowerUseEnabled, Settings →
|
||||||
// usage monitor) AND the list's own opt-in (ListConfigEntity.PowerUseAllowed). That is the whole
|
// Execution) AND the list's own opt-in (ListConfigEntity.PowerUseAllowed). That is the whole
|
||||||
// point of the two-key design: the global switch can be left on overnight without turning some
|
// point of the two-key design: the global switch can be left on overnight without turning some
|
||||||
// unrelated list's handler run unattended, because a list that never opted in keeps the
|
// unrelated list's handler run unattended, because a list that never opted in keeps the
|
||||||
// interactive prompt unchanged.
|
// interactive prompt unchanged.
|
||||||
|
|||||||
@@ -131,6 +131,38 @@ public class SettingsModalViewModelTests : IDisposable
|
|||||||
Assert.Equal(77, worker.Saved.UsageGateSevenDayPct);
|
Assert.Equal(77, worker.Saved.UsageGateSevenDayPct);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task PowerUse_survives_a_load_edit_save_round_trip()
|
||||||
|
{
|
||||||
|
// PowerUse moved off the usage monitor onto this tab, so Settings is now its only writer.
|
||||||
|
// A binding that doesn't reach General silently resets the switch to the record default.
|
||||||
|
var worker = new FakeWorker
|
||||||
|
{
|
||||||
|
AppToReturn = DtoWith(80, 90) with
|
||||||
|
{
|
||||||
|
PowerUseEnabled = true,
|
||||||
|
PowerUseWindowStart = "18:00",
|
||||||
|
PowerUseWindowEnd = "06:00",
|
||||||
|
PowerUseHandoffPct = 88,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
var vm = MakeVm(worker);
|
||||||
|
await vm.LoadAsync();
|
||||||
|
|
||||||
|
Assert.True(vm.General.PowerUseEnabled);
|
||||||
|
Assert.Equal("18:00", vm.General.PowerUseWindowStart);
|
||||||
|
Assert.Equal(88, vm.General.PowerUseHandoffPct);
|
||||||
|
|
||||||
|
// A typo in the window is normalized to null on save, not stored as a fake window.
|
||||||
|
vm.General.PowerUseWindowEnd = "not a time";
|
||||||
|
await vm.SaveCommand.ExecuteAsync(null);
|
||||||
|
|
||||||
|
Assert.True(worker.Saved!.PowerUseEnabled);
|
||||||
|
Assert.Equal("18:00", worker.Saved.PowerUseWindowStart);
|
||||||
|
Assert.Null(worker.Saved.PowerUseWindowEnd);
|
||||||
|
Assert.Equal(88, worker.Saved.PowerUseHandoffPct);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task HasLinkedRepo_false_when_no_list_has_a_working_dir()
|
public async Task HasLinkedRepo_false_when_no_list_has_a_working_dir()
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ using Xunit;
|
|||||||
|
|
||||||
namespace ClaudeDo.Worker.Tests.Hub;
|
namespace ClaudeDo.Worker.Tests.Hub;
|
||||||
|
|
||||||
/// The PowerUse window and handoff threshold are edited on the usage monitor, but Settings→General
|
/// Settings rebuilds the whole AppSettings DTO positionally on save, so the contract that matters
|
||||||
/// rebuilds the whole DTO positionally on save. So the contract that matters is: whatever
|
/// is: whatever GetAppSettings hands out, feeding it straight back must change nothing.
|
||||||
/// GetAppSettings hands out, feeding it straight back must change nothing.
|
|
||||||
public sealed class PowerUseSettingsHubTests : IDisposable
|
public sealed class PowerUseSettingsHubTests : IDisposable
|
||||||
{
|
{
|
||||||
private readonly DbFixture _db = new();
|
private readonly DbFixture _db = new();
|
||||||
|
|||||||
@@ -72,8 +72,8 @@ public sealed class HandlerRestartCoordinatorTests : IDisposable
|
|||||||
});
|
});
|
||||||
await ctx.SaveChangesAsync();
|
await ctx.SaveChangesAsync();
|
||||||
|
|
||||||
// Written straight to the row: AppSettingsRepository.UpdateAsync deliberately doesn't
|
// Written straight to the row so the fixture doesn't depend on the repository's
|
||||||
// copy PowerUseEnabled (the usage monitor owns that toggle, not the settings modal).
|
// clamping/normalizing copy block.
|
||||||
await new AppSettingsRepository(ctx).GetAsync(); // materializes the singleton row
|
await new AppSettingsRepository(ctx).GetAsync(); // materializes the singleton row
|
||||||
var settings = await ctx.AppSettings.FirstAsync(s => s.Id == AppSettingsEntity.SingletonId);
|
var settings = await ctx.AppSettings.FirstAsync(s => s.Id == AppSettingsEntity.SingletonId);
|
||||||
settings.PowerUseEnabled = powerUseEnabled;
|
settings.PowerUseEnabled = powerUseEnabled;
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ public sealed class HandlerUsageGuardTests : IDisposable
|
|||||||
});
|
});
|
||||||
await ctx.SaveChangesAsync();
|
await ctx.SaveChangesAsync();
|
||||||
|
|
||||||
// Written straight to the row: AppSettingsRepository.UpdateAsync deliberately doesn't
|
// Written straight to the row so the fixture doesn't depend on the repository's
|
||||||
// copy PowerUseEnabled (the usage monitor owns that toggle, not the settings modal).
|
// clamping/normalizing copy block.
|
||||||
await new AppSettingsRepository(ctx).GetAsync();
|
await new AppSettingsRepository(ctx).GetAsync();
|
||||||
var settings = await ctx.AppSettings.FirstAsync(s => s.Id == AppSettingsEntity.SingletonId);
|
var settings = await ctx.AppSettings.FirstAsync(s => s.Id == AppSettingsEntity.SingletonId);
|
||||||
settings.PowerUseEnabled = powerUseEnabled;
|
settings.PowerUseEnabled = powerUseEnabled;
|
||||||
|
|||||||
Reference in New Issue
Block a user