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 = ( -
-
- - +
+