Show the provider cards in the menu, with a Detail switch per card - #346
ozgurasan-agent wants to merge 8 commits into
Conversation
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.
|
Thanks @ozgurasan-agent. I want to be fair here: this is not #301 coming back, and I checked the specific things I closed that one over. But it carries #301's actual cost in a new shape, and there's a second thing I can't take. Blocker — SwiftUI layout per provider, every second, with the menu closed. Blocker — it deletes the fixed-width menu bar item. That's the same change I closed #314 for two days ago, and the reason stands: an item that resizes as its numbers change pushes every item to its left along with it, so Two more:
Nit: Fix the two blockers and I'll take it; the feature itself is good and the card rendering is nicely done. Note |
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.
|
Superseded by #358, which carries this same feature with every review finding from this thread addressed (menu layout on close, menu-bar width consistency, switch frame reporting) plus the |
Summary
The menu bar's menu becomes the detailed view. It opens straight onto one card per provider, each with its own Detail switch. Refresh all now refreshes those cards without closing the menu, and a single Claude account is named just "Claude" instead of "Claude Gmail".
Stacked on #313, which is still open. This branch carries #313's commit (
0b2a6de), so until #313 merges, review only the commits after it:a92ad31(the cards),e5e42ca(the switch fix),f33d60e(the Claude name),ca6d274(Refresh all) and the two merges ofmain.Changes
Provider cards in the menu (
a92ad31,e5e42ca)ProviderLimitsContentthe notch's tooltip uses, drawn on a plain rounded surface (MenuUsageCard).Resets in 4h 55m (13:30)or(Thu 01:30). A card shows the time only when every row on it has room, so no card mixes rows with and without it.minimumScaleFactor. The card now always lays out at its full height.plusno longer pushes it down..animation(_:value:)animated the track's position. The root frame now has minimums too, and the animation is scoped withanimation(_:body:)to the accent's opacity and the knob's offset.Refresh all keeps the menu open (
ca6d274)MenuCommandRowView), as the cards' Detail switch is, and it takes the click itself.UsageStore.refreshNow()now returns the pass it starts, or the one already running. It is@discardableResult, so its other callers are unchanged. The wait ends with the menu, so a pass stuck behind an unanswered keychain prompt cannot leave later menus saying "Refreshing…".labelColorfor the title andtertiaryLabelColorfor the shortcut;.selectionmaterial with a 5pt inset and 7pt corners. It matched AppKit's own highlight to the pixel (colour, box and corner curve) and follows the accent colour and appearance. The menu delegate'swillHighlighttells the row when it is highlighted.A lone Claude account is "Claude" (
f33d60e)ClaudeProfile.displayNames(for:)now names a lone profile plain "Claude". Two or more profiles keep their labels, and the full address where two labels collide, exactly as before. The name is set in one place, so the menu, the notch, Settings and notifications all change together.Merged with
main(568dbc8)mainhad moved on by 36 commits, through 1.18.0. Two files conflicted:TooltipCard.swift.mainadded a continuous colour ramp for the limit and money bars (barColor), and renamed the reset-credit section toUsageResetCreditsSection. This branch had moved the tooltip's rows intoProviderDetailContent, which the menu's cards share, and measures bars against the surface's own column (providerDetailWidth). Both are kept:main'sbarColorwith this branch's track width, and the reset credits drawn fromProviderDetailContentwithmain'savailableResetCredits(at:). An opened menu card now shows reset credits just as the notch does.Localizable.xcstrings. The resolution ismain's file byte-for-byte, plus this branch's four keys. Those keys now also carry Turkish and Indonesian, the two localesmainadded. The only key both sides touched is the old reset-time explanation, which this branch had replaced; nothing reads it any more, so it stays retired.Also in this branch, and separable
StatusItemSummary.swiftand its test carry a change that is not part of the menu cards: the menu bar readout no longer reserves a fixed width for its figures, so the item follows the width of what it currently shows.testTheItemKeepsOneWidthAsTheFiguresMoveis replaced bytestTheItemFollowsTheCurrentFigureWidths. It is independent of everything above, and I'm happy to lift it into its own PR.Test Plan
make test-ci): 1965 tests, 0 failures, 5 skipped (the opt-in live checks), on the merged branch.make testgives the same result.New tests:
testALoneAccountIsJustClaudetestRefreshAllRunsWithTheMenuStillOpenAndSaysSoUntilItIsDonetestANextMenuDoesNotInheritAPassThatNeverEndedtestASecondCallerIsHandedThePassAlreadyRunningtestReturnOnTheHighlightedRowRefreshesAndIsPassedOnOtherwisetestEscapeOnTheRowStillClosesTheMenutestCommandRStillRefreshestestTheSwitchDoesNotMoveWhenItIsFlippedandtestAnOpenedCardInTheClosedFrameKeepsItsHeaderInPlace. The second fails against the old root frame (22,361 differing pixels).Checked on screen, in Light mode:
Not checked on screen:
Screenshots / Screen Recordings
The menu was captured on screen while testing. Screenshots can't be attached from the command line; I can add them here if that helps review.