feat: pace-based utilization tracking (#16) - #24
Merged
Conversation
Port the pace-based utilization tracking from eddmann/ClaudeMeter PR #33 into meter-core (pure domain). Upstream's PaceSignalTests are the numeric oracle; all 20 cases are ported as Rust tests. - pacing.rs: add UNDERUSE_THRESHOLD (0.8), HEAVY_OVERUSE_THRESHOLD (2.5), MIN_USAGE_FOR_PROJECTION (5.0) and weekly_pacing_duration(days). Add UsageWindow::{expected_usage_percent, pace_ratio, projected_end_percent, projected_limit_date}, each threading pacing_duration: Option<SignedDuration> with elapsed measured against the pacing span (capped) for expected/ratio and against the full window for the projections. Add the 4-band PaceBand enum. Refactor PacingAssessment to delegate to pace_ratio (no duplicated math). - pace_signal.rs: PaceKind {Hot, Cold} and PaceSignal with the exact-string tooltip() builder. - snapshot.rs: UsageSnapshot::pace_signal(now, weekly_pace_days) implementing the hybrid rule over the headline windows only (scoped limits excluded, per upstream; at_risk keeps its scoped behaviour). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
- pace_signal tie-break now prefers the session (5-hour) signal on an exact ratio tie, matching Swift's max(by:) which keeps the first maximal element (hotSignals is built session-first). - Cap utilization at 100 in src/pacing.ts's isAtRisk to restore numeric identity with meter_core::pacing::pace_ratio for >100% usage. - Factor UsageWindow::elapsed_secs helper, de-duplicating the elapsed-time computation across expected_usage_percent, projected_end_percent and projected_limit_date. - Merge the split jiff imports in snapshot.rs. - Add regression tests: session-wins tie-break, >100% utilization cap (Rust + TS), and projected_end_percent's reset/grace guards. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Port the meter-render half of ClaudeMeter PR #33's pace-first display: - IconState gains pace_kind, pace_band and pace_ratio. Eq/Hash are now hand-written: the f64 ratio is keyed by its one-decimal rendered form ("1.8") so a hair's nudge still hits the icon cache, while the colour band is keyed separately so ratios that round equal but straddle a band boundary (1.16 vs 1.24) never borrow each other's colour. `with_pace` derives the band from the ratio so the number and colour can't drift. - palette.rs: the 4-band pace scale (blue #007AFF underuse / green sustainable / orange overuse / red heavy overuse, reusing the existing status greens/oranges/reds like PacePalette.swift), plus the shared override logic (primary_label, label_ink, override_color, draw_label) and a badge() dispatcher that draws a flame (hot) or snowflake (cold) in place of the at-risk dot. All ink, glyph included, stays pure black in monochrome/template mode. - All six styles route their primary metric through the shared helpers: Battery/Minimal/DualBar/Circular swap the number for the ratio and recolour it; Segments recolours its active bars; Gauge recolours the dial. The subset font lacks "." and "×", so pace_label draws them as vector shapes and leaves the digit rendering byte-identical. - Snapshots regenerated with seven pace cases; existing snapshots unchanged. Tests cover the flame/snowflake, the override text, the drawn punctuation and the band-straddle cache key. - PaceKind/PaceBand gain Hash (needed for the cache key). src-tauri IconState literals get the three new None fields to keep the workspace compiling; the tray pace-signal wiring is a separate task. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Key the IconCache off exactly what renders, not a parallel computation:
- pace_ratio_key now returns the same `format!("{ratio:.1}")` string the
override text displays, instead of `(ratio*10.0).round() as i32`. The two
disagree at `.x5` boundaries (0.85 renders "0.8" but scale-rounds to 9,
colliding with 0.851's "0.9"), so two ratios that render different text in
the same band could share a cache entry and show a stale ratio.
- Fold at_risk into the key only when pace_kind is None: palette::badge draws
the at-risk dot solely in that case, so states differing only in at_risk
under a flame/snowflake render byte-identical SVG and must share one entry.
- Add a circular.rs unit test pinning the asymmetric pace-first split (arc
keeps the quota-status colour, only the centre number takes the pace band).
- Add a Scale::X2 pace snapshot (the production scale) so the flame badge's
legibility at 44px is regression-guarded.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add the src-tauri shell layer for issue #16's pace-based utilization tracking: AppSettings gains weekly_pace_days (5-7, clamped, default 7) and pace_first_display (default off), each with its own set_*/store_* command pair mirroring the existing show_reset_time pattern. The tray build path computes UsageSnapshot::pace_signal from the headline windows, gated behind pace_first_display, and overlays the flame/snowflake badge plus the pace ratio onto IconState; the "Used X% vs Y% expected" tooltip surfaces as an extra tray-menu line since Linux's StatusNotifierItem has no tooltip. commands.rs and tray/model.rs were split (commands/pace.rs, a model_tests.rs test file) to stay under the file-size gate, and the new pace settings share IconOptions/PaceOptions structs to stay under both the too-many-arguments lint and the duplication ceiling. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
set_weekly_pace_days persisted the new 5/6/7 span but never pushed it into the live tray: apply_state only reads the cached weekly_pace_days/pace_first_display in TrayResources, which are only ever written by tray::init and tray::set_pace_options (previously called only from set_pace_first_display). The tray's own pace ratio, badge and menu line -- the primary Linux surface -- kept computing against the stale span indefinitely, contrary to the doc comment's claim that it self-heals on the next scheduler tick. Both pace commands now share a broadcast_and_push_pace helper that emits settings-changed and calls tray::set_pace_options together, so a weekly-pace-basis change takes effect immediately like every other tray-visible setting. The resulting exact-duplicate pair between the two thin command wrappers (Tauri requires each command as its own named function) is acknowledged via .dupes-ignore.toml rather than raising the ratchet ceiling. Also split tray/model.rs's #[path]-based test module into a proper tray/model/mod.rs + tray/model/spec.rs nested-module layout. Verified via a real --lcov run that the #[path] trick (and, empirically, any file literally named `tests.rs`/`*_tests.rs`) makes cargo-llvm-cov silently drop that file's ~486 lines from coverage entirely; renaming the leaf file away from that pattern restores its own SF/LF record. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…is settings (#16) Port the frontend layer of ClaudeMeter PR #33's pace-based utilization tracking, in numeric lockstep with meter-core's pacing.rs and its Swift oracle: - pacing.ts: extend beyond isAtRisk with paceRatio, expectedUsagePercent, projectedEndPercent, projectedLimitDate, the PaceBand classifier and weeklyPacingDurationMs — same constants/formulas as pacing.rs, with vitest cases pinned to the same upstream oracle numbers. - view-model.ts / render.ts: expected-by-now tick, pace line, projection line ("Hits limit ~1:10 PM, N before reset" / "On pace to end at ~X% (Y% unused)"), Overusing/Underusing/On Pace verdict badge, snowflake glyph, and the pace-first primary/secondary swap. Weekly and scoped cards pass the weekly pacing basis and showsUnderuse; the session card never signals underuse. - types.ts / ipc.ts / settings: weekly_pace_days + pace_first_display on AppSettings (+ DEFAULT_SETTINGS and the setWeeklyPaceDays / setPaceFirstDisplay backends), a Display Mode (Consumption vs Pace) and Weekly Pace Basis (5/6/7) segmented control wired into settings-view. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Key card pacing cadence off window.window rather than the synthetic card id, so a scoped limit whose own window is a five-hour kind is paced over its 5-hour window (like the session card) instead of the weekly span. Fold buildViewModel's settings-derived trailing params into a CardOptions options bag, and factor the three segmented-control click handlers into a shared bindSegmented helper. Add format.test.ts coverage for describeRemaining/formatHitTime and a view-model.test.ts case exercising a scoped five-hour card's pace fields. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…ace (#16) In pace-first display the tray icon only overrode the quota % with a pace ratio when the hybrid PaceSignal was hot/cold. For the common on-pace case (no signal) it silently fell back to the raw quota percentage, so turning on pace-first display looked identical to quota-first mode most of the time. Mirror upstream MenuBarManager.updateIcon's fallback chain `paceSignal?.ratio ?? session.paceRatio ?? weekly.paceRatio`: the primary metric always becomes a ratio once pace math is meaningful, its band driving the colour, while the flame/snowflake badge (pace_kind) and the menu's pace-line tooltip stay gated on the hybrid signal being present. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Add a pace_tracking_enabled setting (default on) with a checkbox at the top of the Settings > Pace section that turns the whole feature on/off. When off: the popover computes no pace at all (ratio/expected/projection all null, cards render quota-first) and the Display-mode + Weekly-basis sub-controls hide; the tray shows no pace ratio or flame/snowflake badge. The sub-settings keep their stored values so re-enabling restores the prior configuration. The tray folds the switch into its effective pace-first flag (pace_tracking_enabled && pace_first_display) — no extra field threaded through the tray. New command + store helper mirror the existing pace commands; the frontend gates paceRatio/expected/projection in the view-model on the flag.
The macOS-only NSPopover/activation-policy code read as dead or const-eligible on the Linux clippy leg (a local macOS `just check` can't see this — there those paths are live): - gate MAIN_WINDOW_LABEL / is_main_label to cfg(any(macos, test)) — only the macOS focus-loss handler and the unit test use them; - make the non-macOS set_app_foreground stub a const fn; - make configure_popover_window (and its call) macOS-only — its whole job is the NSPopover conversion, a no-op on Linux. Pre-existing on main from the NSPopover merge; surfaces here via PR CI.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Ports pace-based utilization tracking from upstream eddmann/ClaudeMeter PR #33 — a pace ratio (
usage_fraction / elapsed_time_fraction; 1.0 = exactly sustainable) surfaced in the tray and popover, so you can tell whether your current burn rate will exhaust a window early or leave paid capacity unused. Closes #16.Implemented layer by layer (each layer went through an implement → 4-viewpoint review → remediate cycle, then a final whole-feature review):
meter-core— the pace math and numeric oracle:pace_ratio,expected_usage_percent(elapsed capped at the pacing span),projected_end_percent,projected_limit_date(bypasses the 5%-elapsed grace, honours the 5%-utilization floor), aPaceBandenum, andPaceKind/PaceSignalwith the exact upstream tooltip strings. All 20 upstreamPaceSignalTestsported and passing.meter-render— a 4-band pace palette, flame (hot) / snowflake (cold) badge (pure black in template mode), and pace-first overrides across all six icon styles, with the palette band + rendered-ratio-string folded into the icon cache key.src-tauri—weekly_pace_days(5–7, clamped),pace_first_display, andpace_tracking_enabledsettings with live-updating commands; the tray pace badge; and the Linux tray-menu ratio text (StatusNotifierItem has no tooltip).src/pacing.tskept numerically identical to the Rust; the expected-by-now tick, pace/projection lines, verdict badge, snowflake, and the pace-first primary/secondary swap; plus Display-Mode + Weekly-Basis settings controls.Enable / disable
A master
pace_tracking_enabledswitch (default on) with a checkbox at the top of Settings → Pace turns the whole feature on/off. When off: the popover computes no pace at all (ratio/expected/projection null, quota-first cards), the tray shows no pace ratio or badge, and the sub-controls hide. The sub-settings keep their stored values, so re-enabling restores the prior configuration.Semantics & decisions
min(utilization, 100) / expected%, expected = elapsed fraction of the pacing span capped at 1.0;Noneunder 5% elapsed, but the limit-hit projection bypasses that grace while honouring the 5%-utilization floor.pace_signalis headline-only (session + weekly, never scoped) and hybrid: overuse signals on either window (max wins), underuse only on the weekly window.Verification
just checkgreen:cargo fmt, clippy pedantic+nursery-D warnings,cargo test --workspace, file-size gate,cargo deny,cargo dupes(ceiling held),cargo llvm-cov(coverage 89% lines / 88% functions, above the floor),tsc --noEmit, and vitest (96 frontend tests).pacing.rsandsrc/pacing.tsverified numerically identical; the master-switch "no pace when disabled" behaviour is unit-tested.Notes for review
Worth eyeballing live (
just devon this branch): the pace-first tray icon, the popover projection lines, and the Settings → Pace enable/disable + Display-Mode/Weekly-Basis controls.🤖 Generated with Claude Code