Skip to content

Redesign usage widgets around the binding quota - #3137

Open
iamenahs wants to merge 3 commits into
steipete:mainfrom
iamenahs:widget-readability-redesign
Open

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

Conversation

@iamenahs

@iamenahs iamenahs commented Aug 22, 2026

Copy link
Copy Markdown
Contributor

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.

Runtime proof (on-device)

Follow-up to the maintainer re-check: real WidgetKit desktop captures from the packaged
extension
(CodexBarWidget.appex, Developer ID signed, debug-config app group), running with
the app process dead — timelines and button intents are the extension's own. All frames carry a
synthetic seeded snapshot validated against the real decoder; no live account data appears.
Full steps in .github/pr-proof/widget-readability-redesign.log.

The pager paging in place through five providers, with wrap-around at the end:

Runtime pager cycle

The four-lane Kimi shape holding its row budget (this review round's fix), and the vibrant
rendering keeping marks legible:

Runtime modes

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.

The maintainer re-check on b94815ce9 found one more, also real and fixed in 2cdfe0734:

  • Curation/reservation intersection. The overflow-row reservation asked whether the curated
    remainder exceeded capacity, while overflowCount counts every reported lane. Four lanes,
    three display candidates, the hero inside them and a two-row budget meant no slot was reserved,
    yet "+1 more" still rendered — three rows in a two-row budget. Reservation now derives the
    overflow line from all lanes and gives up a slot only when the line would actually exceed the
    budget
    , so a medium tile with room for lanes plus the line keeps its density (the literal
    "reserve whenever anything is omitted" would have dropped a lane the tile has room for — the
    runtime captures above show both paths). Covered by the combined regression, a density guard,
    and a sweep across lane count × curation × budget × binding-lane position. Widget test count is
    now 46.

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
Contributor 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
Contributor 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
Contributor 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
Contributor 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 27, 2026, 1:04 PM ET / 17:04 UTC.

ClawSweeper review

What this changes

This PR consolidates usage-widget layouts into shared responsive tiles, adds provider paging and reset captions, and emphasizes the quota with the least remaining allowance.

Merge readiness

⚠️ Ready for maintainer review - 3 items remain

Keep open: the implementation is technically sound and has sufficient runtime proof, but the owner has explicitly reserved the semantic choice of a dynamic binding-quota headline before merge.

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

Review scores

Measure Result What it means
Overall readiness 🐚 platinum hermit (4/6) The patch has strong focused validation and credible packaged-extension evidence; the remaining blocker is product direction rather than a code defect.
Proof confidence 🦞 diamond lobster (5/6) ✨ media proof bonus Sufficient (screenshot): Packaged-extension screenshots and the accompanying redacted proof log show after-fix WidgetKit paging, bounded overflow, and dimmed rendering with synthetic data.
Patch quality 🐚 platinum hermit (4/6) No actionable review findings were identified.

Verification

Check Result Evidence
Real behavior Verified Sufficient (screenshot): Packaged-extension screenshots and the accompanying redacted proof log show after-fix WidgetKit paging, bounded overflow, and dimmed rendering with synthetic data.
Evidence reviewed 7 items Shared tile and lane planning: The introduced shared tile obtains all provider lanes, applies compact curation only to secondary display rows, and delegates headline/overflow decisions to the planner.
Binding and overflow behavior: The planner selects the lowest remaining measured lane and reserves a compact overflow row when needed; focused tests sweep curation, capacity, and binding-lane positions.
Snapshot contract checked: The existing snapshot writer emits generic primary/secondary rows without embedded windows and appends Kimi rows; the PR's narrow slot recovery matches that contract without changing persisted snapshot format.
Findings None None.
Security None None.

Live Verification

Command: swift build --target CodexBarWidget

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

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

Assertions:

  • FAIL expect_output: Build of target: 'CodexBarWidget' complete!

How this fits together

CodexBar persists provider usage snapshots, which the WidgetKit extension reads to render compact macOS usage widgets. The tile planner selects quota lanes and the shared views format them into the widget shown to users.

flowchart LR
A[Provider usage snapshots] --> B[Widget snapshot reader]
B --> C[Quota lane planner]
D[Widget size and appearance] --> E[Shared usage tile]
C --> E
F[Provider selection] --> G[Pager header]
G --> E
E --> H[WidgetKit display]
Loading

Decision needed

Question Recommendation
Should the primary widget figure dynamically show the least-remaining quota lane, or remain pinned to the provider’s primary lane? Approve urgency-first headline: Keep the labelled least-remaining quota as the main figure so the widget foregrounds the allowance that is most constraining.

Why: Both choices are technically viable; the first surfaces the quota most likely to block work, while the second preserves stable positional meaning across refreshes.

Before merge

  • Resolve merge risk (P1) - Existing widget users may see the main figure switch between quota types as their remaining allowances cross.
  • Resolve merge risk (P1) - Disabling WidgetKit’s default margins makes the PR’s fixed tile padding responsible for layout consistency across supported macOS releases.
Agent review details

Security

None.

Review metrics

Metric Value Why it matters
Widget implementation production Swift +1,306/-526; tests +610 The substantial but concentrated WidgetKit refactor replaces duplicated layouts with a shared tile surface and focused planner tests.
Runtime coverage 2 native WidgetKit screenshot composites The supplied captures visibly cover provider paging, constrained Kimi lanes, and dimmed rendering.

Merge-risk options

Maintainer options:

  1. Approve the dynamic headline (recommended)
    Accept the labelled binding-quota display as the intended new meaning for existing usage widgets.
  2. Request a stable primary headline
    Change the contained lane-selection policy before merge if existing positional consistency is preferred.
  3. Pause the visual redesign
    Leave the current widgets unchanged if neither semantic trade-off is appropriate for this release.

Technical review

Best possible solution:

Keep the shared responsive tile architecture and land it only after the owner explicitly accepts the urgency-first headline or requests the small primary-lane variant.

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

Not applicable: this is a widget redesign rather than a report of one failing current behavior; supplied native WidgetKit captures show the proposed behavior after the change.

Is this the best way to solve the issue?

Unclear: the implementation is cohesive and validated, but the owner must choose between the more urgent dynamic headline and a stable primary-lane convention.

AGENTS.md: found and applied where relevant.

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

Labels

Label changes:

  • add merge-risk: 🚨 compatibility: The main displayed quota can change identity between refreshes for existing widget users.

Label justifications:

  • P3: This is a polished usability redesign without evidence of an urgent production failure.
  • merge-risk: 🚨 compatibility: The main displayed quota can change identity between refreshes for existing widget users.
  • 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): Packaged-extension screenshots and the accompanying redacted proof log show after-fix WidgetKit paging, bounded overflow, and dimmed rendering with synthetic data.
  • proof: sufficient: Contributor real behavior proof is sufficient. Packaged-extension screenshots and the accompanying redacted proof log show after-fix WidgetKit paging, bounded overflow, and dimmed rendering with synthetic data.
  • proof: 📸 screenshot: Contributor real behavior proof includes screenshot evidence. Packaged-extension screenshots and the accompanying redacted proof log show after-fix WidgetKit paging, bounded overflow, and dimmed rendering with synthetic data.

Evidence

What I checked:

  • Shared tile and lane planning: The introduced shared tile obtains all provider lanes, applies compact curation only to secondary display rows, and delegates headline/overflow decisions to the planner. (Sources/CodexBarWidget/WidgetTiles.swift:76, 746318cd2d8a)
  • Binding and overflow behavior: The planner selects the lowest remaining measured lane and reserves a compact overflow row when needed; focused tests sweep curation, capacity, and binding-lane positions. (Sources/CodexBarWidget/WidgetTilePlan.swift:40, 746318cd2d8a)
  • Snapshot contract checked: The existing snapshot writer emits generic primary/secondary rows without embedded windows and appends Kimi rows; the PR's narrow slot recovery matches that contract without changing persisted snapshot format. (Sources/CodexBar/UsageStore+WidgetSnapshot.swift:463, 746318cd2d8a)
  • Runtime evidence inspected: The supplied native WidgetKit images visibly show five pager positions, the Kimi 1% binding lane with bounded overflow, and legible dimmed widgets. (docs/screenshots/widget-redesign-runtime-pager.png, 746318cd2d8a)
  • Still necessary on main and release: The redesign and its overflow correction are not ancestors of fetched main or v0.55.1; the generated merge commit is only a test merge and is not treated as merged provenance. (2cdfe0734252)
  • Owner decision recorded: The repository owner’s review says the code is in good shape but holds merging because the binding-quota headline changes the at-a-glance meaning and can vary between refreshes.

Likely related people:

  • steipete: The recorded maintainer review explicitly reserves the headline semantics and widget appearance decision for the repository owner. (role: owner decision-maker; confidence: high; commits: b4694bd7edbf; files: Sources/CodexBarWidget/WidgetTilePlan.swift, Sources/CodexBarWidget/CodexBarWidgetBundle.swift)
  • Kyle Varga: Recent history on the widget snapshot writer identifies this contributor on the producer contract consumed by the new tile layer. (role: recent snapshot-writer contributor; confidence: medium; commits: c87c35becbbd; files: Sources/CodexBar/UsageStore+WidgetSnapshot.swift)
  • Yuxin Qiao: History on the Kimi provider descriptor identifies the prior compact-row behavior that the PR intentionally preserves while changing headline selection. (role: Kimi widget-behavior contributor; confidence: medium; commits: 14ee6a83e90c; files: Sources/CodexBarCore/Providers/Kimi/KimiProviderDescriptor.swift)

Rank-up moves

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

  • Obtain the owner’s explicit headline-policy decision before merge.

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 (2 earlier review cycles)
  • reviewed 2026-08-22T05:24:04.188Z sha 8f26c5a :: needs maintainer review before merge. :: none
  • reviewed 2026-08-22T05:38:34.079Z sha b94815c :: 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
Contributor 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
Contributor 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.

@steipete steipete left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Rechecked b94815ce94f5fb82bbedc5e840a4afe53dbc0a96. The existing hold on the widget appearance/headline decision remains; this review does not approve that redesign.

There is also a concrete compact-row budget edge case in WidgetTilePlan.make: overflow reservation checks remainder.count from the curated candidates, but the final overflow count includes every lane. With four lanes, three display candidates, the hero inside those candidates, maxSecondaryLanes: 2, and reservesOverflowRow: true, the remainder has two entries, so no slot is reserved. The result contains two secondary rows plus +1 more—three rows for a two-row budget. UsageTile renders all three.

Please add this combined curation/reservation regression (for example the four-lane Kimi shape with the primary lane most constrained) and reserve space based on total omitted lanes, not only the curated remainder. The existing tests exercise curation and reservation separately, so they miss this intersection.

This is a source-proven budget violation, not a reproduced claim of WidgetKit clipping. After correction and the appearance decision, current-head packaged extension/pager/timeline/tinted-margin proof is still needed; static render images and older green test reports do not establish that runtime behavior. No tests or UI probes were run in this pass.

iamenahs and others added 2 commits August 26, 2026 23:32
The overflow-row reservation asked whether the curated remainder
exceeded capacity, but the overflow count is computed against every
lane the provider reports. With four lanes, three display candidates,
the hero inside them and a two-row budget, the curated remainder fit
exactly, no slot was reserved, and the tile drew two lanes plus
"+1 more" — three rows in a two-row budget.

Reservation now derives the overflow line from all lanes and gives up
a slot only when that line would actually exceed the budget, so a
medium tile with room for lanes plus the line keeps its density.
Adds the combined curation/reservation regression, a density guard,
and a sweep across lane count, curation, budget and binding-lane
position — the intersections the focused tests missed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Real WidgetKit desktop captures from the packaged extension: pager
paging through all five seeded providers in place with wrap-around,
the four-lane Kimi shape holding its row budget on small and medium,
and the vibrant rendering keeping every mark legible. All frames use
a synthetic seeded snapshot; no live account data appears.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@clawsweeper clawsweeper Bot added the merge-risk: 🚨 compatibility 🚨 Merging this PR could break existing users, config, migrations, defaults, or upgrades. label Aug 27, 2026
@steipete

Copy link
Copy Markdown
Owner

Thanks for the follow-up. I rechecked the delta through 746318c and inspected both committed runtime contact sheets.

The row-reservation change addresses the specific source defect from my previous review: it counts omitted lanes from the full set, while retaining the extra lane when the overflow row already fits. The new combined regression, density guard and combination sweep cover that distinction. The submitted images show the small Kimi tile with Session plus +2 more, the medium tile with two rows plus +1 more, and the large tile listing all remaining lanes.

The packaged-extension log and runtime images are meaningful new evidence; the earlier “static renders only” limitation is no longer an accurate description of what you have supplied. I have inspected those artifacts, not independently rerun the build, tests or WidgetKit interactions. The recorded dim/vibrant appearance is also narrower than proof of every tinted/clear mode, and the seeded snapshot with the app stopped does not by itself establish repeated app-to-timeline updates.

The existing owner appearance/headline decision remains open, so this is acknowledgment of the correction and new proof, not approval to merge or a request to add a settings toggle. Once that decision is settled, we can verify the final integrated variant and any remaining runtime gaps without repeating the already-addressed row-budget request.

steipete added a commit that referenced this pull request Sep 7, 2026
fix: let WidgetKit own usage content margins

Remove redundant outer padding from Usage, Switcher, History, and Metric views while preserving system margins and the existing layout semantics. Extracted from #3137; the broader redesign remains separate.

Native signed WidgetKit before/after proof uses the same synthetic snapshot. Full 1,031-selection suite, make check, and independent P2 review pass. Production -9 lines; docs and changelog updated.

Co-authored-by: iamenahs <xianheng.ma@gmail.com>
@steipete

steipete commented Sep 7, 2026

Copy link
Copy Markdown
Owner

Landed the isolated redundant-padding fix in #3480, using the system-owned margin option discussed here. Usage, Switcher, History, and Metric views now keep WidgetKit content margins without adding another outer inset; the patch removes nine production lines.

The PR includes native signed WidgetKit before/after captures from the same synthetic snapshot, plus the full 1,031-selection suite and green final CI. Existing five-provider chip wrapping remains visible in the proof and is not claimed fixed. The broader appearance and binding-quota decision in this PR remains open. Thanks @iamenahs for identifying the inset problem and explaining both ownership options.

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. 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