Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion packages/i18n/src/locales/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -1027,7 +1027,9 @@
"fetch_all": "Fetch All",
"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."
"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:"
},
"claude_quota": {
"title": "Claude Quota",
Expand Down
4 changes: 3 additions & 1 deletion packages/i18n/src/locales/ru.json
Original file line number Diff line number Diff line change
Expand Up @@ -898,7 +898,9 @@
"fetch_all": "Получить все",
"window_weekly": "Неделя",
"window_5h": "5 часов",
"group_hint": "Модели этой группы делят один недельный и один пятичасовой лимит. Квота расходуется пропорционально стоимости токенов, поэтому короткие задачи и более экономичные модели растягивают её дольше."
"group_hint": "Модели этой группы делят один недельный и один пятичасовой лимит. Квота расходуется пропорционально стоимости токенов, поэтому короткие задачи и более экономичные модели растягивают её дольше.",
"shared_group": "Общая для {{count}} моделей",
"shared_group_members": "Модели с общей квотой:"
},
"claude_quota": {
"title": "Квота Claude",
Expand Down
4 changes: 3 additions & 1 deletion packages/i18n/src/locales/zh-CN.json
Original file line number Diff line number Diff line change
Expand Up @@ -1035,7 +1035,9 @@
"fetch_all": "获取全部",
"window_weekly": "周",
"window_5h": "5 小时",
"group_hint": "本组共享一个周限额和一个 5 小时限额。额度按 token 成本比例扣减,因此任务越短、模型越经济,额度越耐用。"
"group_hint": "本组共享一个周限额和一个 5 小时限额。额度按 token 成本比例扣减,因此任务越短、模型越经济,额度越耐用。",
"shared_group": "{{count}} 个模型共享",
"shared_group_members": "共享此额度的模型:"
},
"claude_quota": {
"title": "Claude 额度",
Expand Down
48 changes: 48 additions & 0 deletions pages/auth-files/__tests__/quotaCardSlots.antigravity.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,51 @@ describe("antigravity quota card slots", () => {
expect(thirdParty.hint).toBe("antigravity_quota.group_hint");
});
});


// 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[] = [
{
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: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",
},
];

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",
]);
});

// 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();
});
});
38 changes: 36 additions & 2 deletions pages/auth-files/hooks/quotaCardSlots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,12 @@ const orderAntigravityWindows = (
}> => {
const groups = new Map<string, QuotaItem[]>();
items.forEach((item) => {
const groupKey = String(item.label ?? item.key ?? "");
// Shared buckets all carry the same label key, so they group by their own
// key instead — they are distinct quotas that merely render alike.
const groupKey =
item.label === SHARED_GROUP_LABEL
? String(item.key ?? "")
: String(item.label ?? item.key ?? "");
const bucket = groups.get(groupKey);
if (bucket) bucket.push(item);
else groups.set(groupKey, [item]);
Expand All @@ -80,6 +85,19 @@ const orderAntigravityWindows = (
});
};

const SHARED_GROUP_LABEL = "antigravity_quota.shared_group";

/**
* Members of a shared bucket, as the backend sends them: a comma-separated list
* of model ids. The fallback view groups models by the quota they draw on, so
* this list is the answer to "what does this bar cover".
*/
const readSharedGroupMembers = (item: QuotaItem): string[] =>
String(item.meta ?? "")
.split(",")
.map((entry) => entry.trim())
.filter(Boolean);

/**
* Compose the row label as "<group> · <window>", or just "<group>" when the
* group has nothing to disambiguate against.
Expand All @@ -90,6 +108,12 @@ const orderAntigravityWindows = (
* string, which was too long for the row and could not be localised at all.
*/
const buildAntigravityRowLabel = (item: QuotaItem, showWindow: boolean, t: TFunction): string => {
// A bucket from the fallback view has no name of its own — it is defined by
// the models that share it, so the row says how many that is. Naming it after
// a model family would be the guess this grouping exists to avoid.
if (item.label === SHARED_GROUP_LABEL) {
return t(SHARED_GROUP_LABEL, { count: readSharedGroupMembers(item).length });
}
const group = shortenAntigravityGroupName(String(item.label ?? ""));
if (!showWindow) return group;
const windowLabel =
Expand Down Expand Up @@ -145,7 +169,17 @@ export const resolveQuotaCardSlots = (
// slot keeps it out of the row's detail line, which is reserved for the
// reset countdown, and hands it to the hint icon instead.
const { meta, ...itemWithoutMeta } = item;
const hint = [meta, t("antigravity_quota.group_hint")]
// A shared bucket's meta is the member list; spell it out under a
// heading rather than dropping a bare comma-separated string on the
// reader. Named groups keep the upstream's own description.
const members = readSharedGroupMembers(item);
const description =
item.label === SHARED_GROUP_LABEL
? members.length > 0
? `${t("antigravity_quota.shared_group_members")}\n${members.join("\n")}`
: ""
: (meta ?? "");
const hint = [description, t("antigravity_quota.group_hint")]
.map((part) => (typeof part === "string" ? part.trim() : ""))
.filter(Boolean)
.join("\n\n");
Expand Down
Loading