Skip to content

Extend menu bar conditionals beyond usage percentages - #3088

Merged
steipete merged 8 commits into
steipete:mainfrom
wdmitchelluk:feat/menubar-conditional-metrics
Aug 20, 2026
Merged

Extend menu bar conditionals beyond usage percentages#3088
steipete merged 8 commits into
steipete:mainfrom
wdmitchelluk:feat/menubar-conditional-metrics

Conversation

@wdmitchelluk

@wdmitchelluk wdmitchelluk commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Follow-up to #3076. Conditional predicates could only compare four percent-used windows (session | weekly | scopedWeekly | automatic) against a 0...100 threshold. They now compare 18 metrics across four units, and metrics with two meaningful readings gain a used / remaining select — so session > 50% used AND session resets in < 2h or balance remaining >= 5 are expressible.

Metrics

Group Metrics Unit used/remaining
Percent windows Session % · Weekly % · Scoped weekly % · Auto % %
Direct lanes Primary % · Secondary % · Tertiary % %
Time to reset Session · Weekly · Scoped weekly · Auto resets in h
Pace Session · Weekly · Auto pace % (signed)
Run-out Runs out h
Money Balance · Cost today · Cost 30d USD Balance only

Time thresholds are hours as a Double, so "resets in < 2h" is literally 2 and 30 minutes is 0.5 — no extra unit picker. Currency thresholds are USD; a provider reporting another cost currency is converted through UsageFormatter.convertedCost, so a threshold does not shift when the user changes their display currency. Pace is already signed, and a reset countdown or cost total has no complement, so those hide the direction picker.

Numeric data behind the display strings

MenuBarLayoutRenderData carried pace, run-out, balance and cost only as formatted strings ("+11%", "Runs out in 1d 16h", "$12.34"), which cannot be compared. New MenuBarLayoutRenderMetrics carries their numeric twins, pre-rounded to the same granularity as the text they mirror — an unrounded value drifts on every clock tick and would defeat MenuBarLayoutTitleCache, which keys on this struct.

Sources: a menuBarLayoutPaceDelta twin of menuBarLayoutPaceText (identical parameter list, so text and number can never disagree), the UsagePace already computed for runsOut (now bound instead of discarded), menuBarLayoutCosts replacing menuBarLayoutCostStrings with strings and USD amounts from one snapshot read, and MenuBarLayoutBalanceResolver.balanceAmountsUSD.

Three refresh gates the new metrics exposed

These are the load-bearing part of the change — without them a predicate can be correct and still render stale:

  1. Title cache. MenuBarLayoutRenderKey had no component that moves with the clock (resetText derives only from the automatic window), so a countdown predicate would have served its pre-flip title indefinitely. The key now carries [UUID: Bool] conditional outcomes, evaluated once per render instead of once per placement.
  2. Observation signatures. All four storedMenuBarLayout*Signature builders gated on which display tokens the layout contains; a predicate on cost, balance, pace or run-out has no matching token. They now also read MenuBarLayout.referencedConditionalPredicates. This incidentally fixes a pre-existing gap where selectedLanes never walked conditional branches, so a lanePercent token inside a branch was invisible to the lane signature.
  3. Wake-up scheduling. A …ResetsIn predicate flips at an instant no display token ticks on, so menuBarConditionalResetDelays wakes at resetsAt - threshold. Run-out predicates are deliberately excluded: their estimate drifts with usage rather than crossing a fixed instant, and the existing pace delays already cover that lane.

Persistence

  • MenuBarConditionalPredicate gains an explicit init(from:): direction decodes as .used when absent, so every already-persisted predicate keeps its original meaning. The synthesized decoder would have rejected all of them.
  • normalized() clamps the threshold into the metric's unit range and drops a direction the metric cannot use, so switching metric families in the editor can never leave an out-of-range value.
  • The library is now decoded element-wise. This change makes forward-incompatible metric raw values possible for the first time, and the old try? decode([MenuBarLayoutConditional].self) ?? [] would have silently wiped the user's entire library on a downgrade. One unrecognized entry is now dropped on its own; layouts referencing it already render the dangling-conditional placeholder.

New shipped default

Auto % / Resets in — renders the automatic percentage while the lane has headroom, and the reset countdown once it is spent (Auto % remaining >= 1 → Auto %, else Resets in). Its name is composed from the two palette token labels it switches between, so the chip reads in the same words as the tokens themselves in every language. Shipped defaults only seed on a fresh install, so an existing library is never reseeded.

Localization

Four new keys (..._used, ..._remaining, ..._metric_resets_in, ..._metric_scoped_weekly) translated across all 23 catalogs. Every other metric label reuses an existing palette token string, so a metric and the block it measures always read the same. check-app-locales passes: 22 catalogs against 1476 English keys.


Evidence

All 18 metrics in the picker

metric picker

Percent metric — used/remaining select present, % unit

percent row

Countdown metric — direction select hidden, unit switches to h

hours row

The headline case: two clauses, mixed units, live summary

If Session % used > 50% and Session resets in < 2h then Resets in else Hide

combined condition

Fresh install seeds the new Auto % / Resets in default

shipped library

Both branches of that default, same rule, live data

Automatic lane has headroom → percentage:

percent branch

Automatic lane spent → reset countdown:

countdown branch

Tests

make check clean (0 SwiftLint violations, 23/23 locale catalogs). New coverage in MenuBarLayoutRendererTests (55 tests) and MenuBarLayoutTests (54 tests):

  • resets-in predicate picks the then branch inside the threshold
  • session percent and resets-in combine with and — the headline case
  • remaining direction inverts the percent reading
  • balance direction selects used or remaining amount
  • pace run-out and cost predicates read numeric metrics
  • predicate on a metric with no datum evaluates false
  • time based conditional flips when only the clock advances — regression guard for the cache key
  • shipped auto default swaps percent for the countdown once the quota is spent
  • predicate without direction decodes as used
  • threshold clamps to the metric unit range
  • direction is dropped for metrics without a complement
  • referenced conditional predicates include nested branches
  • unrecognized conditional metric drops only its own entry
  • metric drives the editor row controls and units
  • summary spells out direction and unit for a mixed-unit condition

Full suite run in 8 shards. Remaining failures (StatusMenuSwitcherRefreshTests, MenuCardViewRecyclingTests, AdaptiveRefreshTimerTests load timeouts, CostUsageFetcher* hangs) were verified against a pristine-HEAD worktree on the same volume and reproduce byte-identically — the headless AppKit brittleness AGENTS.md warns about, not this change.


Review round-up

All actionable findings from both reviewers are fixed; ClawSweeper is at Findings: None on 659a8134.

Finding Fix
P2 Cost signatures recorded only the formatted string, so two updates could cross a threshold while both formatted to the same cent 6dbabfc — a referenced cost metric signs the unrounded amount losslessly. The same gap existed for balance, which recorded only the rendered "Remaining" row while a balance used predicate reads "Used"; both are now signed.
P2 Lane signatures recorded the displayed reading, which follows usageBarsShowUsed and clamps remaining at zero, so primaryLane > 105% could move 104% → 106% against a constant 0.000 6dbabfc — the lane signature now covers only what the layout renders; a new conditional-window signature covers what conditionals read: the raw usedPercent (which remaining derives from, so it covers both directions) plus resetsAt, which countdown predicates depend on and no token contributes.
P2 A layout whose only pace/run-out reference is a predicate got no clock wake-up, because the pace scheduler is gated on a placed .pace(.weekly) token and only fires once f2b4673 — referenced weekly-pace predicates now trigger the eligibility wake-up, and any referenced pace/run-out predicate schedules a minute tick. Money predicates deliberately schedule nothing.
P1 Element-wise decoding only helps builds that already have it; the build a user downgrades to decodes the library strictly and falls back to [] 8f62fbad — the library dual-writes like layouts already do: menuBarLayoutConditionalsV2 keeps full fidelity, the original key keeps an older-readable projection, and loadLibrary mirrors preferredLayout.
P1 convertedCost returns the source amount when it has no rate, and both cost producers passed it through as USD 659a8134 — the amount is kept only when the conversion actually landed in USD; otherwise the predicate sees no value and evaluates false.

Downgrade contract (flagged for owner acceptance)

The older-readable projection drops an entry when any clause uses a metric outside the original four or a non-.used direction. The second case is not a decode failure: 0.54.0's predicate uses synthesized Codable, which silently ignores the unknown direction key, so session remaining > 80 would come back as session used > 80 and render the opposite branch. A dropped rule is visibly absent; an inverted one is not. Happy to change that trade if a best-effort translation is preferred.

Added since the first review

  • StatusItemConditionalSignatureTests — over-quota used-direction lane, moved reset timestamp, sub-cent cost, unconvertible currency
  • MenuBarCountdownRefreshTests — predicate-only clock-derived scheduling (parameterised over runsOutIn/weeklyPace/sessionPace/automaticPace), plus the negative cost case and the reset-instant case
  • MenuBarLayoutTests — four downgrade tests using a PreExpandedConditional fixture that reproduces the 0.54.0 surface exactly, including that the old decoder throws on the current blob

Rebased onto 5bbf1773. The red swift-test-macos (0, 2) shard is generic provider details keep canonical labels alongside localized core metrics, which fails on pristine main at HEAD — verified in a clean origin/main worktree, zero changes applied. build-linux-musl-cli fails in "Install Swift Static Linux SDK", before any source is compiled.

@clawsweeper

clawsweeper Bot commented Aug 20, 2026

Copy link
Copy Markdown

🦞👀
ClawSweeper picked this up.

Pull request received. I will update this pull request when review starts.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: dbeffc237f

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread Sources/CodexBar/StatusItemController+IconObservation.swift
Comment thread Sources/CodexBar/StatusItemController+IconObservation.swift Outdated
@clawsweeper clawsweeper Bot added merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. P2 Normal priority bug or improvement with limited blast radius. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. labels Aug 20, 2026
@clawsweeper

clawsweeper Bot commented Aug 20, 2026

Copy link
Copy Markdown

Codex review: found issues before merge. Reviewed August 20, 2026, 5:36 AM ET / 09:36 UTC.

ClawSweeper review

What this changes

This PR expands menu-bar conditional rules from four usage percentages to usage, time, pace, run-out, balance, and cost metrics, with editor, persistence, refresh, localization, and test updates.

Merge readiness

⚠️ Needs maintainer review before merge - 3 items remain

Keep open: the new run-out predicate scheduler can leave a conditional branch stale for up to a minute, and VISION.md requires owner sign-off for this new persisted feature.

Priority: P2
Reviewed head: 0f85619b04c10092986d3f327c5c8213f6836143
Owner decision: Required. See Decision needed.

Review scores

Measure Result What it means
Overall readiness 🦐 gold shrimp (3/6) The feature has strong real UI proof and broad targeted coverage, but the scheduler defect and owner product decision prevent merge readiness.
Proof confidence 🦞 diamond lobster (5/6) ✨ media proof bonus Sufficient (screenshot): After-fix screenshots visibly show the expanded picker, a mixed-unit rule, and both branches of the shipped default; redact any account details in future proof updates.
Patch quality 🦐 gold shrimp (3/6) 1 actionable review finding remain.

Verification

Check Result Evidence
Real behavior Verified Sufficient (screenshot): After-fix screenshots visibly show the expanded picker, a mixed-unit rule, and both branches of the shipped default; redact any account details in future proof updates.
Evidence reviewed 5 items Scheduler defect: Run-out metrics are rounded to minutes, but the new elapsed scheduler wakes at the next Unix-minute boundary rather than at the next rounded ETA transition.
Persisted compatibility policy: The PR dual-writes a full library and an older-readable projection, deliberately omitting rules that older versions would reject or silently invert.
Maintainer sign-off policy: VISION.md lists new features and behavior changes affecting data storage under Needs Sign-Off; this PR adds both conditional capabilities and persisted rule data.
Findings 1 actionable finding [P2] Schedule run-out ticks at the actual rounded-value boundary
Security None None.

Live Verification

Command: swift build -c release --product CodexBarCLI && ./.build/release/CodexBarCLI --help

Result: FAIL (failed) — execution before step 1 run: sh -lc pnpm install --ignore-scripts --frozen-lockfile failed: ! Corepack is about to download https://registry.npmjs.org/pnpm/-/pnpm-11.22.0.tgz

sh -lc pnpm install --ignore-scripts --frozen-lockfile failed: ! Corepack is about to download https://registry.npmjs.org/pnpm/-/pnpm-11.22.0.tgz

Assertions:

  • FAIL expect_output: Usage:

How this fits together

CodexBar converts provider snapshots and clock-derived usage estimates into a configurable menu-bar title. Conditional layout rules consume those values to choose tokens, while persistence retains each user’s rule library across launches and upgrades.

flowchart LR
A[Provider snapshots] --> B[Menu bar render data]
C[Clock and reset times] --> B
B --> D[Conditional rule evaluation]
E[Saved rule library] --> D
D --> F[Refresh scheduling and title cache]
F --> G[Menu bar title]
E --> H[Legacy compatibility projection]
Loading

Decision needed

Question Recommendation
Should CodexBar accept expanded persisted menu-bar conditional rules and the deliberate downgrade behavior that omits rules older versions cannot represent rather than silently changing their meaning? Approve the explicit downgrade contract: Accept visible omission of unsupported rules on older versions as safer than rendering a remaining-direction rule with inverted semantics.

Why: VISION.md requires sign-off for new features and behavior changes affecting data storage; code review cannot determine the acceptable downgrade product contract.

Before merge

  • Schedule run-out ticks at the actual rounded-value boundary (P2) - runsOutMinutes is derived by rounding a continuously changing ETA, so its next transition is tied to the ETA phase, not necessarily the next Unix-minute boundary. A run-out predicate can therefore retain its old branch for almost a minute after its rounded value crosses the threshold. Compute the next rounded ETA transition and cover a non-minute-aligned case.
  • Resolve merge risk (P1) - On downgrade, expanded-metric or remaining-direction rules are intentionally omitted from the older-readable library; any placed reference then uses the existing dangling-rule placeholder until the user returns to a compatible version.

Findings

  • [P2] Schedule run-out ticks at the actual rounded-value boundary — Sources/CodexBar/StatusItemController+CountdownRefresh.swift:235-238
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Changed files 47 files affected The feature spans rendering, saved settings, refresh scheduling, tests, proof assets, and localization catalogs.
Production versus tests production +1013/-139, tests +940/-15 The substantial production growth is paired with focused renderer, persistence, observation-signature, and scheduling coverage.

Merge-risk options

Maintainer options:

  1. Approve the downgrade projection (recommended)
    Accept omission of unsupported expanded rules on older versions so older decoders never reject the whole library or invert a rule.
  2. Pause the persisted expansion
    Keep the prior rule surface until maintainers choose a different downgrade contract.

Technical review

Best possible solution:

Schedule run-out predicates at the actual rounded-ETA transition, retain the explicit non-inverting downgrade projection, and merge only after the owner approves the expanded persisted-rule surface.

Do we have a high-confidence way to reproduce the issue?

Yes, from source: a run-out ETA changes continuously with now and is rounded to minutes, while the new scheduler wakes only on the next Unix-minute boundary, which need not be the next rounded-value transition.

Is this the best way to solve the issue?

No: the refresh plan must calculate the next rounded ETA transition rather than use a generic minute boundary; the persistence approach is otherwise a reasonable safety trade-off pending owner approval.

Full review comments:

  • [P2] Schedule run-out ticks at the actual rounded-value boundary — Sources/CodexBar/StatusItemController+CountdownRefresh.swift:235-238
    runsOutMinutes is derived by rounding a continuously changing ETA, so its next transition is tied to the ETA phase, not necessarily the next Unix-minute boundary. A run-out predicate can therefore retain its old branch for almost a minute after its rounded value crosses the threshold. Compute the next rounded ETA transition and cover a non-minute-aligned case.
    Confidence: 0.94

Overall correctness: patch is incorrect
Overall confidence: 0.94

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against a3457dafbe63.

Labels

Label changes:

  • add rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦞 diamond lobster and patch quality is 🦐 gold shrimp.
  • add status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Sufficient (screenshot): After-fix screenshots visibly show the expanded picker, a mixed-unit rule, and both branches of the shipped default; redact any account details in future proof updates.
  • remove status: 👀 ready for maintainer look: Current PR status label is status: ⏳ waiting on author.
  • remove rating: 🐚 platinum hermit: Current PR rating is rating: 🦐 gold shrimp, so this older rating label is no longer current.

Label justifications:

  • P2: The incorrect refresh boundary can leave a user-visible conditional result stale, but it is bounded to predicate-only clock-derived rules.
  • merge-risk: 🚨 compatibility: The branch changes persisted conditional-rule data and defines a deliberate downgrade projection for older application versions.
  • rating: 🦐 gold shrimp: Overall readiness is 🦐 gold shrimp; proof is 🦞 diamond lobster and patch quality is 🦐 gold shrimp.
  • status: ⏳ waiting on author: ClawSweeper has contributor-facing work open and is waiting for author action. Sufficient (screenshot): After-fix screenshots visibly show the expanded picker, a mixed-unit rule, and both branches of the shipped default; redact any account details in future proof updates.
  • proof: sufficient: Contributor real behavior proof is sufficient. After-fix screenshots visibly show the expanded picker, a mixed-unit rule, and both branches of the shipped default; redact any account details in future proof updates.
  • proof: 📸 screenshot: Contributor real behavior proof includes screenshot evidence. After-fix screenshots visibly show the expanded picker, a mixed-unit rule, and both branches of the shipped default; redact any account details in future proof updates.

Evidence

What I checked:

Likely related people:

  • wdmitchelluk: William Mitchell authored the merged conditional-token feature that established this subsystem, and this PR extends that same surface. (role: introduced conditional-token behavior; confidence: high; commits: 4615951172fe; files: Sources/CodexBar/MenuBarLayout.swift, Sources/CodexBar/MenuBarLayoutRenderer.swift)
  • steipete: Recent history shows Peter Steinberger repeatedly maintaining the central menu-bar layout area, and VISION.md makes this feature and persisted-data decision owner-gated. (role: recent area contributor and likely product decision owner; confidence: high; commits: 0f85619b04c1, 5a2a70458d95; files: Sources/CodexBar/MenuBarLayout.swift, Sources/CodexBar/StatusItemController+CountdownRefresh.swift, VISION.md)
  • Yuxin Qiao: History attributes the weekly pace eligibility refresh path to Yuxin Qiao, which this PR extends for predicate-driven refreshes. (role: adjacent refresh-scheduling contributor; confidence: medium; commits: 1bf1bae14ba9; files: Sources/CodexBar/StatusItemController+CountdownRefresh.swift)

Rank-up moves

Optional improvements that raise the rating; they are not merge blockers.

  • Schedule run-out predicate refreshes at the next rounded ETA transition and add a non-minute-aligned regression test.
  • Obtain owner sign-off for the persisted-rule downgrade contract.

Rating scale

Score Internal tier Crab rank Meaning
6/6 S 🦀 challenger crab Exceptional readiness
5/6 A 🦞 diamond lobster Very strong readiness
4/6 B 🐚 platinum hermit Good normal PR; ordinary maintainer review
3/6 C 🦐 gold shrimp Useful, but confidence is limited
2/6 D 🦪 silver shellfish Proof or implementation needs work
1/6 F 🧂 unranked krab Not merge-ready
N/A NA 🌊 off-meta tidepool Rating does not apply

Overall follows the weaker of proof and patch quality.
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

Workflow

  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

History

Review history (10 earlier review cycles; latest 8 shown)
  • reviewed 2026-08-20T05:22:42.045Z sha f2b4673 :: found issues before merge. :: [P1] Preserve older conditional libraries on downgrade
  • reviewed 2026-08-20T05:51:16.161Z sha 3b53b91 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-20T06:32:45.961Z sha 3b53b91 :: needs changes before merge. :: [P1] Restore provider-detail localization
  • reviewed 2026-08-20T07:35:47.220Z sha 8f62fba :: found issues before merge. :: [P1] Reject unconvertible cost values for USD predicates
  • reviewed 2026-08-20T07:50:59.426Z sha 659a813 :: needs maintainer review before merge. :: none
  • reviewed 2026-08-20T08:03:08.416Z sha f19cdec :: needs maintainer review before merge. :: none
  • reviewed 2026-08-20T08:08:29.584Z sha f19cdec :: needs maintainer review before merge. :: none
  • reviewed 2026-08-20T09:05:31.824Z sha 79348be :: needs maintainer review before merge. :: none

@wdmitchelluk

Copy link
Copy Markdown
Contributor Author

P2 — Schedule predicate-only pace and run-out refreshes → fixed in f2b4673

You were right, and my reasoning for excluding runsOutIn was wrong for a reason I had not checked: menuBarWeeklyPaceRefreshDelays is gated on a placed .pace(.weekly) token and only wakes once, at the pace-eligibility boundary. So a layout whose only pace/run-out reference is a predicate got no clock wake-up at all, and kept rendering the branch that was true when the value last moved.

Two changes:

  1. Eligibility wake-up follows predicates toomenuBarWeeklyPaceRefreshDelays now fires on a placed .pace(.weekly) token or a referenced .weeklyPace predicate.
  2. New minute tick for clock-derived predicatesmenuBarConditionalElapsedDelays schedules the next whole-minute boundary whenever a predicate reads sessionPace, weeklyPace, automaticPace, or runsOutIn.

A minute tick is the right granularity rather than a hedge: both numbers are pre-rounded to what the menu bar actually shows — MenuBarDisplayText.paceText rounds the delta to whole percentage points, and runsOutMinutes is (etaSeconds / 60).rounded() — so nothing can change between minute boundaries. It is also the exact cadence a .resetCountdown token already costs, so this adds no new power profile.

The gate stays precise: money predicates (balance, costToday, cost30d) move only when new provider data arrives and schedule nothing, and reset-countdown predicates keep their exact resetsAt - threshold wake-up rather than a tick.

Regression coverage in MenuBarCountdownRefreshTests, all placing a single conditional with no pace/reset/countdown token anywhere in the layout, so the token-gated schedulers cannot be what fires:

  • `predicate-only clock-derived conditional schedules a refresh` — parameterised over runsOutIn, weeklyPace, sessionPace, automaticPace
  • `predicate-only cost conditional schedules nothing` — the negative case, proving the gate is precise
  • `predicate-only reset countdown conditional schedules its flip instant`

P1 — persisted conditional surface needs maintainer sign-off

Agreed, and not something I can resolve from here — flagging for @steipete. Two notes that may help the decision:

  • The surface addition is a metric enum plus one direction field on the existing predicate; no new storage key, no new UI concept beyond one extra select in a row that already existed.
  • Downgrade behaviour is stricter than before this PR: the library now decodes element-wise, so an older build reading a newer library drops only the entries it cannot understand instead of silently discarding all of them (`unrecognized conditional metric drops only its own entry`).

Note on the live-verification step

swift run CodexBarCLI --help failed in your sandbox before reaching step 1, on pnpm install trying to download corepack's pnpm tarball — an environment/network issue rather than a code failure. Locally swift build, make check (0 SwiftLint violations, 23/23 locale catalogs) and the affected suites all pass; the two macOS CI shards are green on the previous head and running on this one.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Aug 20, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event exact_review_queue).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

@clawsweeper clawsweeper Bot added P1 Urgent regression or broken agent/channel workflow affecting real users now. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. and removed P2 Normal priority bug or improvement with limited blast radius. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. labels Aug 20, 2026
@wdmitchelluk

Copy link
Copy Markdown
Contributor Author

CI note: swift-test-macos (0, 2) red on a pre-existing flake

Shard 1 is green. Shard 0's only failure is unrelated to this branch:

✘ Test "generic provider details keep canonical labels alongside localized core metrics"
  PopupLocalizationTests.swift:91: Expectation failed:
  model.providerDetails.first { $0.title == "API key" } → nil

That test builds an OpenRouterUsageSnapshot and asserts its "API key" detail section under a zh-Hant override. Nothing on this branch touches OpenRouterUsageSnapshot, UsageMenuCardView.Model.make, or the OpenRouter detail sections — MenuBarLayoutBalanceResolver only reads detail rows.

Evidence it is pre-existing: the same test failed on main at 61f54225 in run 32318564053, roughly three hours before this PR was opened.

Locally the exact CI group passes on this head:

$ swift test --skip-build --no-parallel --filter \
  '^CodexBarTests\.PopupLocalizationTests/|^CodexBarTests\.PredictivePaceWarningTests/|^CodexBarTests\.PreferencesPaneSmokeTests/|^CodexBarTests\.PreferencesSelectionTests/'
✔ Test run with 53 tests in 4 suites passed after 0.779 seconds.

I don't have rerun rights on the repo, so the shard cannot be retried from here — happy to fix that flake in a separate PR if it's wanted, but it looked out of scope to fold into this one.

@wdmitchelluk

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Aug 20, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event exact_review_queue).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

@clawsweeper clawsweeper Bot added rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. and removed rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. labels Aug 20, 2026
@wdmitchelluk

Copy link
Copy Markdown
Contributor Author

P1 — Preserve older conditional libraries on downgrade → fixed in 3b53b91

You're right, and you caught the exact hole in my reasoning: element-wise decoding only helps a build that already has the lenient decoder. The build a user actually downgrades to decodes menuBarLayoutConditionals strictly and falls back to [], so one saved rule using a new metric empties the whole library there. My mitigation was forward-looking only.

Fixed by dual-writing the way layouts already do, so the mechanism matches MenuBarLayoutPersistence.preferredLayout rather than inventing a second scheme:

  • menuBarLayoutConditionalsV2 — full fidelity.
  • menuBarLayoutConditionals — an older-readable projection.
  • loadLibrary prefers the current key unless the legacy key disagrees with its own projection, which only happens when an older release wrote it; that edit then wins.
  • Startup materializes the missing key when only one exists, so a pre-upgrade install is downgrade-safe without needing an editor write first.

The projection drops an entry when any clause uses a metric outside the original four or a non-.used direction. The second case is the one worth calling out, and it is not a decode failure at all: a 0.54.0 predicate uses synthesized Codable, which silently ignores the unknown direction key, so session remaining > 80 would come back as session used > 80 and render the opposite branch. A missing rule is visibly missing; an inverted one is not.

Coverage in MenuBarLayoutTests, using a new PreExpandedConditional fixture that reproduces the 0.54.0 surface exactly (four metrics, no direction, synthesized Codable):

  • `conditional library dual-writes an older-readable projection` — saves a readable rule, a new-metric rule, and an inverted-direction rule; asserts the old decoder reads the projection and gets only the readable one, and that it throws on the current blob. That is the assertion that would have failed before this commit.
  • `conditional library load prefers a legacy blob edited by an older release`
  • `conditional library load keeps new metrics when the legacy blob is its own projection`
  • `startup materializes a missing conditional projection`

Remaining item

The VISION.md feature sign-off is the one thing left, and it needs @steipete rather than a code change. Everything actionable from both reviews is now addressed:

Finding Status
P2 Include numeric costs in conditional signatures (codex) Fixed 6dbabfc
P2 Track used lane values for used-direction predicates (codex) Fixed 6dbabfc
P2 Schedule predicate-only pace and run-out refreshes Fixed f2b4673
P1 Preserve older conditional libraries on downgrade Fixed 3b53b91
P2 Maintainer sign-off for the expanded persisted surface Needs maintainer

make check clean; 229 tests across the 8 affected suites green.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Aug 20, 2026

Copy link
Copy Markdown

🦞👀
Exact review queued.

Re-review progress:

@wdmitchelluk

Copy link
Copy Markdown
Contributor Author

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Aug 20, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event exact_review_queue).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

Re-review progress:

@wdmitchelluk

Copy link
Copy Markdown
Contributor Author

CI: the two red jobs are identical to main, not a flake

Sharpening my earlier note — this is reproducible rather than intermittent, and it reproduces on main:

main @ 61f54225 (run 32318564053) this PR @ 3b53b91 (run 32336983376)
Failing jobs swift-test-macos (0, 2), lint-build-test swift-test-macos (0, 2), lint-build-test
Failing tests generic provider details keep canonical labels alongside localized core metrics — and only that one same, and only that one

lint-build-test is not an independent failure: it is the aggregate gate, failing with macOS test gate/result mismatch: required=true deferred=false result=failure because shard 0 is red.

That main run predates this PR, and it is the only recent main run that actually executed the macOS suite — the others show swift-test-macos: skipped, so they carry no signal either way.

The test builds an OpenRouterUsageSnapshot and asserts its "API key" detail section under a zh-Hant override. Nothing here touches OpenRouterUsageSnapshot, UsageMenuCardView.Model.make, or the OpenRouter detail sections; MenuBarLayoutBalanceResolver only reads detail rows. Shard 1, which carries the conditional suites, is green.

Locally the exact CI group passes on this head:

$ swift test --skip-build --no-parallel --filter \
  '^CodexBarTests\.PopupLocalizationTests/|^CodexBarTests\.PredictivePaceWarningTests/|^CodexBarTests\.PreferencesPaneSmokeTests/|^CodexBarTests\.PreferencesSelectionTests/'
✔ Test run with 53 tests in 4 suites passed

Happy to chase it in a separate PR if wanted — it looks like a CI-environment difference in how that provider's detail sections resolve, which is a different area from this change.

Conditional predicates could only compare four percent-used windows. They now
compare 18 metrics across four units: percent windows, the direct
primary/secondary/tertiary lanes, four reset countdowns, three pace deltas,
run-out, credit balance, and today/30-day cost. Metrics with two readings
(percent windows, lanes, balance) gain a used/remaining select, so
"session > 50% used and session resets in < 2h" is expressible.

Pace, run-out, balance and cost were only carried as display strings, which
cannot be compared, so MenuBarLayoutRenderMetrics carries their numeric twins
pre-rounded to the same granularity as the text they mirror.

Three refresh gates needed widening for the new data dependencies:
- The title cache key had no component that moves with the clock, so a
  countdown predicate would have served its pre-flip title indefinitely. It now
  keys on the per-conditional outcome, evaluated once per render.
- The four observation signatures gated on display tokens; a predicate on cost
  or balance has no token. They now also read the conditionals' metrics, which
  additionally fixes lane tokens inside conditional branches being invisible to
  the lane signature.
- A reset-countdown predicate flips at an instant nothing else ticks on, so the
  countdown scheduler wakes at `resetsAt - threshold`.

The conditional library is now decoded element-wise: this change makes
forward-incompatible metric values possible for the first time, and one unknown
value would otherwise have wiped the whole library on a downgrade.

Ships an "Auto % / Resets in" default that renders the automatic percentage
while the lane has headroom and the reset countdown once it is spent.
Three observation-signature gaps let a predicate flip without a redraw:

- Cost signatures recorded only the currency-formatted string, so two token-cost
  updates could cross a threshold while both formatted to the same cent. A
  referenced cost metric now signs the unrounded amount losslessly.
- The balance signature recorded only the rendered "Remaining" row, so a
  `balance used` predicate — which reads the "Used" row no token surfaces — was
  entirely unsigned. Both amounts are now signed.
- The lane signature recorded the displayed reading, which follows
  `usageBarsShowUsed` and clamps remaining at zero, while `RateWindow.usedPercent`
  deliberately preserves over-quota values. A used-direction predicate such as
  `primaryLane > 105%` could move 104% -> 106% against a constant `0.000`.

The lane signature is now scoped to what the layout renders, and a new
conditional-window signature covers what conditionals read: the raw used percent
(which remaining derives from, so it covers both directions) plus `resetsAt`,
which countdown predicates depend on and no display token contributes.
`menuBarWeeklyPaceRefreshDelays` is gated on a placed `.pace(.weekly)` token and
only wakes once, at the pace-eligibility boundary. Excluding `runsOutIn` from the
conditional reset schedule on the assumption that scheduler covered it therefore
left a hole: a layout whose only pace or run-out reference is a predicate got no
clock wake-up at all, so it kept rendering the branch that was true when the
value last moved.

Referenced weekly-pace predicates now also trigger the eligibility wake-up, and
any referenced pace or run-out predicate schedules a minute tick. Both numbers
are pre-rounded to the granularity the menu bar shows -- whole percentage points
and whole minutes -- so a minute tick is exactly enough, and it is the cadence a
`.resetCountdown` token already costs. Money predicates deliberately schedule
nothing: they move only when new provider data arrives.
Decoding the library element-wise only helps builds that already have the
lenient decoder. The build a user actually downgrades to decodes
`menuBarLayoutConditionals` strictly and falls back to `[]`, so one saved rule
using a new metric would empty the entire library there.

The conditional library now dual-writes the way layouts already do:
`menuBarLayoutConditionalsV2` keeps full fidelity, and the original key keeps an
older-readable projection. `loadLibrary` mirrors `preferredLayout` — the current
key wins unless the legacy key disagrees with its own projection, which only
happens when an older release wrote it, and that edit must survive.

The projection drops an entry when any clause uses a metric outside the original
four, and also when any clause uses a non-`.used` direction. The second case is
the subtler one: an older release's synthesized decoder ignores the unknown
`direction` key, so `session remaining > 80` would come back as
`session used > 80` and render the opposite branch. A missing rule is visibly
missing; an inverted one is not.
@wdmitchelluk
wdmitchelluk force-pushed the feat/menubar-conditional-metrics branch from 3b53b91 to 8f62fba Compare August 20, 2026 07:27
@wdmitchelluk

Copy link
Copy Markdown
Contributor Author

P1 "Restore provider-detail localization" → branch was stale; rebased onto 5bbf1773

Good catch on the symptom, wrong cause — and the cause turned out to be my fault in a different way. This branch was based on 0caf970a, and main had since moved six commits ahead, including 5878c4eb and 4f157784 (the provider-detail i18n work). Diffing a stale branch against main makes that work look deleted; nothing here ever touched MenuCardView+ProviderDetailLocalization.swift, its translations, or its tests.

Rebased onto current main (5bbf1773) — clean, no conflicts. Head is now 8f62fbad, and the diff no longer removes anything.

The failing macOS test is main's, verified directly

Now that I'm on current main, that test reproduces locally, which let me check it properly instead of arguing from CI logs. I built a worktree at pristine origin/main (5bbf1773e), zero changes from me:

$ cd <worktree at origin/main>
$ swift test --skip-build --no-parallel --filter '^CodexBarTests\.PopupLocalizationTests/'
✘ Test "generic provider details keep canonical labels alongside localized core metrics"
  failed after 0.002 seconds with 1 issue.
✘ Test run with 6 tests in 1 suite failed

So it is broken on main at HEAD, independent of this PR. Worth noting for whoever owns #3084/#3086: localizedProviderDetails is gated to .deepseek/.zai, and this test asserts on .openrouter, so the regression is somewhere else in that area rather than in the localization pass itself. Happy to open a separate PR for it — it is unrelated to conditionals and I'd rather not fold it in here.

State on the rebased head

Finding Status
P2 Include numeric costs in conditional signatures (codex) Fixed
P2 Track used lane values for used-direction predicates (codex) Fixed
P2 Schedule predicate-only pace and run-out refreshes Fixed
P1 Preserve older conditional libraries on downgrade Fixed
P1 Restore provider-detail localization Not a real deletion — stale branch, now rebased
Expanded persisted surface needs VISION sign-off Needs @steipete

make check clean on the rebase (0 SwiftLint violations, 23/23 locale catalogs); 226 tests green across the 8 affected suites.

On the downgrade contract you flagged for owner acceptance: the projection deliberately omits expanded rules on an older build rather than handing over rules it would misread. A dropped rule is visibly absent; a silently inverted one (session remaining > 80 decoding as session used > 80 on 0.54.0, whose synthesized decoder ignores the unknown key) would be worse than absent. Happy to change that trade if you'd rather older builds see a best-effort translation.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Aug 20, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event exact_review_queue).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

`UsageFormatter.convertedCost` returns the source amount unchanged when it has no
rate for the provider's currency, and both cost producers passed that value
straight through as `costTodayUSD`/`cost30dUSD`. A `Cost today > 5 USD` rule
would then compare, say, €6 against a $5 threshold and pick the wrong branch.

Both producers now keep the amount only when the conversion actually landed in
USD. Otherwise the predicate sees no value and evaluates false, which is the
existing contract for a metric the provider does not report. The rendered text is
untouched and still shows the provider's own currency.
@wdmitchelluk

Copy link
Copy Markdown
Contributor Author

P1 — Reject unconvertible cost values for USD predicates → fixed in 659a813

Real bug, and my own code comment was the tell — I wrote "without a rate convertedCost returns the source amount, which beats dropping the datum entirely" while discarding the returned currency code. That is exactly backwards for a comparison: Cost today > 5 USD would happily match €6.

Both producers now keep the amount only when the conversion actually landed in USD:

let toUSD = { (value: Double) -> Double? in
    let converted = UsageFormatter.convertedCost(
        value,
        preferredCurrency: "USD",
        providerCurrency: sourceCurrencyCode)
    return converted.currencyCode == "USD" ? converted.value : nil
}

Guard mirrored in MenuBarLayoutPreview.liveData as you asked, and the call sites moved from map to flatMap. Dropping the datum lands on the existing contract — a predicate with no value evaluates false and the else branch renders — rather than inventing a third behaviour. The rendered costToday/cost30d text is untouched and still shows the provider's own currency.

Balance needs no equivalent guard: balanceAmountsUSD parses OpenRouter's $-prefixed rows, which the plugin formats as USD by construction, so no conversion is involved.

Coverage: `cost in an unconvertible currency yields no USD metric` in StatusItemConditionalSignatureTests sets the token snapshot's currencyCode to XXX with a cost of 6 against a 5 threshold, and asserts todayUSD/last30DaysUSD are nil while the display string still renders; then flips to USD and asserts todayUSD == 6.

make check clean; 227 tests green across the 8 affected suites.

Remaining item is the VISION sign-off for @steipete — no actionable code findings left from either reviewer.

@clawsweeper re-review

@clawsweeper

clawsweeper Bot commented Aug 20, 2026

Copy link
Copy Markdown

🦞🧹
ClawSweeper re-review requested.

I asked ClawSweeper to review this item again.
Action: item re-review queued (workflow sweep.yml, event exact_review_queue).
Result: when the review finishes, ClawSweeper will create the durable review comment if needed or update the existing comment in place.

@clawsweeper clawsweeper Bot added P2 Normal priority bug or improvement with limited blast radius. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. and removed P1 Urgent regression or broken agent/channel workflow affecting real users now. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. labels Aug 20, 2026
@clawsweeper clawsweeper Bot added rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. and removed rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. labels Aug 20, 2026
@clawsweeper clawsweeper Bot added rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. and removed rating: 🦞 diamond lobster Very strong PR readiness with only minor maintainer review expected. labels Aug 20, 2026
@clawsweeper clawsweeper Bot added rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action. and removed status: 👀 ready for maintainer look ClawSweeper has no concrete contributor-facing blocker left for this PR. rating: 🐚 platinum hermit Good normal PR readiness with ordinary maintainer review expected. labels Aug 20, 2026
@steipete
steipete merged commit bc3c4b3 into steipete:main Aug 20, 2026
9 checks passed
steipete added a commit that referenced this pull request Aug 20, 2026
steipete added a commit that referenced this pull request Aug 20, 2026
* feat(kiro): show overage credits against their cap

kiro-cli /usage states plan credits alone and omits the overage section
for organization accounts, so a spent plan looks like the account is out.
Read GetUsageLimits with the CLI's own token (read-only) and surface
overage as a second credit window plus charges against the overage budget.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(kiro): reject impossible usage counters from GetUsageLimits

An overage larger than total usage would clamp planUsed to zero and
overwrite valid CLI numbers. Honor API-disabled overage over a stale
CLI Enabled line so enrichment cannot resurrect a cap the API says is off.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(kiro): reject GetUsageLimits payloads that exceed the plan

Drop enrichment when plan usage is above the plan ceiling, and treat an unrecognized overage status as unknown so the CLI overage line can still stand.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: import FoundationNetworking for Linux URLSession types

* fix(kiro): resolve the CLI state database on Linux

GetUsageLimits enrichment always looked under macOS Application Support,
so Linux refreshes never found data.sqlite3.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(kiro): reject overage above cap and honor API currency

Best-effort GetUsageLimits data should not present a spend window
above its ceiling or format non-USD charges as dollars.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(kiro): propagate cancellation from usage-limits enrichment

A cancelled GetUsageLimits call now fails the refresh instead of publishing a CLI-only snapshot as success.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(kiro): drop CLI USD overage estimate when the API currency is not USD

A missing overageCharges field no longer lets a dollar CLI fallback render in the API's non-USD currency.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(kiro): add locale entries for overage copy

Translate the new Overage window title and Overage credits left detail row in every app catalog so non-English UIs no longer fall back to English.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(kiro): honor API-disabled overage and localize cap phrases

GetUsageLimits DISABLED now replaces a stale CLI Enabled status, and Kiro "of N" detail values go through the of %@ localization key.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(i18n): expect localized OpenRouter API key details in zh-Hant

Provider detail titles and catalogued labels go through L after #3084, so the popup test must assert the Traditional Chinese strings.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(kiro): clamp overage that runs slightly past the cap

Rejecting over-cap counters dropped the whole GetUsageLimits payload and hid overage on organization accounts, so the gauge now clamps to the cap instead.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(kiro): keep over-cap overage and localize credit units

Clamping hid the amount Kiro actually billed, and overage usage still rendered the English "credits" suffix in other locales.

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix(kiro): keep CLI overage when API omits the cap

ENABLED without overageCapWithPrecision is incomplete enrichment, not a disabled account.

* fix(kiro): mark API-enabled overage when the CLI omits it

Organization accounts skip the CLI overage section, so the menu-bar modes never saw an Enabled row.

* fix(kiro): keep CLI plan usage when API bonuses are present

GetUsageLimits folds bonus spend into currentUsage, so overwriting the plan gauge would double-count bonus credits.

* fix(kiro): parse bonus-inclusive usage above the plan limit

GetUsageLimits folds bonus spend into currentUsage, so rejecting planUsed > planLimit dropped overage enrichment for those accounts.

* fix: preserve stacked menu bar layout line breaks (#3094)

* Preserve card menu-item subclass during cached swaps (#3093)

* Preserve menu item subclasses during cached swaps

* Add native menu proof for cached shell swaps

---------

Co-authored-by: Kiran Magic <262980978+kiranmagic7@users.noreply.github.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>

* docs: changelog for #3093

* Extend menu bar conditionals beyond usage percentages (#3088)

* Extend menu bar conditionals beyond usage percentages

Conditional predicates could only compare four percent-used windows. They now
compare 18 metrics across four units: percent windows, the direct
primary/secondary/tertiary lanes, four reset countdowns, three pace deltas,
run-out, credit balance, and today/30-day cost. Metrics with two readings
(percent windows, lanes, balance) gain a used/remaining select, so
"session > 50% used and session resets in < 2h" is expressible.

Pace, run-out, balance and cost were only carried as display strings, which
cannot be compared, so MenuBarLayoutRenderMetrics carries their numeric twins
pre-rounded to the same granularity as the text they mirror.

Three refresh gates needed widening for the new data dependencies:
- The title cache key had no component that moves with the clock, so a
  countdown predicate would have served its pre-flip title indefinitely. It now
  keys on the per-conditional outcome, evaluated once per render.
- The four observation signatures gated on display tokens; a predicate on cost
  or balance has no token. They now also read the conditionals' metrics, which
  additionally fixes lane tokens inside conditional branches being invisible to
  the lane signature.
- A reset-countdown predicate flips at an instant nothing else ticks on, so the
  countdown scheduler wakes at `resetsAt - threshold`.

The conditional library is now decoded element-wise: this change makes
forward-incompatible metric values possible for the first time, and one unknown
value would otherwise have wiped the whole library on a downgrade.

Ships an "Auto % / Resets in" default that renders the automatic percentage
while the lane has headroom and the reset countdown once it is spent.

* Sign the readings conditional predicates actually compare

Three observation-signature gaps let a predicate flip without a redraw:

- Cost signatures recorded only the currency-formatted string, so two token-cost
  updates could cross a threshold while both formatted to the same cent. A
  referenced cost metric now signs the unrounded amount losslessly.
- The balance signature recorded only the rendered "Remaining" row, so a
  `balance used` predicate — which reads the "Used" row no token surfaces — was
  entirely unsigned. Both amounts are now signed.
- The lane signature recorded the displayed reading, which follows
  `usageBarsShowUsed` and clamps remaining at zero, while `RateWindow.usedPercent`
  deliberately preserves over-quota values. A used-direction predicate such as
  `primaryLane > 105%` could move 104% -> 106% against a constant `0.000`.

The lane signature is now scoped to what the layout renders, and a new
conditional-window signature covers what conditionals read: the raw used percent
(which remaining derives from, so it covers both directions) plus `resetsAt`,
which countdown predicates depend on and no display token contributes.

* Tick clock-derived predicates that no token schedules

`menuBarWeeklyPaceRefreshDelays` is gated on a placed `.pace(.weekly)` token and
only wakes once, at the pace-eligibility boundary. Excluding `runsOutIn` from the
conditional reset schedule on the assumption that scheduler covered it therefore
left a hole: a layout whose only pace or run-out reference is a predicate got no
clock wake-up at all, so it kept rendering the branch that was true when the
value last moved.

Referenced weekly-pace predicates now also trigger the eligibility wake-up, and
any referenced pace or run-out predicate schedules a minute tick. Both numbers
are pre-rounded to the granularity the menu bar shows -- whole percentage points
and whole minutes -- so a minute tick is exactly enough, and it is the cadence a
`.resetCountdown` token already costs. Money predicates deliberately schedule
nothing: they move only when new provider data arrives.

* Keep older releases' conditional libraries readable on downgrade

Decoding the library element-wise only helps builds that already have the
lenient decoder. The build a user actually downgrades to decodes
`menuBarLayoutConditionals` strictly and falls back to `[]`, so one saved rule
using a new metric would empty the entire library there.

The conditional library now dual-writes the way layouts already do:
`menuBarLayoutConditionalsV2` keeps full fidelity, and the original key keeps an
older-readable projection. `loadLibrary` mirrors `preferredLayout` — the current
key wins unless the legacy key disagrees with its own projection, which only
happens when an older release wrote it, and that edit must survive.

The projection drops an entry when any clause uses a metric outside the original
four, and also when any clause uses a non-`.used` direction. The second case is
the subtler one: an older release's synthesized decoder ignores the unknown
`direction` key, so `session remaining > 80` would come back as
`session used > 80` and render the opposite branch. A missing rule is visibly
missing; an inverted one is not.

* Drop cost metrics that could not be converted to USD

`UsageFormatter.convertedCost` returns the source amount unchanged when it has no
rate for the provider's currency, and both cost producers passed that value
straight through as `costTodayUSD`/`cost30dUSD`. A `Cost today > 5 USD` rule
would then compare, say, €6 against a $5 threshold and pick the wrong branch.

Both producers now keep the amount only when the conversion actually landed in
USD. Otherwise the predicate sees no value and evaluates false, which is the
existing contract for a metric the provider does not report. The rendered text is
untouched and still shows the provider's own currency.

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>

* Prevent RPC pipe writes from aborting after child exit (#3095)

* fix: prevent RPC pipe writes from aborting after child exit

* test: repin UsageFetcher codex identity anchor after pipe-write refactor

* Publish live Grok tokens and xAI spend into Usage & Spend (#3085)

* Publish live Grok tokens and xAI spend into Usage & Spend

Enabled Grok and xAI now join the shared spend catalog instead of only
inflating the unavailable denominator. xAI contributes vendor-metered
daily USD from the Management API chart; Grok contributes local session
tokens. SuperGrok credits and xAI prepaid balance stay quotas, not spend.

* docs: add #3085 to changelog

* Fix lint on Grok and xAI spend messages

Wrap no-data copy under 120 characters and drop a redundant throws
on the prepaid-balance mapping test.

* Fix Grok/xAI spend publication, Today, and coverage

Preserve xAI analytics failures as unavailable instead of known-zero
spend, publish local Grok tokens when remote billing fails, pin Today
to the current UTC/local day, and keep xAI history as a 30-day source.

* Fix Grok and xAI spend edge cases

* Fix OpenRouter localization test after #3086

#3086 scoped localizedProviderDetails to DeepSeek and z.ai, so generic
OpenRouter details keep canonical English. The merge test still expected
zh-Hant "API 金鑰" and failed macOS shard 0.

* Align OpenRouter localization test with main

Peter restored generic title/row L() localization in 84a4ca7 after
#3086 scoped it away. The merge kept the canonical-English assertion
from the earlier CI fix; match the restored shared catalog instead.

* test: reconcile gatekeeper anchors and fingerprints with Grok/xAI spend clusters

* test: include Grok and xAI in the cost-capable dashboard source contract

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>

* docs: credit #3085 and #3088 changelog entries

* chore: finalize 0.54.0 changelog and bump build to 127

* docs: update appcast for 0.54.0

* chore: open 0.54.1 unreleased changelog section

* Clarify five-hour quota wording in Simplified Chinese (#3070)

* Clarify Simplified Chinese five-hour quota label

* Derive Simplified Chinese session quota labels from duration

---------

Co-authored-by: UNGETSU <ungetsu@UNGETSUdeMacBook-Air.local>

* Fix agent session menu width (#3096)

* fix(alibaba): resolve mainland Personal/Solo sec_token from the console shell (#3098)

Mainland Personal/Solo Token Plan (cn-personal) fails with a 200
`BailianGateway.Login.NotLogined` body ("Alibaba Token Plan login required")
even with fresh, valid aliyun cookies, because the request lacks the
`sec_token` the OneConsole gateway requires. #2533 already forwards the token
when present, but it was never resolvable for this path for two reasons:

- The console shell only server-renders `window.ALIYUN_CONSOLE_CONFIG.SEC_TOKEN`
  for a genuine same-origin document navigation; a bare GET receives a
  token-less shell. Send the browser-navigation headers (Referer, Sec-Fetch-*,
  Accept-Language) so the shell includes the token.
- The shell embeds it as an upper-case, unquoted key (`SEC_TOKEN: "..."`), but
  `extractSECToken` only matched the lower-case `secToken`/`sec_token` shapes.
  Add the `SEC_TOKEN` pattern.

With both, the scraper resolves the Personal `sec_token`, the gateway returns
real usage, and the mainland Personal/Solo card renders. Verified end-to-end
on a real cn-personal account: `secTokenSource=resolved`, body `message=Success`,
5-hour/weekly windows populated (was "login required").

Adds AlibabaTokenPlanSECTokenScrapeTests covering the upper-case shell format,
the existing lower-case shapes, and the no-token case.

Refs #2500, #2349, #2370, #2533.

Co-authored-by: LeoLin <leolin990405@gmail.com>

* Add BigModel CN account balance to z.ai provider (#3109)

* Add BigModel CN account balance to z.ai provider

* Bound balance timeout, region-aware override validation, null-safe amounts

---------

Co-authored-by: RunhuaHuang <RunhuaHuang@users.noreply.github.com>

* docs: credit #3070 #3096 #3098 #3109 changelog entries

* test: repair gatekeeper anchors after #3096 menu refactor

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
Co-authored-by: kiranmagic7 <kiranmagic@proton.me>
Co-authored-by: Kiran Magic <262980978+kiranmagic7@users.noreply.github.com>
Co-authored-by: William Mitchell <13799071+wdmitchelluk@users.noreply.github.com>
Co-authored-by: Alec Gutman, Chip <44984861+Chipagosfinest@users.noreply.github.com>
Co-authored-by: UNGETSU <44204222+YunyueLi@users.noreply.github.com>
Co-authored-by: UNGETSU <ungetsu@UNGETSUdeMacBook-Air.local>
Co-authored-by: KaranocaVe <39995482+KaranocaVe@users.noreply.github.com>
Co-authored-by: Zhongyue Lin <101193087+LeoLin990405@users.noreply.github.com>
Co-authored-by: LeoLin <leolin990405@gmail.com>
Co-authored-by: Xigua <rh2897@columbia.edu>
Co-authored-by: RunhuaHuang <RunhuaHuang@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. P2 Normal priority bug or improvement with limited blast radius. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. proof: sufficient Contributor real behavior proof is sufficient. rating: 🦐 gold shrimp Decent PR readiness signal, but merge confidence is limited. status: ⏳ waiting on author ClawSweeper has contributor-facing work open and is waiting for author action.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants