fix(pool): isolate model quotas and compact account display

This commit is contained in:
ZheFox
2026-09-01 10:15:57 +08:00
parent 9b819169d5
commit 2fe2600021
12 changed files with 383 additions and 43 deletions
@@ -8,7 +8,7 @@
</div>
<div
v-if="items.length"
class="space-y-2"
:class="hasNumericOnlyItems ? '' : 'space-y-2'"
>
<QuotaProgressRows
:items="items"
@@ -39,7 +39,8 @@
<template v-else>
<div
v-if="items.length"
class="max-w-[208px] space-y-2"
class="w-full max-w-[208px]"
:class="hasNumericOnlyItems ? '' : 'space-y-2'"
>
<QuotaProgressRows :items="items" />
<div
@@ -64,7 +65,7 @@
</template>
<script setup lang="ts">
import { defineComponent, h, type PropType } from 'vue'
import { computed, defineComponent, h, type PropType } from 'vue'
import { useI18n } from '@/i18n'
export interface PoolQuotaProgressDisplayItem {
@@ -103,6 +104,7 @@ const emit = defineEmits<{
}>()
const { legacyT } = useI18n()
const hasNumericOnlyItems = computed(() => props.items.length > 0 && props.items.every(item => item.numericOnly))
const ResetCredits = defineComponent({
name: 'PoolQuotaResetCredits',
@@ -142,18 +144,28 @@ const QuotaProgressRows = defineComponent({
},
},
setup(props) {
return () => props.items.map((item, idx) => h('div', {
return () => h('div', {
'data-testid': 'pool-quota-rows',
class: props.items.every(item => item.numericOnly)
? 'grid grid-cols-2 gap-x-3 gap-y-1.5 min-w-0'
: 'space-y-2',
}, props.items.map((item, idx) => h('div', {
key: `${item.label}-${idx}`,
class: props.mobile
? 'flex flex-col gap-1 min-w-0'
: 'flex flex-col gap-1 min-w-[140px] max-w-[208px]',
class: item.numericOnly
? 'flex min-w-0 items-baseline justify-between gap-2 text-[10px] leading-4'
: props.mobile
? 'flex flex-col gap-1 min-w-0'
: 'flex flex-col gap-1 min-w-[140px] max-w-[208px]',
}, [
h('div', { class: 'flex items-center justify-between text-[10px] leading-none' }, [
h('div', { class: item.numericOnly ? 'contents' : 'flex items-center justify-between text-[10px] leading-none' }, [
h('span', {
'data-testid': 'pool-quota-period-label',
class: 'text-muted-foreground font-medium shrink-0',
class: item.numericOnly
? 'min-w-0 truncate text-muted-foreground'
: 'text-muted-foreground font-medium shrink-0',
title: item.numericOnly ? item.label : undefined,
}, item.label),
item.resetText
item.resetText && !item.numericOnly
? h('span', {
'data-testid': 'pool-quota-reset-text',
class: 'text-muted-foreground/80 tabular-nums truncate',
@@ -161,7 +173,7 @@ const QuotaProgressRows = defineComponent({
}, item.resetText)
: null,
]),
h('div', { class: 'flex items-center gap-1.5' }, [
h('div', { class: item.numericOnly ? 'contents' : 'flex items-center gap-1.5' }, [
item.numericOnly
? null
: h('div', {
@@ -177,12 +189,12 @@ const QuotaProgressRows = defineComponent({
'data-testid': 'pool-quota-meter-text',
class: [
'shrink-0 text-[10px] font-medium tabular-nums leading-none',
item.numericOnly ? 'ml-auto' : '',
item.numericOnly ? 'text-right' : '',
item.meterClass,
],
}, item.meterText),
]),
]))
])))
},
})
</script>
@@ -90,24 +90,41 @@ describe('pool key display panels', () => {
root.remove()
})
it('renders Antigravity quota as numeric values without progress tracks', () => {
it('renders Antigravity quota summaries in a compact numeric grid', () => {
const root = document.createElement('div')
document.body.appendChild(root)
const app = createApp(PoolKeyQuotaPanel, {
items: [{
label: 'Gemini 3.1 Pro (High)',
remainingPercent: 42,
resetText: '1h 后重置',
meterText: '42',
barClass: 'bg-amber-500',
meterClass: 'text-amber-600',
numericOnly: true,
}],
items: [
{
label: 'Gemini额度',
remainingPercent: 90.6,
resetText: '1h 后重置',
meterText: '90.6100',
barClass: 'bg-emerald-500',
meterClass: 'text-emerald-600',
numericOnly: true,
},
{
label: 'Claude额度',
remainingPercent: 100,
resetText: '1h 后重置',
meterText: '100',
barClass: 'bg-emerald-500',
meterClass: 'text-emerald-600',
numericOnly: true,
},
],
})
app.use(createI18n())
app.mount(root)
expect(root.querySelector('[data-testid="pool-quota-meter-text"]')?.textContent).toBe('42')
expect(root.querySelector('[data-testid="pool-quota-rows"]')?.className).toContain('grid-cols-2')
expect(Array.from(root.querySelectorAll('[data-testid="pool-quota-period-label"]')).map(node => node.textContent)).toEqual([
'Gemini额度',
'Claude额度',
])
expect(Array.from(root.querySelectorAll('[data-testid="pool-quota-meter-text"]')).map(node => node.textContent)).toEqual(['90.6100', '100'])
expect(root.textContent).not.toContain('1h 后重置')
expect(root.querySelector('[data-testid="pool-quota-progress-track"]')).toBeNull()
app.unmount()
@@ -0,0 +1,23 @@
import { describe, expect, it } from 'vitest'
import { summarizeAntigravityQuotaItems } from '@/features/providers/utils/antigravityQuota'
describe('summarizeAntigravityQuotaItems', () => {
it('groups model families without collapsing their independent quota values', () => {
const items = summarizeAntigravityQuotaItems([
{ model: 'claude-opus-4-6-thinking', label: 'Claude Opus', remainingPercent: 100, resetSeconds: 60 },
{ model: 'claude-sonnet-4-6', label: 'Claude Sonnet', remainingPercent: 82, resetSeconds: 120 },
{ model: 'gemini-3.1-pro-high', label: 'Gemini Pro', remainingPercent: 90.6, resetSeconds: 180 },
{ model: 'gemini-3-flash-agent', label: 'Gemini Flash', remainingPercent: 95, resetSeconds: 240 },
{ model: 'gpt-oss-120b-medium', label: 'GPT-OSS', remainingPercent: 100, resetSeconds: 300 },
{ model: 'tab_flash_lite_preview', label: 'Tab', remainingPercent: 76, resetSeconds: 360 },
{ model: 'chat_20706', label: 'Chat', remainingPercent: 64, resetSeconds: 420 },
])
expect(items.map(item => [item.label, item.remainingPercent, item.detail])).toEqual([
['Gemini额度', 90.6, '90.695'],
['Claude额度', 82, '82100'],
])
expect(items[1]?.model).toBe('claude-sonnet-4-6')
})
})
@@ -3,8 +3,17 @@ export interface AntigravityQuotaSortableItem {
label: string
remainingPercent: number
resetSeconds: number | null
detail?: string
}
const ANTIGRAVITY_QUOTA_GROUPS = [
{ label: 'Gemini额度', matches: (model: string) => model.startsWith('gemini-') },
{
label: 'Claude额度',
matches: (model: string) => model.startsWith('claude-') || model.startsWith('gpt-'),
},
] as const
const ANTIGRAVITY_MODEL_LABELS: Record<string, string> = {
'gemini-pro-agent': 'Gemini 3.1 Pro (High)',
'gemini-3.1-pro-high': 'Gemini 3.1 Pro (High)',
@@ -115,3 +124,39 @@ export function dedupeAntigravityQuotaItemsByLabel<T extends AntigravityQuotaSor
}
return Array.from(selectedByLabel.values()).sort(compareAntigravityQuotaItems)
}
export function summarizeAntigravityQuotaItems<T extends AntigravityQuotaSortableItem>(
items: T[],
): T[] {
const itemsByGroup = new Map<string, T[]>()
for (const item of items) {
const normalizedModel = item.model.trim().toLowerCase().replace(/^model:/, '')
const group = ANTIGRAVITY_QUOTA_GROUPS.find(candidate => candidate.matches(normalizedModel))
if (!group) continue
const groupedItems = itemsByGroup.get(group.label) ?? []
groupedItems.push(item)
itemsByGroup.set(group.label, groupedItems)
}
return ANTIGRAVITY_QUOTA_GROUPS.map(group => group.label)
.map((label) => {
const groupedItems = itemsByGroup.get(label)
if (!groupedItems?.length) return undefined
const remainingValues = groupedItems
.map(item => item.remainingPercent)
.sort((left, right) => left - right)
const minRemaining = remainingValues[0] ?? 0
const maxRemaining = remainingValues.at(-1) ?? minRemaining
const selected = groupedItems.find(item => item.remainingPercent === minRemaining) ?? groupedItems[0]
const detail = Math.abs(maxRemaining - minRemaining) < 1e-6
? formatAntigravityQuotaValue(minRemaining)
: `${formatAntigravityQuotaValue(minRemaining)}${formatAntigravityQuotaValue(maxRemaining)}`
return { ...selected, label, remainingPercent: minRemaining, detail }
})
.filter((item): item is T => item !== undefined)
}
function formatAntigravityQuotaValue(value: number): string {
const rounded = Math.round(value)
return Math.abs(value - rounded) < 1e-6 ? String(rounded) : value.toFixed(1)
}
+12 -11
View File
@@ -212,7 +212,7 @@
:class="getPoolKeyRowClass(key.key_id)"
>
<TableCell
class="px-4 py-3"
class="px-4 py-3 align-top"
>
<div class="flex min-w-0 items-center gap-2">
<Checkbox
@@ -324,7 +324,7 @@
</TableCell>
<TableCell
v-if="showAccountQuotaColumn"
class="py-3 align-middle"
class="py-3 align-top"
>
<PoolKeyQuotaPanel
:items="quotaProgressDisplayMap[key.key_id] || []"
@@ -338,24 +338,24 @@
@consume-reset-credit="handleConsumeCodexResetCredit(key)"
/>
</TableCell>
<TableCell class="py-3 px-2 align-middle">
<TableCell class="py-3 px-2 align-top">
<PoolKeyStatsPanel
:cycle="isPoolKeyCycleStatsDisplay(key)"
:cycle-groups="getPoolKeyCycleStatsGroups(key)"
:account-metrics="getPoolKeyAccountStatsMetrics(key)"
/>
</TableCell>
<TableCell class="py-3 text-center">
<TableCell class="py-3 text-center align-top">
<span class="text-[10px] text-muted-foreground whitespace-nowrap">
{{ keyUiStateMap[key.key_id]?.importedAtRelative || '-' }}
</span>
</TableCell>
<TableCell class="py-3 text-center">
<TableCell class="py-3 text-center align-top">
<span class="text-[10px] text-muted-foreground whitespace-nowrap">
{{ keyUiStateMap[key.key_id]?.lastUsedRelative || '-' }}
</span>
</TableCell>
<TableCell class="py-3 text-center align-middle">
<TableCell class="py-3 text-center align-top">
<div class="inline-flex items-center justify-center gap-1">
<span class="font-mono text-xs tabular-nums text-foreground/90">
{{ formatPoolScore(key.pool_score?.score) }}
@@ -416,7 +416,7 @@
</Popover>
</div>
</TableCell>
<TableCell class="py-3 text-center">
<TableCell class="py-3 text-center align-top">
<Badge
:variant="keyUiStateMap[key.key_id]?.schedulingBadgeVariant || 'default'"
class="text-[10px]"
@@ -425,7 +425,7 @@
{{ keyUiStateMap[key.key_id]?.schedulingBadgeLabel }}
</Badge>
</TableCell>
<TableCell class="py-3 px-2 align-middle">
<TableCell class="py-3 px-2 align-top">
<div class="flex justify-center gap-0.5">
<Button
v-if="key.cooldown_reason"
@@ -1146,6 +1146,7 @@ import { mergePoolKeyQuotaSnapshots } from '@/features/pool/utils/poolQuotaRefre
import {
dedupeAntigravityQuotaItemsByLabel,
resolveAntigravityQuotaLabel,
summarizeAntigravityQuotaItems,
} from '@/features/providers/utils/antigravityQuota'
import {
clearPendingCodexResetCreditIdempotencyKey,
@@ -2064,7 +2065,7 @@ const quotaProgressDisplayMap = computed<Record<string, QuotaProgressDisplayItem
remainingPercent: item.remainingPercent,
resetText: getQuotaProgressResetDisplayText(item),
meterText: item.numericOnly
? formatQuotaValue(item.remainingPercent)
? item.detail || formatQuotaValue(item.remainingPercent)
: getQuotaProgressMeterDisplayText(item),
barClass: getQuotaRemainingBarColorByRemaining(item.remainingPercent),
meterClass: getQuotaRemainingClassByRemaining(item.remainingPercent),
@@ -3866,7 +3867,7 @@ function buildQuotaProgressItemsFromSnapshot(key: PoolKeyDetail): QuotaProgressI
const windows = getQuotaSnapshotWindowsByScope(quota, 'model')
if (windows.length === 0) return []
const opaqueDisplayIndex = { value: 1 }
return dedupeAntigravityQuotaItemsByLabel(windows
return summarizeAntigravityQuotaItems(dedupeAntigravityQuotaItemsByLabel(windows
.map((window): (QuotaProgressItem & { model: string, resetSeconds: number | null }) | null => {
const remainingPercent = getQuotaWindowRemainingPercent(window)
if (remainingPercent == null) return null
@@ -3882,7 +3883,7 @@ function buildQuotaProgressItemsFromSnapshot(key: PoolKeyDetail): QuotaProgressI
allowDynamicReset: true,
}
})
.filter((item): item is QuotaProgressItem & { model: string, resetSeconds: number | null } => item != null))
.filter((item): item is QuotaProgressItem & { model: string, resetSeconds: number | null } => item != null)))
}
if (providerType === 'gemini_cli') {