Show the provider cards in the menu, with a Detail switch per card - #358
Open
ozgurasan-agent wants to merge 9 commits into
Open
ozgurasan-agent wants to merge 9 commits into
ozgurasan-agent wants to merge 9 commits into
Conversation
added 9 commits
September 22, 2026 14:14
The menu bar's menu now opens straight onto a card per provider: the same ProviderLimitsContent the notch's tooltip starts from, on a plain rounded surface. Each card has a Detail switch in its header; closed it is the limits, open it is everything the notch's tooltip carries, and the card grows downward to hold it. The choice is per provider and survives a relaunch. Resets also say when they land, "(13:30)" or "(Thu 01:30)", on any card whose every row has room for it. The card always lays out at the height its content asks for and is pinned to the top of its menu item. Offered less, SwiftUI squeezed the one thing that gives — a usage line's minimumScaleFactor — so one "% Used · % left" came out smaller than the one under it. Centred in an item whose frame is its fitting height rounded up, the header moved by a different fraction open than closed, taking the switch with it. The switch sits on the title's line, so a tier named under the title no longer pushes it down.
The only conflict was the string catalog, where both sides had appended keys: upstream added 36, this branch added three and retired one — the "Time remaining" explanation, which now describes the reset time the row carries beside the countdown. No key was touched by both sides, so the resolution is upstream's file with this branch's own four changes applied to it and every upstream entry left byte-for-byte as it was.
On screen, turning Detail on slid the switch's track up into place from below while the word beside it stood still. The offscreen checks could not see it, because it happens between two layouts. The item is handed the new card before it is re-measured, so for one pass an open card is laid out in a closed card's frame. With only a maximum on the root frame, a card taller than its frame kept its own height and was centred on it, putting the header half the growth too high. The track carried `.animation(_:value:)`, which animates its position as well as its colour and knob, so it glided back from there. The root frame now has minimums too, so it is always the item's size and a card that does not fit yet hangs off the bottom until the item grows. The switch's animation is scoped with `animation(_:body:)` to the accent's opacity and the knob's offset, so nothing about the track's geometry is ever animated. testAnOpenedCardInTheClosedFrameKeepsItsHeaderInPlace draws the header in that in-between state; against the old frame it differs from the settled header in 22,361 pixels.
With one Claude Code login the menu card read "Claude Gmail Usage": the account label, taken from the signed-in address's domain, was added to every Claude ring whether or not there was another ring to tell it from. The label exists to tell two accounts apart; with only one it names a product that does not exist. displayNames(for:) is the one caller that sees every profile, so it now names a lone profile plain "Claude". Two or more profiles keep their labels, and the whole address where two labels collide, exactly as before. The name reaches the menu, the notch, Settings and notifications from there, so all of them change together.
Clicking Refresh all took the menu away, and with it the cards the refresh was for. AppKit ends a menu's tracking on mouse-up and only then sends the action, and nothing public keeps a plain item's menu open. The row is now a view of its own, as the cards' Detail switch is, so it takes the click and the menu stays up. While the pass runs the row reads "Refreshing…" in a disabled row's ink and ignores clicks; each card redraws as its reading lands. refreshNow() hands back the pass, or the one already running, so the row knows when it is over. The wait ends with the menu, so a pass stuck behind an unanswered keychain prompt cannot leave every later menu saying "Refreshing…". A row with a view gets nothing drawn for it, so MenuCommandRowView puts it back, measured off AppKit's own rows in the same menu at 2x on macOS 27: the title at 30pt, the shortcut's column, labelColor and tertiaryLabelColor ink, and the highlight. The highlight is the system's .selection material with a 5pt inset and 7pt corners, which matches AppKit's own to the pixel and follows the accent colour and the appearance. The menu's delegate tells the row when it is highlighted, because AppKit tracks that for a row with a view but draws nothing. AppKit hands Return to a highlighted row with a view instead of acting on it, and only to a view that takes first responder. The row takes it only while highlighted — taking it always made the row grab the keyboard as the menu opened and draw itself highlighted — and answers Return the way it answers a click. ⌘R still matches the item's key equivalent, and Escape, should it reach the row, cancels tracking. "Refreshing…" is translated for every locale Refresh all is.
Two files conflicted. TooltipCard.swift: main ramps the limit and money bars' colour across the whole range, and renamed the Codex reset-credit section to UsageResetCreditsSection, now drawn for Claude too. This branch had moved the tooltip's rows into ProviderDetailContent, which the menu's cards share, and measures a bar against the surface's own column (providerDetailWidth). Both are kept: main's barColor with the branch's track width, and the reset credits drawn from ProviderDetailContent with main's availableResetCredits(at:), so an opened menu card shows them as the notch does. The string catalog: main made 889 changes, among them two new locales, Turkish and Indonesian; this branch added four keys and retired one. No key was changed by both sides except the retired one, "Time until usage resets, such as 3 Days 3h or 3h 20m.", which main gave Turkish and Indonesian; nothing reads it any more, so it stays retired. The resolution is main's file byte-for-byte with the branch's four keys added, each now carrying Turkish and Indonesian as well.
refreshNow() now hands back the pass it starts, and the wake and language
observers call it as the single expression of MainActor.assumeIsolated.
That closure's result type is generic: newer compilers settle it to
Void and drop the pass, but the Xcode 26.6 toolchain CI builds with
infers it from both sides and fails to type-check —
conflicting arguments to generic parameter 'T'
('Void' vs. 'Task<Void, Never>?')
— so neither the unit tests nor the unsigned bundle built. The
observers have no use for the pass, so they now discard it outright.
Three things the review found, and the nit beside them. The menu was laid out after it closed. `builtMenu` was set when the menu was built and never let go, so every reading that landed afterwards drew a card per provider and asked SwiftUI for its height — a forced layout — for a menu nobody was looking at. A local runtime republishes every second, so one open cost N layouts a second for the rest of the session. `menuDidClose` lets go of the menu now; the next open rebuilds from scratch, as it always did. The menu bar item keeps its reserved width. Dropping `percentRoom` and `countdownRoom` made the item follow whatever its figures currently measure, which moves every item to its left about once a minute as "1h 00m" becomes "59m" — the change closed as vinzdg#314, and nothing about drawing provider cards needed it. `testTheItemKeepsOneWidthAsTheFiguresMove` comes back with it. The Detail switch was dead until something made its card lay out twice. The card reports where the switch landed, and that was being used to look the hosting view up in the menu by `representedObject` — which is not assigned, on an item not yet in any menu, at the moment the card is first measured inside `make`. The report goes straight to the view that will hit-test the click instead. The 30s clock no longer writes `model.now`. It exists to keep the countdowns on an open menu current, and writing the model republished it to every notch on every tick; the notches run their own clocks for this. `testTheCardsFollowTheNotchsSettings` set a threshold on the fleet and then asserted the fleet held it, which it could not fail. It now renders the card under two sets of Watch/Critical limits and asserts the pixels differ.
3 tasks
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.
Replaces #346. Same feature, with @vinzdg's review answered — thank you for reading it that closely.
Still stacked on #313, as #346 was: this branch carries #313's commit, so review from
a92ad31up until #313 merges.The two blockers
SwiftUI layout per provider, every second, with the menu closed. Correct, and worse than it reads:
builtMenuwas set when the menu was built and never let go, so every publication afterwards drew a card per provider and asked for itsfittingSize— a forced layout — for a menu nobody was looking at. A local runtime republishes every second.menuDidClosenow nils it. The next open rebuilds from scratch, as it always did, so nothing is lost by letting go.The fixed-width menu bar item. Restored —
percentRoom,countdownRoom, the right-aligned percent, andtestTheItemKeepsOneWidthAsTheFiguresMovewith its< 150bound. You are right that nothing in "show provider cards" required it; it should never have been in this branch.StatusItemSummary.swiftis now additive againstmainand carries only #313's glyph frames.The two beside them
The Detail switch looked dead on first open. Also correct, and the diagnosis was exact. The card reports where its switch landed, and that report was looking the hosting view up in the menu by
representedObject— which is unassigned, on an item not yet in any menu, at the moment the card is first measured insidemake.makenow hands the card a setter bound to the very view that will hit-test the click, so the rect is right before the item is ever shown. The menu scan is gone.The 30s clock wrote
model.now. It exists to keep an open menu's countdowns current; writing the model republished it to every notch on every tick. It redraws the cards and nothing else now — the notches run their own clocks for this.Nit.
testTheCardsFollowTheNotchsSettingsset a threshold on the fleet and asserted the fleet held it, which could not fail. It now renders the card under two sets of Watch/Critical limits and asserts the drawn pixels differ, plus that.remainingreaches the reset wording.Testing
make test: 1965 tests, 0 failures, 5 skipped.main(through 1.17.0, Offer the weekly limit as the main ring #336 and Name the menu bar's second ring after the window it draws #343 included), so the conflicts Show the provider cards in the menu, with a Detail switch per card #346 had are gone.Why a new PR rather than a push to #346
#346's branch is being worked on in parallel; this was built in a separate worktree off its head so the two could not stomp each other. #346 can be closed in favour of this one.
One thing I did not do:
StatusItemSummary.swift's change was in my working tree before I started and is not mine, so I only reverted it — I have not checked whether whoever wrote it wanted it somewhere else.