From 158f5d5cef3244d8264ebce05187c9d74837f2b1 Mon Sep 17 00:00:00 2001 From: kittors Date: Fri, 21 Aug 2026 00:11:38 +0800 Subject: [PATCH] feat(ai-accounts): compact single-line quota rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The card gave each quota two lines — a label row and a separate track below it — so five quotas filled the card and the numbers sat far from the names they belonged to. Reading down a column meant hopping between rows. Each quota is now one line: the fill is the row's own background rather than a track underneath, and the label, countdown and percentage share it. Same information, half the height, and the numbers line up in a column you can scan. The countdown moves inline from the detail row it used to occupy, which is what freed the second line. Antigravity rows carry the family name straight from the backend again. That view now measures each family from one representative model, so the hint says which model the number came from — an aggregate had no honest answer to that question, since it mixed models on different reset cycles. The previous "N 个模型共享" labelling is gone. It described the data correctly and told the reader nothing: a bucket count is an internal detail, not something to hand to someone checking whether they have quota left. Co-Authored-By: Claude Opus 5 --- packages/i18n/src/locales/en.json | 3 +- packages/i18n/src/locales/ru.json | 3 +- packages/i18n/src/locales/zh-CN.json | 3 +- .../quotaCardSlots.antigravity.test.ts | 64 +++++----- pages/auth-files/hooks/quotaBar.tsx | 74 ++++++------ pages/auth-files/hooks/quotaCardSlots.ts | 109 +++++------------- 6 files changed, 96 insertions(+), 160 deletions(-) diff --git a/packages/i18n/src/locales/en.json b/packages/i18n/src/locales/en.json index b99ffc86..fcb500c7 100644 --- a/packages/i18n/src/locales/en.json +++ b/packages/i18n/src/locales/en.json @@ -1028,8 +1028,7 @@ "window_weekly": "Weekly", "window_5h": "5-hour", "group_hint": "Models in this group share one weekly limit and one 5-hour limit. Quota is consumed in proportion to token cost, so shorter tasks and cheaper models make it last longer.", - "shared_group": "Shared by {{count}} models", - "shared_group_members": "Models sharing this quota:" + "measured_from": "Measured from {{model}}" }, "claude_quota": { "title": "Claude Quota", diff --git a/packages/i18n/src/locales/ru.json b/packages/i18n/src/locales/ru.json index d48debff..3fd2e55e 100644 --- a/packages/i18n/src/locales/ru.json +++ b/packages/i18n/src/locales/ru.json @@ -899,8 +899,7 @@ "window_weekly": "Неделя", "window_5h": "5 часов", "group_hint": "Модели этой группы делят один недельный и один пятичасовой лимит. Квота расходуется пропорционально стоимости токенов, поэтому короткие задачи и более экономичные модели растягивают её дольше.", - "shared_group": "Общая для {{count}} моделей", - "shared_group_members": "Модели с общей квотой:" + "measured_from": "Показатель модели {{model}}" }, "claude_quota": { "title": "Квота Claude", diff --git a/packages/i18n/src/locales/zh-CN.json b/packages/i18n/src/locales/zh-CN.json index 4b6f5b4e..53782c56 100644 --- a/packages/i18n/src/locales/zh-CN.json +++ b/packages/i18n/src/locales/zh-CN.json @@ -1036,8 +1036,7 @@ "window_weekly": "周", "window_5h": "5 小时", "group_hint": "本组共享一个周限额和一个 5 小时限额。额度按 token 成本比例扣减,因此任务越短、模型越经济,额度越耐用。", - "shared_group": "{{count}} 个模型共享", - "shared_group_members": "共享此额度的模型:" + "measured_from": "本行额度取自 {{model}}" }, "claude_quota": { "title": "Claude 额度", diff --git a/pages/auth-files/__tests__/quotaCardSlots.antigravity.test.ts b/pages/auth-files/__tests__/quotaCardSlots.antigravity.test.ts index cdf65ad2..e0e011ae 100644 --- a/pages/auth-files/__tests__/quotaCardSlots.antigravity.test.ts +++ b/pages/auth-files/__tests__/quotaCardSlots.antigravity.test.ts @@ -70,7 +70,12 @@ describe("antigravity quota card slots", () => { const slots = resolveQuotaCardSlots( "antigravity", [ - { key: "antigravity:gemini_pro", label: "Gemini Pro", percent: 40, windowSeconds: FIVE_HOUR }, + { + key: "antigravity:gemini_pro", + label: "Gemini Pro", + percent: 40, + windowSeconds: FIVE_HOUR, + }, { key: "antigravity:claude", label: "Claude", percent: 60, windowSeconds: FIVE_HOUR }, ], t, @@ -98,50 +103,41 @@ describe("antigravity quota card slots", () => { }); }); - // The fallback view (fetchAvailableModels) groups models by the quota they // actually share, which the backend sends as a member list. The row has no // family name to fall back on — and inventing one is exactly what this // grouping exists to avoid — so it says how many models draw on the bucket. -describe("antigravity shared buckets (fallback view)", () => { - const sharedItems: QuotaItem[] = [ +describe("antigravity fallback view", () => { + // The fallback view sends one row per family, already measured from a single + // representative model. The card shows the family name as-is; meta names the + // model behind the number and belongs behind the icon. + const familyItems: QuotaItem[] = [ { - key: "antigravity:group_gemini_2_5_flash", - label: "antigravity_quota.shared_group", - percent: 100, - meta: "gemini-2.5-flash,gemini-2.5-pro", + key: "antigravity:gemini_pro", + label: "Gemini Pro", + percent: 80, + windowSeconds: FIVE_HOUR, + meta: "gemini-3.1-pro-high", }, { - key: "antigravity:group_claude_sonnet_4_6", - label: "antigravity_quota.shared_group", - percent: 100, - meta: "claude-sonnet-4-6,gemini-3.1-pro-high,gpt-oss-120b-medium", + key: "antigravity:claude", + label: "Claude", + percent: 90, + windowSeconds: FIVE_HOUR, + meta: "claude-sonnet-4-6", }, ]; - test("names each row by how many models share the bucket", () => { - const slots = resolveQuotaCardSlots("antigravity", sharedItems, t); - expect(slots.map((slot) => slot.label)).toEqual([ - "antigravity_quota.shared_group", - "antigravity_quota.shared_group", - ]); - // Distinct buckets must stay distinct rows even though they render alike. - expect(slots.map((slot) => slot.id)).toEqual([ - "antigravity:group_gemini_2_5_flash", - "antigravity:group_claude_sonnet_4_6", - ]); + test("keeps the family name and adds no window suffix", () => { + const slots = resolveQuotaCardSlots("antigravity", familyItems, t); + expect(slots.map((slot) => slot.label)).toEqual(["Gemini Pro", "Claude"]); }); - // The member list answers "what does this bar cover" and belongs behind the - // icon; a bare comma-separated string in the row would be unreadable. - test("lists the sharing models in the hint, one per line", () => { - const slots = resolveQuotaCardSlots("antigravity", sharedItems, t); - expect(slots[1].hint).toContain("claude-sonnet-4-6"); - expect(slots[1].hint).toContain("gpt-oss-120b-medium"); - expect(slots[1].hint).toContain("antigravity_quota.shared_group_members"); - expect(slots[1].hint).toContain("antigravity_quota.group_hint"); - // The raw list must not survive on the item, where it would reach the - // detail line reserved for the reset countdown. - expect(slots[1].item?.meta).toBeUndefined(); + test("explains which model the row was measured from, off the item", () => { + const slots = resolveQuotaCardSlots("antigravity", familyItems, t); + expect(slots[0].hint).toContain("antigravity_quota.measured_from"); + expect(slots[0].hint).toContain("antigravity_quota.group_hint"); + // meta must not survive on the item, where it would reach the countdown. + expect(slots[0].item?.meta).toBeUndefined(); }); }); diff --git a/pages/auth-files/hooks/quotaBar.tsx b/pages/auth-files/hooks/quotaBar.tsx index c9d9baf1..0db25fd1 100644 --- a/pages/auth-files/hooks/quotaBar.tsx +++ b/pages/auth-files/hooks/quotaBar.tsx @@ -39,20 +39,32 @@ export const renderQuotaBarNode = ( const detailText = formatQuotaItemDetailText(item); const tooltipParts = [translatedLabel, percentText]; if (detailText) tooltipParts.push(detailText); + // One line per quota: the fill is the row's background rather than a separate + // track underneath it, so a card fits twice as many windows at the same + // height. Label, countdown and percentage share the line, which is what makes + // the numbers scannable down a column instead of hunting between two rows. const bar = ( -
-
- - +
+