diff --git a/packages/cli/src/config/footerItems.ts b/packages/cli/src/config/footerItems.ts
index 9f3943b692..612418a48f 100644
--- a/packages/cli/src/config/footerItems.ts
+++ b/packages/cli/src/config/footerItems.ts
@@ -34,8 +34,8 @@ export const ALL_ITEMS = [
},
{
id: 'quota',
- header: '/stats',
- description: 'Remaining usage on daily limit (not shown when unavailable)',
+ header: 'quota',
+ description: 'Percentage of daily limit used (not shown when unavailable)',
},
{
id: 'memory-usage',
diff --git a/packages/cli/src/ui/components/Footer.test.tsx b/packages/cli/src/ui/components/Footer.test.tsx
index bb2e0c5e4d..277a8d3b13 100644
--- a/packages/cli/src/ui/components/Footer.test.tsx
+++ b/packages/cli/src/ui/components/Footer.test.tsx
@@ -281,7 +281,7 @@ describe('', () => {
},
},
});
- expect(lastFrame()).toContain('85%');
+ expect(lastFrame()).toContain('85% used');
expect(normalizeFrame(lastFrame())).toMatchSnapshot();
unmount();
});
@@ -306,7 +306,7 @@ describe('', () => {
},
},
});
- expect(normalizeFrame(lastFrame())).not.toContain('used');
+ expect(normalizeFrame(lastFrame())).toContain('15% used');
expect(normalizeFrame(lastFrame())).toMatchSnapshot();
unmount();
});
diff --git a/packages/cli/src/ui/components/Footer.tsx b/packages/cli/src/ui/components/Footer.tsx
index 0696334577..b1fa98819f 100644
--- a/packages/cli/src/ui/components/Footer.tsx
+++ b/packages/cli/src/ui/components/Footer.tsx
@@ -351,13 +351,11 @@ export const Footer: React.FC = () => {
),
- 10, // "daily 100%" is 10 chars, but terse is "100%" (4 chars)
+ 9, // "100% used" is 9 chars
);
}
break;
diff --git a/packages/cli/src/ui/components/FooterConfigDialog.test.tsx b/packages/cli/src/ui/components/FooterConfigDialog.test.tsx
index cbcdade4ec..18b7c2a401 100644
--- a/packages/cli/src/ui/components/FooterConfigDialog.test.tsx
+++ b/packages/cli/src/ui/components/FooterConfigDialog.test.tsx
@@ -256,7 +256,7 @@ describe('', () => {
expect(nextLine).toContain('·');
expect(nextLine).toContain('~/project/path');
expect(nextLine).toContain('docker');
- expect(nextLine).toContain('97%');
+ expect(nextLine).toContain('42% used');
});
await expect(renderResult).toMatchSvgSnapshot();
diff --git a/packages/cli/src/ui/components/FooterConfigDialog.tsx b/packages/cli/src/ui/components/FooterConfigDialog.tsx
index 0c1f9ce320..e74b78de6f 100644
--- a/packages/cli/src/ui/components/FooterConfigDialog.tsx
+++ b/packages/cli/src/ui/components/FooterConfigDialog.tsx
@@ -242,7 +242,7 @@ export const FooterConfigDialog: React.FC = ({
'context-used': (
85% used
),
- quota: 97%,
+ quota: 42% used,
'memory-usage': (
260 MB
),
diff --git a/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap
index 3980ddbd0a..77fe519c64 100644
--- a/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap
@@ -1,14 +1,14 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[` > displays "Limit reached" message when remaining is 0 1`] = `
-" workspace (/directory) sandbox /model /stats
+" workspace (/directory) sandbox /model quota
~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro limit reached
"
`;
exports[` > displays the usage indicator when usage is low 1`] = `
-" workspace (/directory) sandbox /model /stats
- ~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro 85%
+" workspace (/directory) sandbox /model quota
+ ~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro 85% used
"
`;
@@ -39,7 +39,7 @@ exports[` > footer configuration filtering (golden snapshots) > render
`;
exports[` > hides the usage indicator when usage is not near limit 1`] = `
-" workspace (/directory) sandbox /model /stats
- ~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro 15%
+" workspace (/directory) sandbox /model quota
+ ~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro 15% used
"
`;
diff --git a/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-highlights-the-active-item-in-the-preview.snap.svg b/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-highlights-the-active-item-in-the-preview.snap.svg
index a83fad40c7..9bc0920398 100644
--- a/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-highlights-the-active-item-in-the-preview.snap.svg
+++ b/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-highlights-the-active-item-in-the-preview.snap.svg
@@ -50,7 +50,7 @@
quota
│
│
- Remaining usage on daily limit (not shown when unavailable)
+ Percentage of daily limit used (not shown when unavailable)
│
│
[ ]
@@ -132,10 +132,10 @@
│
│
workspace (/directory)
- branch
- sandbox
- /model
- /stats
+ branch
+ sandbox
+ /model
+ quota
diff
@@ -144,10 +144,10 @@
│
│
~/project/path
- main
- docker
- gemini-2.5-pro
- 97%
+ main
+ docker
+ gemini-2.5-pro
+ 42% used
+12
diff --git a/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-renders-correctly-with-default-settings.snap.svg b/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-renders-correctly-with-default-settings.snap.svg
index 6d8034d7f9..518c7d138d 100644
--- a/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-renders-correctly-with-default-settings.snap.svg
+++ b/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-renders-correctly-with-default-settings.snap.svg
@@ -59,7 +59,7 @@
quota
│
│
- Remaining usage on daily limit (not shown when unavailable)
+ Percentage of daily limit used (not shown when unavailable)
│
│
[ ]
@@ -133,10 +133,10 @@
│
workspace (/directory)
- branch
- sandbox
- /model
- /stats
+ branch
+ sandbox
+ /model
+ quota
│
│
│
@@ -144,10 +144,10 @@
~/project/path
- main
- docker
- gemini-2.5-pro
- 97%
+ main
+ docker
+ gemini-2.5-pro
+ 42% used
│
│
│
diff --git a/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-updates-the-preview-when-Show-footer-labels-is-toggled-off.snap.svg b/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-updates-the-preview-when-Show-footer-labels-is-toggled-off.snap.svg
index e3448b97ca..6edd77b9e2 100644
--- a/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-updates-the-preview-when-Show-footer-labels-is-toggled-off.snap.svg
+++ b/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog--FooterConfigDialog-updates-the-preview-when-Show-footer-labels-is-toggled-off.snap.svg
@@ -50,7 +50,7 @@
quota
│
│
- Remaining usage on daily limit (not shown when unavailable)
+ Percentage of daily limit used (not shown when unavailable)
│
│
[ ]
@@ -131,13 +131,13 @@
│
~/project/path
·
- main
- ·
- docker
- ·
- gemini-2.5-pro
- ·
- 97%
+ main
+ ·
+ docker
+ ·
+ gemini-2.5-pro
+ ·
+ 42% used
│
│
│
diff --git a/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog.test.tsx.snap
index 626ae93359..a703338771 100644
--- a/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/FooterConfigDialog.test.tsx.snap
@@ -16,7 +16,7 @@ exports[` > highlights the active item in the preview 1`]
│ [✓] model-name │
│ Current model identifier │
│ [✓] quota │
-│ Remaining usage on daily limit (not shown when unavailable) │
+│ Percentage of daily limit used (not shown when unavailable) │
│ [ ] context-used │
│ Percentage of context window used │
│ [ ] memory-usage │
@@ -38,8 +38,8 @@ exports[` > highlights the active item in the preview 1`]
│ │
│ ┌────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Preview: │ │
-│ │ workspace (/directory) branch sandbox /model /stats diff │ │
-│ │ ~/project/path main docker gemini-2.5-pro 97% +12 -4 │ │
+│ │ workspace (/directory) branch sandbox /model quota diff │ │
+│ │ ~/project/path main docker gemini-2.5-pro 42% used +12 -4 │ │
│ └────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
@@ -61,7 +61,7 @@ exports[` > renders correctly with default settings 1`] =
│ [✓] model-name │
│ Current model identifier │
│ [✓] quota │
-│ Remaining usage on daily limit (not shown when unavailable) │
+│ Percentage of daily limit used (not shown when unavailable) │
│ [ ] context-used │
│ Percentage of context window used │
│ [ ] memory-usage │
@@ -83,8 +83,8 @@ exports[` > renders correctly with default settings 1`] =
│ │
│ ┌────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Preview: │ │
-│ │ workspace (/directory) branch sandbox /model /stats │ │
-│ │ ~/project/path main docker gemini-2.5-pro 97% │ │
+│ │ workspace (/directory) branch sandbox /model quota │ │
+│ │ ~/project/path main docker gemini-2.5-pro 42% used │ │
│ └────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
@@ -107,7 +107,7 @@ exports[` > renders correctly with default settings 2`] =
│ [✓] model-name │
│ Current model identifier │
│ [✓] quota │
-│ Remaining usage on daily limit (not shown when unavailable) │
+│ Percentage of daily limit used (not shown when unavailable) │
│ [ ] context-used │
│ Percentage of context window used │
│ [ ] memory-usage │
@@ -129,8 +129,8 @@ exports[` > renders correctly with default settings 2`] =
│ │
│ ┌────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Preview: │ │
-│ │ workspace (/directory) branch sandbox /model /stats │ │
-│ │ ~/project/path main docker gemini-2.5-pro 97% │ │
+│ │ workspace (/directory) branch sandbox /model quota │ │
+│ │ ~/project/path main docker gemini-2.5-pro 42% used │ │
│ └────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
@@ -152,7 +152,7 @@ exports[` > updates the preview when Show footer labels is
│ [✓] model-name │
│ Current model identifier │
│ [✓] quota │
-│ Remaining usage on daily limit (not shown when unavailable) │
+│ Percentage of daily limit used (not shown when unavailable) │
│ [ ] context-used │
│ Percentage of context window used │
│ [ ] memory-usage │
@@ -174,7 +174,7 @@ exports[` > updates the preview when Show footer labels is
│ │
│ ┌────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ Preview: │ │
-│ │ ~/project/path · main · docker · gemini-2.5-pro · 97% │ │
+│ │ ~/project/path · main · docker · gemini-2.5-pro · 42% used │ │
│ └────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯"
diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts
index 3ac1aa2c34..3bc1e94f8d 100644
--- a/packages/core/src/config/config.test.ts
+++ b/packages/core/src/config/config.test.ts
@@ -3006,6 +3006,78 @@ describe('Config Quota & Preview Model Access', () => {
// Never set => stays null (unknown); getter returns true so UI shows preview
expect(config.getHasAccessToPreviewModel()).toBe(true);
});
+ it('should derive quota from remainingFraction when remainingAmount is missing', async () => {
+ mockCodeAssistServer.retrieveUserQuota.mockResolvedValue({
+ buckets: [
+ {
+ modelId: 'gemini-3-flash-preview',
+ remainingFraction: 0.96,
+ },
+ ],
+ });
+
+ config.setModel('gemini-3-flash-preview');
+ mockCoreEvents.emitQuotaChanged.mockClear();
+ await config.refreshUserQuota();
+
+ // Normalized: limit=100, remaining=96
+ expect(mockCoreEvents.emitQuotaChanged).toHaveBeenCalledWith(
+ 96,
+ 100,
+ undefined,
+ );
+ expect(config.getQuotaRemaining()).toBe(96);
+ expect(config.getQuotaLimit()).toBe(100);
+ });
+
+ it('should store quota from remainingFraction when remainingFraction is 0', async () => {
+ mockCodeAssistServer.retrieveUserQuota.mockResolvedValue({
+ buckets: [
+ {
+ modelId: 'gemini-3-pro-preview',
+ remainingFraction: 0,
+ },
+ ],
+ });
+
+ config.setModel('gemini-3-pro-preview');
+ mockCoreEvents.emitQuotaChanged.mockClear();
+ await config.refreshUserQuota();
+
+ // remaining=0, limit=100 but limit>0 check still passes
+ // however remaining=0 means 0% remaining = 100% used
+ expect(config.getQuotaRemaining()).toBe(0);
+ expect(config.getQuotaLimit()).toBe(100);
+ });
+
+ it('should emit QuotaChanged when model is switched via setModel', async () => {
+ mockCodeAssistServer.retrieveUserQuota.mockResolvedValue({
+ buckets: [
+ {
+ modelId: 'gemini-2.5-pro',
+ remainingAmount: '10',
+ remainingFraction: 0.2,
+ },
+ {
+ modelId: 'gemini-2.5-flash',
+ remainingAmount: '80',
+ remainingFraction: 0.8,
+ },
+ ],
+ });
+
+ config.setModel('auto-gemini-2.5');
+ await config.refreshUserQuota();
+ mockCoreEvents.emitQuotaChanged.mockClear();
+
+ // Switch to a specific model — should re-emit quota for that model
+ config.setModel('gemini-2.5-pro');
+ expect(mockCoreEvents.emitQuotaChanged).toHaveBeenCalledWith(
+ 10,
+ 50,
+ undefined,
+ );
+ });
});
describe('refreshUserQuotaIfStale', () => {
diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts
index 5e8507eba4..258e58ea35 100644
--- a/packages/core/src/config/config.ts
+++ b/packages/core/src/config/config.ts
@@ -832,18 +832,16 @@ export class Config implements McpContext, AgentLoopContext {
private lastEmittedQuotaLimit: number | undefined;
private emitQuotaChangedEvent(): void {
- const pooled = this.getPooledQuota();
+ const remaining = this.getQuotaRemaining();
+ const limit = this.getQuotaLimit();
+ const resetTime = this.getQuotaResetTime();
if (
- this.lastEmittedQuotaRemaining !== pooled.remaining ||
- this.lastEmittedQuotaLimit !== pooled.limit
+ this.lastEmittedQuotaRemaining !== remaining ||
+ this.lastEmittedQuotaLimit !== limit
) {
- this.lastEmittedQuotaRemaining = pooled.remaining;
- this.lastEmittedQuotaLimit = pooled.limit;
- coreEvents.emitQuotaChanged(
- pooled.remaining,
- pooled.limit,
- pooled.resetTime,
- );
+ this.lastEmittedQuotaRemaining = remaining;
+ this.lastEmittedQuotaLimit = limit;
+ coreEvents.emitQuotaChanged(remaining, limit, resetTime);
}
}
@@ -1819,6 +1817,9 @@ export class Config implements McpContext, AgentLoopContext {
// When the user explicitly sets a model, that becomes the active model.
this._activeModel = newModel;
coreEvents.emitModelChanged(newModel);
+ this.lastEmittedQuotaRemaining = undefined;
+ this.lastEmittedQuotaLimit = undefined;
+ this.emitQuotaChangedEvent();
}
if (this.onModelChange && !isTemporary) {
this.onModelChange(newModel);
@@ -2112,24 +2113,31 @@ export class Config implements McpContext, AgentLoopContext {
this.lastQuotaFetchTime = Date.now();
for (const bucket of quota.buckets) {
- if (
- bucket.modelId &&
- bucket.remainingAmount &&
- bucket.remainingFraction != null
- ) {
- const remaining = parseInt(bucket.remainingAmount, 10);
- const limit =
+ if (!bucket.modelId || bucket.remainingFraction == null) {
+ continue;
+ }
+
+ let remaining: number;
+ let limit: number;
+
+ if (bucket.remainingAmount) {
+ remaining = parseInt(bucket.remainingAmount, 10);
+ limit =
bucket.remainingFraction > 0
? Math.round(remaining / bucket.remainingFraction)
: (this.modelQuotas.get(bucket.modelId)?.limit ?? 0);
+ } else {
+ // Server only sent remainingFraction — use a normalized scale.
+ limit = 100;
+ remaining = Math.round(bucket.remainingFraction * limit);
+ }
- if (!isNaN(remaining) && Number.isFinite(limit) && limit > 0) {
- this.modelQuotas.set(bucket.modelId, {
- remaining,
- limit,
- resetTime: bucket.resetTime,
- });
- }
+ if (!isNaN(remaining) && Number.isFinite(limit) && limit > 0) {
+ this.modelQuotas.set(bucket.modelId, {
+ remaining,
+ limit,
+ resetTime: bucket.resetTime,
+ });
}
}
this.emitQuotaChangedEvent();