Skip to content

Redesign usage widgets around the binding quota - #3137

Open
iamenahs wants to merge 1 commit into
steipete:mainfrom
iamenahs:widget-readability-redesign
Open

Redesign usage widgets around the binding quota#3137
iamenahs wants to merge 1 commit into
steipete:mainfrom
iamenahs:widget-readability-redesign

Conversation

@iamenahs

@iamenahs iamenahs commented Aug 22, 2026

Copy link
Copy Markdown

Summary

The usage widgets spend their space on a provider switcher that cannot render provider names,
and bury the one number a glance is for.

The switcher lays every provider out as a chip in a single row. That never fits: on large the
names wrap mid-word (Cod/ex, Clau/de, Cur/sor), on medium they truncate to
indistinguishable stems (Co… is both Codex and Copilot), and on small they collapse to a
single letter, where four of five common providers are a C. Meanwhile the percentage is drawn
.secondary — dimmer than its own label — and never says whether it means remaining or used.

Widgets cannot host a menu or a picker, so the chip row is replaced by a pager:
[CL] Claude · 4m ago … ‹ 2/5 ›. One line instead of a row, and the provider whose numbers are
on screen is always spelled out in full.

Changes

  • Every tile leads with the binding quota — the lane with the least left — as one large
    figure with its lane name, its reset, and a bar. WidgetTilePlan picks it; ties keep the
    provider's own ordering.
  • Percentages state their meaning: "Weekly left" / "Weekly used", so usageBarsShowUsed is
    no longer silent.
  • RateWindow.resetsAt is rendered. It was already in the snapshot and dropped on the floor.
    Counts down compactly ("Resets in 47m"); provider wording is the fallback and gets labelled, so
    Codex's bare tomorrow, 12:28 PM no longer appears without context.
  • Nothing is clipped silently. Lanes beyond capacity are counted in a +N more line, and on
    compact tiles that line takes a lane slot so it cannot push content off the edge.
  • Lane capacity is derived, not fixed: it depends on whether the tile also draws a cost block
    and a history chart, so a seven-lane provider does not clip and a one-lane provider does not
    page for nothing. Medium drops to a single column when there is no second lane.
  • .contentMarginsDisabled() on the four redesigned widgets. WidgetKit was applying its own
    content margins on top of the tile's padding, insetting every tile roughly twice as far as
    intended.
  • The provider mark survives tinted and clear appearances. Those render through a luminance
    mask, which turned a brand fill carrying a dark label into an empty white square. Outside
    .fullColor the mark inverts to a faint plate with a bright label.
  • Bar track 8% → 12% (nearly invisible in dark mode) with a minimum visible nub, so a 1%-left
    lane is not an empty track.
  • Six near-duplicate size views collapse into one UsageTile, so the usage and switcher widgets
    cannot drift apart. Size differences are values on WidgetTileSize.

BurnDown widgets are deliberately untouched.

Screenshots

Rendered headless at true macOS tile sizes with synthetic fixtures — no account identity, spend,
or token counts.

Before / after — same fixture, same framing, rendered from the pre- and post-redesign views

Before and after

Light

Light

Dark

Dark

Clear / tinted desktop appearance

Clear

Test plan

  • make check — 0 violations
  • make test — green apart from a pre-existing failure, see caveat
  • xcodebuild CodexBarWidgetExtension — Release, arm64 + x86_64
  • 43 focused tests over the pure layout logic (headline selection, lane capacity, overflow
    reservation, reset rounding, pager wrap-around, monogram uniqueness, tinted-mode mark
    styling, WCAG contrast for every provider colour)
  • 40 tiles rendered across light / dark / clear, including the placeholder(in:) gallery
    path that nothing previously covered
  • Edge cases: 3%-left, seven quota lanes, nine providers, balance-only provider, show-used
    inverted, longest provider name
  • Device check: locally signed build installed, widget extension confirmed reading its App
    Group container (frames not attached — they contain live usage)

Full proof artifact: .github/pr-proof/widget-readability-redesign.log

Commands run

make check
make test
swift build --target CodexBarWidget
xcodebuild -project WidgetExtension/CodexBarWidgetExtension.xcodeproj \
  -scheme CodexBarWidgetExtension -configuration Release ARCHS="arm64 x86_64"

Verification caveat

StatusMenuSwitcherRefreshTests → "merged provider switch updates live tab rows in place" fails
with 2 issues. It fails identically on a clean checkout of the same base (27c7f33) with this
branch absent — verified in a separate worktree. It is a headless AppKit menu-identity assertion;
this diff touches only Sources/CodexBarWidget.

Automated review follow-up

All three Codex findings on c9feddf3a2 were reproduced and fixed:

  • P1 — binding lane hidden by a provider row cap. Real. Kimi writes four rows
    (primary, secondary, kimi-monthly, kimi-code-7d) while its resolver caps compact
    families at 3, and the cap was applied before the headline was chosen. A 1%-left
    kimi-code-7d lane was discarded, a healthier lane became the headline, and overflowCount
    stayed 0 so nothing hinted at the omission. The cap now curates only what a tile lists;
    headline and overflow are computed against every lane the provider reports, and the provider's
    intended row count is still honoured because the headline occupies one of those rows.
  • P2 — reset caption missing for non-Codex providers. Real, and it explains a device
    screenshot where the Claude tile showed no reset line. The generic writer emits
    primary/secondary/tertiary rows with a percentage only and leaves the reset on the
    entry's own windows; only legacy Codex windows were reconstructed. Reset is now recovered from
    the matching entry slot, with the row's own percentage left authoritative. Fixed widget-side so
    snapshots already on disk benefit without a format change.
  • P2 — stale reset wording after the reset date. Real. A known resetsAt in the past fell
    through to the cached description, so a stale snapshot kept reading "Resets in 4h" after the
    reset had happened. An expired known date now renders nothing.

Each carries a regression test that fails without the fix. Widget test count is now 43.

Reviewer notes

  • Headline policy — the one product decision here. The headline follows the binding lane, so
    it can change identity between refreshes (Weekly → Session) as values move. It is always
    labelled. Pinning it instead is a one-function change (WidgetTilePlan.bindingLane(in:)
    lanes.first); everything downstream is independent of which lane wins. Say the word and I will
    push that variant or a settings toggle. Worth weighing: the P1 fix above exists precisely to
    protect this signal — a provider row cap was hiding a 1%-left lane behind a healthier one, and a
    pinned headline would have read "Session 88%" while the account was actually blocked.
  • Severity colour is lost in clear/tinted appearances, since those render monochrome. The figure
    is unchanged; this matches how first-party widgets degrade.
  • widgetRenderingModeOverride is a snapshot-rendering seam and is always nil in the shipping
    widget. WidgetKit owns widgetRenderingMode and no host sets it outside a widget, so the
    tinted variants are otherwise unreachable from a preview.

@clawsweeper

clawsweeper Bot commented Aug 22, 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: c9feddf3a2

ℹ️ 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".


var body: some View {
let color = WidgetColors.color(for: self.entry.provider)
let allLanes = WidgetTileLane.lanes(for: self.entry, limit: self.laneLimit)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Select the binding lane before truncating provider rows

For compact Kimi widgets, laneLimit is 3 even though live snapshots can contain four quota rows (UsageStore+WidgetSnapshot.swift appends kimi-code-7d). Applying that limit here before WidgetTilePlan chooses its minimum means a fourth lane at, for example, 1% is discarded while a healthier lane becomes the headline; it is also absent from overflowCount, so the tile gives no indication that the actual binding quota was omitted.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Confirmed and fixed in b94815ce9.

Reproduced exactly as described: KimiProviderDescriptor sets widgetRowLimitResolver: { _, _ in 3 } while UsageStore+WidgetSnapshot.swift:507 appends kimi-monthly and kimi-code-7d on top of the generic primary/secondary pair — four rows against a cap of three. With a kimi-code-7d lane at 1% the tile headlined Session 88% and reported overflowCount == 0.

UsageTile now resolves two sets: allLanes (unlimited) and displayLanes (the provider-curated subset). WidgetTilePlan.make takes both, picks the headline from allLanes, and computes overflowCount against allLanes too. The provider’s intended row count is preserved because the headline occupies one of those rows — capacity = min(maxSecondaryLanes, displayCandidates.count - 1) — and Antigravity’s one-row-per-model-family curation still decides which lanes get listed, since that is real curation rather than a prefix.

The same fixture now headlines the 1% lane in red with +2 more. Covered by a provider row cap never hides the binding lane and the curated row count is honoured once the headline takes a row.

Comment on lines +350 to +352
percentLeft: row.window?.remainingPercent ?? row.percentLeft,
resetsAt: row.window?.resetsAt,
resetDescription: row.window?.resetDescription)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Recover reset metadata for generic usage rows

For normal live snapshots from non-Codex providers, entry.usageRows is populated, but the generic snapshot writer at UsageStore+WidgetSnapshot.swift:463-471 stores only percentLeft while the corresponding reset date remains on entry.primary/secondary. Since the preceding resolver only reconstructs legacy Codex windows, these assignments receive nil, so the redesigned reset caption works in previews and Codex but silently disappears for providers such as Claude, Gemini, and Alibaba.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Confirmed and fixed in b94815ce9.

This one also explained a device screenshot I had already misread — the Claude tile showed no reset line while Codex did, and I had put that down to provider data rather than the writer.

WidgetUsageRow.rows now falls back to the matching entry window when a row carries no window:, via a deliberately narrow resolver:

static func slotWindow(for rowID: String, entry: WidgetSnapshot.ProviderEntry) -> RateWindow? {
    switch rowID {
    case "primary": entry.primary
    case "secondary": entry.secondary
    case "tertiary": entry.tertiary
    default: nil
    }
}

Only the reset is taken from it; the row keeps its own percentLeft, so nothing about existing percentage resolution changes. Provider-specific row IDs deliberately do not match, so no reset is invented where the writer has none — slot recovery does not invent resets for provider specific rows pins that.

Fixed widget-side rather than in the writer so snapshots already on disk gain the caption without a snapshot format change. Kimi’s own extra windows still ship without a window:, so their reset stays absent; that needs a writer change and felt out of scope here.

Comment on lines +100 to +105
if let resetsAt {
let interval = resetsAt.timeIntervalSince(now)
if interval > 0 {
return "Resets in \(self.duration(interval))"
}
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Suppress stale reset descriptions after the reset date

When a snapshot contains both resetsAt and a cached resetDescription, reaching the reset date skips this return and falls through to the description fallback below. If the app has not yet published a fresh snapshot, the widget therefore continues showing stale copy such as “Resets in 4h” after that reset has already passed; an expired known date should return nil rather than reuse its old wording.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Confirmed and fixed — GitHub re-anchored this thread onto the newer commit, but the fix is in it. WidgetTilePlan.swift now reads:

if let resetsAt {
    let interval = resetsAt.timeIntervalSince(now)
    guard interval > 0 else { return nil }
    return "Resets in \(self.duration(interval))"
}

An expired known date returns nil instead of falling through to the cached wording. My original test only exercised the resetDescription: nil path, which is exactly why it missed this; a known reset that already passed renders nothing, even with cached wording now passes resetDescription: "Resets in 4h" alongside a past resetsAt.

@iamenahs
iamenahs force-pushed the widget-readability-redesign branch from c9feddf to 8f26c5a Compare August 22, 2026 05:18
@iamenahs

iamenahs commented Aug 22, 2026

Copy link
Copy Markdown
Author

Thanks — all three were real. Reproduced each before fixing, and each fix carries a regression test that fails without it. Pushed as 8f26c5a59.

P1 · binding lane hidden by a provider row cap — confirmed. KimiProviderDescriptor sets widgetRowLimitResolver: { _, _ in 3 }, while UsageStore+WidgetSnapshot.swift:507 appends kimi-monthly and kimi-code-7d on top of the generic primary/secondary pair — four rows against a cap of three. The cap was applied in WidgetTileLane.lanes(for:limit:) before WidgetTilePlan picked its minimum, so a kimi-code-7d lane at 1% was discarded, a healthier lane became the headline, and overflowCount stayed 0.

The cap now curates only what a tile lists. WidgetTilePlan.make takes the full lane set plus displayCandidates, picks the headline from all of them, and counts overflow against all of them. The provider's intended row count is preserved because the headline occupies one of those rows, and Antigravity's one-row-per-model-family curation still governs which lanes get listed.

P2 · reset caption missing for generic rows — confirmed, and it explains something I had already seen on device: the Claude tile showed no reset line while Codex did, and I had put that down to the provider. The generic writer builds WidgetUsageRowSnapshot(id:title:percentLeft:) with no window:, and only legacy Codex windows were being reconstructed. Reset is now recovered from the matching entry slot via a narrow primary/secondary/tertiary resolver; the row keeps its own percentage, and provider-specific row IDs are deliberately not matched so nothing invents a reset it does not have.

Fixed widget-side rather than in the writer so snapshots already on disk get the caption without a format change. Kimi's own extra windows still ship without a window, so their reset stays absent — that one needs a writer change and felt out of scope here.

P2 · stale reset wording after the reset date — confirmed. A known resetsAt in the past skipped the countdown and fell through to the cached description, so a stale snapshot kept reading "Resets in 4h" after the reset. An expired known date now returns nil. My original test only covered the resetDescription: nil path, which is why it missed this.

Widget test count is 43. make check clean, appex builds Release for arm64 + x86_64.

@clawsweeper clawsweeper Bot added P3 Low-risk cleanup, docs, polish, ergonomics, or speculative feature. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. proof: sufficient Contributor real behavior proof is sufficient. 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. labels Aug 22, 2026
@clawsweeper

clawsweeper Bot commented Aug 22, 2026

Copy link
Copy Markdown

Codex review: needs maintainer review before merge. Reviewed August 22, 2026, 1:38 AM ET / 05:38 UTC.

ClawSweeper review

What this changes

This PR replaces separate usage-widget layouts with shared responsive tiles that foreground the lowest remaining quota, add provider paging and reset captions, and improve compact and tinted rendering.

Merge readiness

⚠️ Ready for maintainer review - 3 items remain

No current-head correctness defect remains after the follow-up fixes; this PR needs a maintainer decision on whether the headline should favor the most constrained quota or remain stable across refreshes.

Priority: P3
Reviewed head: b94815ce94f5fb82bbedc5e840a4afe53dbc0a96
Owner decision: Required. See Decision needed.

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) Strong visual proof and focused layout coverage support a good patch; only the deliberate headline-policy choice remains.
Proof confidence 🦞 diamond lobster (5/6) ✨ media proof bonus Sufficient (screenshot): Prepared synthetic before/after widget renders visibly demonstrate the changed layouts across light, dark, and tinted appearances; private account data is absent.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Verified Sufficient (screenshot): Prepared synthetic before/after widget renders visibly demonstrate the changed layouts across light, dark, and tinted appearances; private account data is absent.
Evidence reviewed 4 items Current-head lane handling: The tile plan selects its hero from every reported lane, keeps provider-curated display candidates separate, and counts unshown lanes as overflow; this implements the three prior review corrections on the current head.
Current widget data boundary: The existing switcher timeline resolves its selected provider from supported providers before rendering, so the pager operates on the same provider-selection boundary rather than introducing a separate source of state.
Visual after-fix proof: Prepared before/after, light, dark, and tinted renders visibly show the new pager, binding-quota hierarchy, reset copy, and tinted monograms at the target tile sizes.
Findings None None.
Security None None.

Live Verification

Command: swift build --target CodexBarWidget

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: Build of target: 'CodexBarWidget' complete!

How this fits together

CodexBar’s WidgetKit extension turns app-produced provider usage snapshots into macOS tiles. This changes the presentation layer between quota rows and the usage, switcher, history, and metric widgets.

flowchart LR
A[Provider usage snapshots] --> B[Widget snapshot entries]
B --> C[Tile layout plan]
C --> D[Usage and switcher tiles]
D --> E[WidgetKit widget families]
B --> F[Provider pager]
F --> D
Loading

Decision needed

Question Recommendation
Should the usage widget’s headline always follow the quota lane with the least remaining capacity, even when the lane type can change between refreshes? Keep the binding lane: Show the lowest remaining quota and retain its explicit lane label so the widget foregrounds the most immediate limit.

Why: A dynamic headline favors the quota that currently constrains work, while a pinned headline favors stable visual identity; source review cannot choose that UX trade-off.

Before merge

  • Resolve merge risk (P1) - The large figure can change from one quota type to another as remaining values cross, so maintainers should explicitly accept the urgency-versus-stability trade-off before merge.
  • Complete next step (P2) - The remaining blocker is an explicit maintainer UX choice, not a narrow mechanical repair.
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Widget refactor scope production +1,299/-526; tests +524; 14 files affected The PR consolidates six widget presentation files into shared tile components while adding focused pager and layout coverage.

Merge-risk options

Maintainer options:

  1. Decide the mitigation before merge
    Keep the dynamic binding-lane policy if the widget should always foreground the quota most likely to block work; otherwise pin the provider’s first lane and update the focused tests before landing.
  2. Pause or close
    Do not merge this PR until maintainers decide whether the risk is worth taking.

Technical review

Best possible solution:

Keep the dynamic binding-lane policy if the widget should always foreground the quota most likely to block work; otherwise pin the provider’s first lane and update the focused tests before landing.

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

Not applicable: this is a presentation redesign, and the supplied after-fix renders plus focused source paths provide the relevant verification evidence.

Is this the best way to solve the issue?

Unclear: the shared-tile implementation is coherent and the prior concrete defects are fixed, but maintainers must choose whether changing headline identity between refreshes is desired behavior.

AGENTS.md: found and applied where relevant.

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

Labels

Label justifications:

  • P3: This is a polished widget usability redesign with no demonstrated urgent runtime regression.
  • rating: 🐚 platinum hermit: Overall readiness is 🐚 platinum hermit; proof is 🦞 diamond lobster and patch quality is 🐚 platinum hermit.
  • status: 👀 ready for maintainer look: ClawSweeper has no concrete contributor-facing blocker left for this PR. Sufficient (screenshot): Prepared synthetic before/after widget renders visibly demonstrate the changed layouts across light, dark, and tinted appearances; private account data is absent.
  • proof: sufficient: Contributor real behavior proof is sufficient. Prepared synthetic before/after widget renders visibly demonstrate the changed layouts across light, dark, and tinted appearances; private account data is absent.
  • proof: 📸 screenshot: Contributor real behavior proof includes screenshot evidence. Prepared synthetic before/after widget renders visibly demonstrate the changed layouts across light, dark, and tinted appearances; private account data is absent.

Evidence

What I checked:

Likely related people:

  • steipete: Peter Steinberger authored recent current-main widget/provider hardening and the current switcher timeline lines. (role: recent widget and provider-architecture contributor; confidence: high; commits: 4cdb349cbc57, 37114d21920c; files: Sources/CodexBarWidget/CodexBarWidgetProvider.swift, Sources/CodexBarWidget)
  • JoeVenner: Kimi’s provider-specific row cap is central to the corrected binding-lane behavior. (role: introduced Kimi widget support; confidence: high; commits: 76a63dc55939; files: Sources/CodexBarWidget)

Rank-up moves

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

  • Obtain maintainer confirmation that the dynamic binding-lane headline is the intended widget behavior.

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 (1 earlier review cycle)
  • reviewed 2026-08-22T05:24:04.188Z sha 8f26c5a :: needs maintainer review before merge. :: none

The tiles spent their space on a provider switcher that could not render
provider names, and buried the one number a glance is for.

Switcher: a row of chips never fit. Names wrapped mid-word on large,
truncated to indistinguishable stems on medium ("Co…" is both Codex and
Copilot), and collapsed to a single letter on small, where four of five
common providers are a "C". Widgets cannot host a menu or a picker, so the
replacement pages one provider at a time: one line instead of a row, and the
provider whose numbers are on screen is always spelled out.

Layout: every tile now leads with the binding quota — the lane with the least
left — as one large figure with its lane, its reset, and a bar. Percentages
say "left" or "used" so the display preference is no longer silent. Lanes that
do not fit are counted in a "+N more" line rather than clipped off the edge.
Lane capacity is derived from whether the tile also draws costs and a chart,
so a busy provider does not clip and a sparse one does not page for nothing.

Also fixes, all reproduced before being fixed:
- WidgetKit's default content margins applied on top of the tile's own
  padding, insetting every tile roughly twice as far as intended
- Small tiles truncated the provider name because the timestamp took the width
- Tinted and clear appearances render through a luminance mask, which turned
  the brand-filled provider mark into an empty white square
- The percentage was drawn dimmer than its own label
- Reset times were present in the snapshot and never rendered

Six near-duplicate size views collapse into one `UsageTile`, so the usage and
switcher widgets cannot drift apart.

Verified by rendering every layout headless at the exact macOS tile sizes
(155x155, 329x155, 329x345) across light, dark and clear, including the
gallery placeholder path, plus 37 tests over the pure layout logic.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@iamenahs
iamenahs force-pushed the widget-readability-redesign branch from 8f26c5a to b94815c Compare August 22, 2026 05:33
@iamenahs

Copy link
Copy Markdown
Author

Thanks — noting the two items and one correction.

Headline policy (the decision you flagged). Your recommendation matches what's implemented: the headline follows the binding lane. I'd already surfaced this in the PR body as the first reviewer note, and I agree it's a product call rather than a correctness one, so I won't pre-empt it.

To make the alternative cheap to evaluate, pinning it is a one-function change — WidgetTilePlan.bindingLane(in:) is the only place the choice lives:

private static func bindingLane(in lanes: [WidgetTileLane]) -> WidgetTileLane? {
    lanes.first    // instead of min(by: remainingPercent)
}

Everything downstream — lane listing, overflow counting, severity, the reset caption — is independent of which lane becomes the headline, and headline is the lane with the least left plus headline ties keep the provider ordering are the two tests that would need inverting. Happy to push that variant, or a settings toggle, on a maintainer's word.

Worth adding to the decision: the binding lane is what the P1 fix in this PR exists to protect. A provider row cap was hiding a 1%-left Kimi lane behind a healthier one, so a pinned headline would have shown Session 88% while the account was actually blocked. Pinning is still a legitimate choice for stability — it just gives up that signal, and the caption naming the lane is what keeps the dynamic version honest.

Live verification. The swift build --target CodexBarWidget FAIL looks environmental rather than a real build break — the log shows it aborted during setup, before step 1:

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

That's Corepack blocking on a pnpm download, so the Swift build never ran. For the record, on b94815ce9:

  • swift build --target CodexBarWidgetBuild of target: 'CodexBarWidget' complete!
  • xcodebuild -scheme CodexBarWidgetExtension -configuration Release ARCHS="arm64 x86_64"** BUILD SUCCEEDED ** (the real appex target, not just SwiftPM)
  • make check → 0 violations
  • 43 widget tests pass; full make test is green apart from the pre-existing StatusMenuSwitcherRefreshTests failure documented in the PR body, which reproduces on a clean 27c7f334e worktree

One note on the diff stat. The production +1,299 / -526 figure counts docs/screenshots/*.png and .github/pr-proof/*.log as production. Actual shipped Swift is roughly +1,300/-526 across six files under Sources/CodexBarWidget, and the net is smaller than it reads because six near-duplicate size views collapse into one UsageTile.

@steipete

Copy link
Copy Markdown
Owner

Maintainer review: technically this is in good shape — merges cleanly onto current main, builds, all 134 widget-suite tests pass, CI is green, and the before/after screenshots make a persuasive case (the Co…-is-four-providers problem is real, and rendering resetsAt instead of dropping it is a straight win). The UsageTile consolidation of six near-duplicate size views is also the right structural move, and leaving BurnDown untouched keeps the blast radius contained.

Holding the merge for an owner decision since this reshapes the shipped appearance of all four usage widgets — @steipete, the screenshots in the description are worth the look. Two things to note when deciding:

  1. The binding-quota-first layout changes what users see at a glance (biggest number = least-left lane rather than the provider's primary lane). That's arguably more useful, but it's a semantic change to what the big number means, and it varies per refresh as lanes drain.
  2. .contentMarginsDisabled() fixes genuine double-inset, but means our padding is now load-bearing across macOS versions.

No code changes requested from my side.

@iamenahs

Copy link
Copy Markdown
Author

Thank you and especially for running the full 134-test widget suite. That gives confidence across the broader provider surface I couldn't fully validate myself.

On point 2: fair, and this was a deliberate tradeoff. There are two clean fixes for the doubled inset:

  1. Disable the system margins and let the tile own its padding, as in the current patch.
  2. Keep the system margins and remove the tile padding entirely - which removes the load-bearing concern outright.

I chose the first because 14pt is about 2pt tighter per side. On the 155pt small tile, that extra 4pt is where the truncation fix lives: ProviderTitle.compactCharacterBudget is tuned against the resulting 127pt content width. It also keeps the headless ImageRenderer proofs faithful, since those render outside a widget host where system margins are absent. (Shrinking our padding while keeping the system's isn't a third option- that's still doubled, just less.)

If you prefer the system-owned approach, I'm happy to switch it. It's a small diff, followed by a character-budget retune and a re-render to confirm nothing regresses. As shipped, that inset is single-sourced through WidgetLayout.tilePadding (WidgetDesignSystem.swift:21) and consumed at seven call sites, so retuning it for a future macOS is a one-line change rather than a hunt.

On point 1: "it varies per refresh as lanes drain" is the strongest form of the objection. Naming the lane — such as "Weekly left" — keeps the figure accurate, but it doesn't preserve positional habit. The pinned variant is a one-function change from WidgetTilePlan.bindingLane(in:) to lanes.first, plus two test updates.

I'll keep the current binding-lane behavior unless @steipete prefers the pinned variant.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

P3 Low-risk cleanup, docs, polish, ergonomics, or speculative feature. proof: 📸 screenshot Contributor real behavior proof includes screenshot evidence. proof: sufficient Contributor real behavior proof is sufficient. 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.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants