Skip to content

Show the provider cards in the menu, with a Detail switch per card - #358

Open
ozgurasan-agent wants to merge 9 commits into
vinzdg:mainfrom
ozgurasan-agent:codex/menu-cards-review-fixes
Open

ozgurasan-agent wants to merge 9 commits into
vinzdg:mainfrom
ozgurasan-agent:codex/menu-cards-review-fixes

Conversation

@ozgurasan-agent

Copy link
Copy Markdown

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 a92ad31 up until #313 merges.

The two blockers

SwiftUI layout per provider, every second, with the menu closed. Correct, and worse than it reads: builtMenu was set when the menu was built and never let go, so every publication afterwards drew a card per provider and asked for its fittingSize — a forced layout — for a menu nobody was looking at. A local runtime republishes every second. menuDidClose now 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, and testTheItemKeepsOneWidthAsTheFiguresMove with its < 150 bound. You are right that nothing in "show provider cards" required it; it should never have been in this branch. StatusItemSummary.swift is now additive against main and 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 inside make. make now 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. testTheCardsFollowTheNotchsSettings set 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 .remaining reaches the reset wording.

Testing

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.

Özgür Asan 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant