diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a358ad8b07..973d88f5f8 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -169,7 +169,7 @@ jobs:
npm run test:ci --workspace @google/gemini-cli
else
# Explicitly list non-cli packages to ensure they are sharded correctly
- npm run test:ci --workspace @google/gemini-cli-core --workspace @google/gemini-cli-a2a-server --workspace gemini-cli-vscode-ide-companion --workspace @google/gemini-cli-test-utils --if-present
+ npm run test:ci --workspace @google/gemini-cli-core --workspace @google/gemini-cli-a2a-server --workspace gemini-cli-vscode-ide-companion --workspace @google/gemini-cli-test-utils --if-present -- --coverage.enabled=false
npm run test:scripts
fi
diff --git a/packages/cli/src/config/policy.test.ts b/packages/cli/src/config/policy.test.ts
index 9baccd3359..8d368bfb91 100644
--- a/packages/cli/src/config/policy.test.ts
+++ b/packages/cli/src/config/policy.test.ts
@@ -183,7 +183,6 @@ describe('resolveWorkspacePolicyState', () => {
setAutoAcceptWorkspacePolicies(originalValue);
}
});
-
it('should not return workspace policies if cwd is the home directory', async () => {
const policiesDir = path.join(tempDir, '.gemini', 'policies');
fs.mkdirSync(policiesDir, { recursive: true });
diff --git a/packages/cli/src/ui/components/Footer.test.tsx b/packages/cli/src/ui/components/Footer.test.tsx
index b79b005d85..f2b8193865 100644
--- a/packages/cli/src/ui/components/Footer.test.tsx
+++ b/packages/cli/src/ui/components/Footer.test.tsx
@@ -235,8 +235,8 @@ describe('', () => {
},
);
await waitUntilReady();
- expect(lastFrame()).toContain('15%');
- expect(normalizeFrame(lastFrame())).toMatchSnapshot();
+ expect(lastFrame()).toContain('85%');
+ expect(lastFrame()).toMatchSnapshot();
unmount();
});
diff --git a/packages/cli/src/ui/components/QuotaDisplay.test.tsx b/packages/cli/src/ui/components/QuotaDisplay.test.tsx
index 150eb7097c..5a8b8c5bf8 100644
--- a/packages/cli/src/ui/components/QuotaDisplay.test.tsx
+++ b/packages/cli/src/ui/components/QuotaDisplay.test.tsx
@@ -5,10 +5,20 @@
*/
import { render } from '../../test-utils/render.js';
-import { describe, it, expect } from 'vitest';
+import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import { QuotaDisplay } from './QuotaDisplay.js';
describe('QuotaDisplay', () => {
+ beforeEach(() => {
+ vi.stubEnv('TZ', 'America/Los_Angeles');
+ vi.useFakeTimers();
+ vi.setSystemTime(new Date('2026-03-02T20:29:00.000Z'));
+ });
+
+ afterEach(() => {
+ vi.useRealTimers();
+ vi.unstubAllEnvs();
+ });
it('should not render when remaining is undefined', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
,
@@ -36,7 +46,7 @@ describe('QuotaDisplay', () => {
unmount();
});
- it('should not render when usage > 20%', async () => {
+ it('should not render when usage < 80%', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
,
);
@@ -45,7 +55,7 @@ describe('QuotaDisplay', () => {
unmount();
});
- it('should render yellow when usage < 20%', async () => {
+ it('should render warning when used >= 80%', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
,
);
@@ -54,7 +64,7 @@ describe('QuotaDisplay', () => {
unmount();
});
- it('should render red when usage < 5%', async () => {
+ it('should render critical when used >= 95%', async () => {
const { lastFrame, waitUntilReady, unmount } = render(
,
);
diff --git a/packages/cli/src/ui/components/QuotaDisplay.tsx b/packages/cli/src/ui/components/QuotaDisplay.tsx
index 881f4c77fd..f62d321972 100644
--- a/packages/cli/src/ui/components/QuotaDisplay.tsx
+++ b/packages/cli/src/ui/components/QuotaDisplay.tsx
@@ -7,9 +7,9 @@
import type React from 'react';
import { Text } from 'ink';
import {
- getStatusColor,
- QUOTA_THRESHOLD_HIGH,
- QUOTA_THRESHOLD_MEDIUM,
+ getUsedStatusColor,
+ QUOTA_USED_WARNING_THRESHOLD,
+ QUOTA_USED_CRITICAL_THRESHOLD,
} from '../utils/displayUtils.js';
import { formatResetTime } from '../utils/formatters.js';
@@ -34,32 +34,34 @@ export const QuotaDisplay: React.FC = ({
return null;
}
- const percentage = (remaining / limit) * 100;
+ const usedPercentage = 100 - (remaining / limit) * 100;
- if (!forceShow && percentage > QUOTA_THRESHOLD_HIGH) {
+ if (!forceShow && usedPercentage < QUOTA_USED_WARNING_THRESHOLD) {
return null;
}
- const color = getStatusColor(percentage, {
- green: QUOTA_THRESHOLD_HIGH,
- yellow: QUOTA_THRESHOLD_MEDIUM,
+ const color = getUsedStatusColor(usedPercentage, {
+ warning: QUOTA_USED_WARNING_THRESHOLD,
+ critical: QUOTA_USED_CRITICAL_THRESHOLD,
});
- const resetInfo =
- !terse && resetTime ? `, ${formatResetTime(resetTime)}` : '';
-
+ let text: string;
if (remaining === 0) {
- let text = terse
- ? 'Limit reached'
- : `/stats Limit reached${resetInfo}${!terse && '. /auth to continue.'}`;
- if (lowercase) text = text.toLowerCase();
- return {text};
+ const resetMsg = resetTime
+ ? `, resets in ${formatResetTime(resetTime, true)}`
+ : '';
+ text = terse ? 'Limit reached' : `Limit reached${resetMsg}`;
+ } else {
+ text = terse
+ ? `${usedPercentage.toFixed(0)}%`
+ : `${usedPercentage.toFixed(0)}% used${
+ resetTime ? ` (Limit resets in ${formatResetTime(resetTime)})` : ''
+ }`;
}
- let text = terse
- ? `${percentage.toFixed(0)}%`
- : `/stats ${percentage.toFixed(0)}% usage remaining${resetInfo}`;
- if (lowercase) text = text.toLowerCase();
+ if (lowercase) {
+ text = text.toLowerCase();
+ }
return {text};
};
diff --git a/packages/cli/src/ui/components/QuotaStatsInfo.tsx b/packages/cli/src/ui/components/QuotaStatsInfo.tsx
index 22325db147..8028500233 100644
--- a/packages/cli/src/ui/components/QuotaStatsInfo.tsx
+++ b/packages/cli/src/ui/components/QuotaStatsInfo.tsx
@@ -9,9 +9,9 @@ import { Box, Text } from 'ink';
import { theme } from '../semantic-colors.js';
import { formatResetTime } from '../utils/formatters.js';
import {
- getStatusColor,
- QUOTA_THRESHOLD_HIGH,
- QUOTA_THRESHOLD_MEDIUM,
+ getUsedStatusColor,
+ QUOTA_USED_WARNING_THRESHOLD,
+ QUOTA_USED_CRITICAL_THRESHOLD,
} from '../utils/displayUtils.js';
interface QuotaStatsInfoProps {
@@ -31,19 +31,24 @@ export const QuotaStatsInfo: React.FC = ({
return null;
}
- const percentage = (remaining / limit) * 100;
- const color = getStatusColor(percentage, {
- green: QUOTA_THRESHOLD_HIGH,
- yellow: QUOTA_THRESHOLD_MEDIUM,
+ const usedPercentage = 100 - (remaining / limit) * 100;
+ const color = getUsedStatusColor(usedPercentage, {
+ warning: QUOTA_USED_WARNING_THRESHOLD,
+ critical: QUOTA_USED_CRITICAL_THRESHOLD,
});
return (
{remaining === 0
- ? `Limit reached`
- : `${percentage.toFixed(0)}% usage remaining`}
- {resetTime && `, ${formatResetTime(resetTime)}`}
+ ? `Limit reached${
+ resetTime ? `, resets in ${formatResetTime(resetTime, true)}` : ''
+ }`
+ : `${usedPercentage.toFixed(0)}% used${
+ resetTime
+ ? ` (Limit resets in ${formatResetTime(resetTime)})`
+ : ''
+ }`}
{showDetails && (
<>
diff --git a/packages/cli/src/ui/components/StatsDisplay.test.tsx b/packages/cli/src/ui/components/StatsDisplay.test.tsx
index af7e1b884d..6f7341965b 100644
--- a/packages/cli/src/ui/components/StatsDisplay.test.tsx
+++ b/packages/cli/src/ui/components/StatsDisplay.test.tsx
@@ -465,9 +465,9 @@ describe('', () => {
await waitUntilReady();
const output = lastFrame();
- expect(output).toContain('Usage remaining');
- expect(output).toContain('75.0%');
- expect(output).toContain('resets in 1h 30m');
+ expect(output).toContain('Model usage');
+ expect(output).toContain('25% used');
+ expect(output).toContain('Limit resets in');
expect(output).toMatchSnapshot();
vi.useRealTimers();
@@ -521,8 +521,8 @@ describe('', () => {
await waitUntilReady();
const output = lastFrame();
- // (10 + 700) / (100 + 1000) = 710 / 1100 = 64.5%
- expect(output).toContain('65% usage remaining');
+ // (1 - 710/1100) * 100 = 35.5%
+ expect(output).toContain('35% used');
expect(output).toContain('Usage limit: 1,100');
expect(output).toMatchSnapshot();
@@ -571,8 +571,8 @@ describe('', () => {
expect(output).toContain('gemini-2.5-flash');
expect(output).toContain('-'); // for requests
- expect(output).toContain('50.0%');
- expect(output).toContain('resets in 2h');
+ expect(output).toContain('50% used');
+ expect(output).toContain('Limit resets in');
expect(output).toMatchSnapshot();
vi.useRealTimers();
diff --git a/packages/cli/src/ui/components/StatsDisplay.tsx b/packages/cli/src/ui/components/StatsDisplay.tsx
index 65169f6d74..d9412eb88d 100644
--- a/packages/cli/src/ui/components/StatsDisplay.tsx
+++ b/packages/cli/src/ui/components/StatsDisplay.tsx
@@ -19,6 +19,9 @@ import {
USER_AGREEMENT_RATE_MEDIUM,
CACHE_EFFICIENCY_HIGH,
CACHE_EFFICIENCY_MEDIUM,
+ getUsedStatusColor,
+ QUOTA_USED_WARNING_THRESHOLD,
+ QUOTA_USED_CRITICAL_THRESHOLD,
} from '../utils/displayUtils.js';
import { computeSessionStats } from '../utils/computeStats.js';
import {
@@ -168,7 +171,26 @@ const ModelUsageTable: React.FC<{
const uncachedWidth = 15;
const cachedWidth = 14;
const outputTokensWidth = 15;
- const usageLimitWidth = showQuotaColumn ? 28 : 0;
+ const usageLimitWidth = showQuotaColumn ? 85 : 0;
+
+ const renderProgressBar = (usedFraction: number, color: string) => {
+ const totalSteps = 20;
+ let filledSteps = Math.round(usedFraction * totalSteps);
+
+ // If something is used (fraction > 0) but rounds to 0, show 1 tick.
+ // If < 100% (fraction < 1) but rounds to 20, show 19 ticks.
+ if (usedFraction > 0 && usedFraction < 1) {
+ filledSteps = Math.min(Math.max(filledSteps, 1), totalSteps - 1);
+ }
+
+ const emptySteps = totalSteps - filledSteps;
+ return (
+
+ {'▬'.repeat(filledSteps)}
+ {'▬'.repeat(emptySteps)}
+
+ );
+ };
const cacheEfficiencyColor = getStatusColor(cacheEfficiency, {
green: CACHE_EFFICIENCY_HIGH,
@@ -183,19 +205,21 @@ const ModelUsageTable: React.FC<{
: uncachedWidth + cachedWidth + outputTokensWidth);
const isAuto = currentModel && isAutoModel(currentModel);
- const modelUsageTitle = isAuto
- ? `${getDisplayString(currentModel)} Usage`
- : `Model Usage`;
+ const modelUsageTitle = isAuto ? (
+
+ Model Usage: {getDisplayString(currentModel)}
+
+ ) : (
+
+ Model Usage
+
+ );
return (
{/* Header */}
-
-
- {modelUsageTitle}
-
-
+ {modelUsageTitle}
{isAuto &&
@@ -270,10 +294,11 @@ const ModelUsageTable: React.FC<{
- Usage remaining
+ Model usage
)}
@@ -355,16 +380,62 @@ const ModelUsageTable: React.FC<{
- {row.bucket &&
- row.bucket.remainingFraction != null &&
- row.bucket.resetTime && (
-
- {(row.bucket.remainingFraction * 100).toFixed(1)}%{' '}
- {formatResetTime(row.bucket.resetTime)}
-
- )}
+ {row.bucket && row.bucket.remainingFraction != null && (
+
+ {(() => {
+ const actualUsedFraction = 1 - row.bucket.remainingFraction;
+ // If we have session activity but 0% server usage, show 0.1% as a hint.
+ const effectiveUsedFraction =
+ actualUsedFraction === 0 && row.isActive
+ ? 0.001
+ : actualUsedFraction;
+
+ const usedPercentage = effectiveUsedFraction * 100;
+
+ const statusColor =
+ getUsedStatusColor(usedPercentage, {
+ warning: QUOTA_USED_WARNING_THRESHOLD,
+ critical: QUOTA_USED_CRITICAL_THRESHOLD,
+ }) ??
+ (row.isActive ? theme.text.primary : theme.ui.comment);
+
+ const percentageText =
+ usedPercentage > 0 && usedPercentage < 1
+ ? `${usedPercentage.toFixed(1)}% used`
+ : `${usedPercentage.toFixed(0)}% used`;
+
+ return (
+ <>
+ {renderProgressBar(effectiveUsedFraction, statusColor)}
+
+
+ {row.bucket.remainingFraction === 0 ? (
+
+ Limit reached
+ {row.bucket.resetTime &&
+ `, resets in ${formatResetTime(row.bucket.resetTime, true)}`}
+
+ ) : (
+ <>
+ {percentageText}
+
+ {row.bucket.resetTime &&
+ formatResetTime(row.bucket.resetTime)
+ ? ` (Limit resets in ${formatResetTime(row.bucket.resetTime)})`
+ : ''}
+
+ >
+ )}
+
+
+ >
+ );
+ })()}
+
+ )}
))}
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 2d7b413787..f2892488c3 100644
--- a/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/Footer.test.tsx.snap
@@ -8,7 +8,7 @@ exports[` > displays "Limit reached" message when remaining is 0 1`] =
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 15%
+ ~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro 85%
"
`;
@@ -40,6 +40,6 @@ 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 85%
+ ~/project/foo/bar/and/some/more/directories/to/make/it/long no sandbox gemini-pro 15%
"
`;
diff --git a/packages/cli/src/ui/components/__snapshots__/QuotaDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/QuotaDisplay.test.tsx.snap
index b631f4e8ad..3f5af99dd9 100644
--- a/packages/cli/src/ui/components/__snapshots__/QuotaDisplay.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/QuotaDisplay.test.tsx.snap
@@ -1,12 +1,12 @@
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
exports[`QuotaDisplay > should NOT render reset time when terse is true 1`] = `
-"15%
+"85%
"
`;
-exports[`QuotaDisplay > should render red when usage < 5% 1`] = `
-"/stats 4% usage remaining
+exports[`QuotaDisplay > should render critical when used >= 95% 1`] = `
+"96% used
"
`;
@@ -15,12 +15,12 @@ exports[`QuotaDisplay > should render terse limit reached message 1`] = `
"
`;
-exports[`QuotaDisplay > should render with reset time when provided 1`] = `
-"/stats 15% usage remaining, resets in 1h
+exports[`QuotaDisplay > should render warning when used >= 80% 1`] = `
+"85% used
"
`;
-exports[`QuotaDisplay > should render yellow when usage < 20% 1`] = `
-"/stats 15% usage remaining
+exports[`QuotaDisplay > should render with reset time when provided 1`] = `
+"85% used (Limit resets in 1 hour at 1:29 PM PST)
"
`;
diff --git a/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap
index cc31c301ba..ec70cdaecd 100644
--- a/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap
+++ b/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap
@@ -162,16 +162,16 @@ exports[` > Quota Display > renders pooled quota information for
│ » API Time: 0s (0.0%) │
│ » Tool Time: 0s (0.0%) │
│ │
-│ auto Usage │
-│ 65% usage remaining │
+│ Model Usage: auto │
+│ 35% used │
│ Usage limit: 1,100 │
│ Usage limits span all sessions and reset daily. │
│ For a full token breakdown, run \`/stats model\`. │
│ │
-│ Model Reqs Usage remaining │
-│ ──────────────────────────────────────────────────────────── │
-│ gemini-2.5-pro - │
-│ gemini-2.5-flash - │
+│ Model Reqs Model usage │
+│ ────────────────────────────────────────────────────────────────────────────────────────────────│
+│ gemini-2.5-pro - ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 90% used │
+│ gemini-2.5-flash - ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 30% used │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
@@ -194,9 +194,9 @@ exports[` > Quota Display > renders quota information for unused
│ » Tool Time: 0s (0.0%) │
│ │
│ Model Usage │
-│ Model Reqs Usage remaining │
-│ ──────────────────────────────────────────────────────────── │
-│ gemini-2.5-flash - 50.0% resets in 2h │
+│ Model Reqs Model usage │
+│ ────────────────────────────────────────────────────────────────────────────────────────────────│
+│ gemini-2.5-flash - ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 50% used (Limit resets in 2 hours at 6:00 AM│
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
@@ -219,9 +219,9 @@ exports[` > Quota Display > renders quota information when quota
│ » Tool Time: 0s (0.0%) │
│ │
│ Model Usage │
-│ Model Reqs Usage remaining │
-│ ──────────────────────────────────────────────────────────── │
-│ gemini-2.5-pro 1 75.0% resets in 1h 30m │
+│ Model Reqs Model usage │
+│ ────────────────────────────────────────────────────────────────────────────────────────────────│
+│ gemini-2.5-pro 1 ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 25% used (Limit resets in 1 hour 30 minutes │
│ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
"
diff --git a/packages/cli/src/ui/components/messages/ToolMessage.tsx b/packages/cli/src/ui/components/messages/ToolMessage.tsx
index 6b9184b0b4..15bfc9e1ed 100644
--- a/packages/cli/src/ui/components/messages/ToolMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolMessage.tsx
@@ -60,6 +60,7 @@ export const ToolMessage: React.FC = ({
originalRequestName,
progress,
progressTotal,
+ progressPercent,
}) => {
const isThisShellFocused = checkIsShellFocused(
name,
@@ -98,6 +99,8 @@ export const ToolMessage: React.FC = ({
status={status}
description={description}
emphasis={emphasis}
+ progressMessage={progressMessage}
+ progressPercent={progressPercent}
originalRequestName={originalRequestName}
/>
= ({
description,
status: coreStatus,
emphasis,
+ progressMessage: _progressMessage,
+ progressPercent: _progressPercent,
originalRequestName,
}) => {
const status = mapCoreStatusToDisplayStatus(coreStatus);
diff --git a/packages/cli/src/ui/hooks/toolMapping.test.ts b/packages/cli/src/ui/hooks/toolMapping.test.ts
index 3b4e942357..e949a8575c 100644
--- a/packages/cli/src/ui/hooks/toolMapping.test.ts
+++ b/packages/cli/src/ui/hooks/toolMapping.test.ts
@@ -325,7 +325,6 @@ describe('toolMapping', () => {
const result = mapToDisplay(toolCall);
expect(result.tools[0].originalRequestName).toBe('original_tool');
});
-
it('propagates isClientInitiated from tool request', () => {
const clientInitiatedTool: ScheduledToolCall = {
status: CoreToolCallStatus.Scheduled,
diff --git a/packages/cli/src/ui/hooks/useToolScheduler.ts b/packages/cli/src/ui/hooks/useToolScheduler.ts
index 496143e590..88e75464fb 100644
--- a/packages/cli/src/ui/hooks/useToolScheduler.ts
+++ b/packages/cli/src/ui/hooks/useToolScheduler.ts
@@ -117,6 +117,20 @@ export function useToolScheduler(
const handler = (event: ToolCallsUpdateMessage) => {
const isRoot = event.schedulerId === ROOT_SCHEDULER_ID;
+ // Update output timer for UI spinners (Side Effect)
+ const hasExecuting = event.toolCalls.some(
+ (tc) =>
+ tc.status === CoreToolCallStatus.Executing ||
+ ((tc.status === CoreToolCallStatus.Success ||
+ tc.status === CoreToolCallStatus.Error) &&
+ 'tailToolCallRequest' in tc &&
+ tc.tailToolCallRequest != null),
+ );
+
+ if (hasExecuting) {
+ setLastToolOutputTime(Date.now());
+ }
+
setToolCallsMap((prev) => {
const prevCalls = prev[event.schedulerId] ?? [];
const prevCallIds = new Set(prevCalls.map((tc) => tc.request.callId));
@@ -151,20 +165,6 @@ export function useToolScheduler(
[event.schedulerId]: adapted,
};
});
-
- // Update output timer for UI spinners (Side Effect)
- const hasExecuting = event.toolCalls.some(
- (tc) =>
- tc.status === CoreToolCallStatus.Executing ||
- ((tc.status === CoreToolCallStatus.Success ||
- tc.status === CoreToolCallStatus.Error) &&
- 'tailToolCallRequest' in tc &&
- tc.tailToolCallRequest != null),
- );
-
- if (hasExecuting) {
- setLastToolOutputTime(Date.now());
- }
};
messageBus.subscribe(MessageBusType.TOOL_CALLS_UPDATE, handler);
diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts
index c9910179a5..a57a2a059a 100644
--- a/packages/cli/src/ui/types.ts
+++ b/packages/cli/src/ui/types.ts
@@ -115,6 +115,7 @@ export interface IndividualToolCallDisplay {
originalRequestName?: string;
progress?: number;
progressTotal?: number;
+ progressPercent?: number;
}
export interface CompressionProps {
diff --git a/packages/cli/src/ui/utils/displayUtils.ts b/packages/cli/src/ui/utils/displayUtils.ts
index e311aa4974..6da169788e 100644
--- a/packages/cli/src/ui/utils/displayUtils.ts
+++ b/packages/cli/src/ui/utils/displayUtils.ts
@@ -19,6 +19,9 @@ export const CACHE_EFFICIENCY_MEDIUM = 15;
export const QUOTA_THRESHOLD_HIGH = 20;
export const QUOTA_THRESHOLD_MEDIUM = 5;
+export const QUOTA_USED_WARNING_THRESHOLD = 80;
+export const QUOTA_USED_CRITICAL_THRESHOLD = 95;
+
// --- Color Logic ---
export const getStatusColor = (
value: number,
@@ -36,3 +39,19 @@ export const getStatusColor = (
}
return options.defaultColor ?? theme.status.error;
};
+
+/**
+ * Gets the status color based on "used" percentage (where higher is worse).
+ */
+export const getUsedStatusColor = (
+ usedPercentage: number,
+ thresholds: { warning: number; critical: number },
+) => {
+ if (usedPercentage >= thresholds.critical) {
+ return theme.status.error;
+ }
+ if (usedPercentage >= thresholds.warning) {
+ return theme.status.warning;
+ }
+ return undefined;
+};
diff --git a/packages/cli/src/ui/utils/formatters.ts b/packages/cli/src/ui/utils/formatters.ts
index 3b4335bac9..f02c5b95bd 100644
--- a/packages/cli/src/ui/utils/formatters.ts
+++ b/packages/cli/src/ui/utils/formatters.ts
@@ -98,26 +98,44 @@ export function stripReferenceContent(text: string): string {
return text.replace(pattern, '').trim();
}
-export const formatResetTime = (resetTime: string): string => {
- const diff = new Date(resetTime).getTime() - Date.now();
+export const formatResetTime = (
+ resetTime: string | undefined,
+ terse = false,
+): string => {
+ if (!resetTime) return '';
+ const resetDate = new Date(resetTime);
+ if (isNaN(resetDate.getTime())) return '';
+
+ const diff = resetDate.getTime() - Date.now();
if (diff <= 0) return '';
const totalMinutes = Math.ceil(diff / (1000 * 60));
const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60;
- const fmt = (val: number, unit: 'hour' | 'minute') =>
- new Intl.NumberFormat('en', {
- style: 'unit',
- unit,
- unitDisplay: 'narrow',
- }).format(val);
-
- if (hours > 0 && minutes > 0) {
- return `resets in ${fmt(hours, 'hour')} ${fmt(minutes, 'minute')}`;
- } else if (hours > 0) {
- return `resets in ${fmt(hours, 'hour')}`;
+ if (terse) {
+ const hoursStr = hours > 0 ? `${hours}hr` : '';
+ const minutesStr = minutes > 0 ? `${minutes}m` : '';
+ return hoursStr && minutesStr
+ ? `${hoursStr} ${minutesStr}`
+ : hoursStr || minutesStr;
}
- return `resets in ${fmt(minutes, 'minute')}`;
+ let duration = '';
+ if (hours > 0) {
+ duration = `${hours} hour${hours > 1 ? 's' : ''}`;
+ if (minutes > 0) {
+ duration += ` ${minutes} minute${minutes > 1 ? 's' : ''}`;
+ }
+ } else {
+ duration = `${minutes} minute${minutes > 1 ? 's' : ''}`;
+ }
+
+ const timeStr = new Intl.DateTimeFormat('en-US', {
+ hour: 'numeric',
+ minute: 'numeric',
+ timeZoneName: 'short',
+ }).format(resetDate);
+
+ return `${duration} at ${timeStr}`;
};
diff --git a/packages/core/src/policy/policy-engine.test.ts b/packages/core/src/policy/policy-engine.test.ts
index 7f8418fbae..baf475701c 100644
--- a/packages/core/src/policy/policy-engine.test.ts
+++ b/packages/core/src/policy/policy-engine.test.ts
@@ -3107,7 +3107,6 @@ describe('PolicyEngine', () => {
expect(checkers[0].checker.name).toBe('c2');
});
});
-
describe('Tool Annotations', () => {
it('should match tools by semantic annotations', async () => {
engine = new PolicyEngine({
@@ -3171,7 +3170,6 @@ describe('PolicyEngine', () => {
).toBe(PolicyDecision.ALLOW);
});
});
-
describe('hook checkers', () => {
it('should add and retrieve hook checkers in priority order', () => {
engine.addHookChecker({
diff --git a/packages/core/src/tools/mcp-client.test.ts b/packages/core/src/tools/mcp-client.test.ts
index 0f7b58c39a..8612a838ca 100644
--- a/packages/core/src/tools/mcp-client.test.ts
+++ b/packages/core/src/tools/mcp-client.test.ts
@@ -2053,7 +2053,6 @@ describe('mcp-client', () => {
expect(callArgs.env!['GEMINI_CLI_EXT_VAR']).toBe('ext-value');
expect(callArgs.env!['RESOLVED_VAR']).toBe('ext-value');
});
-
it('should expand environment variables in mcpServerConfig.env and not redact them', async () => {
const mockedTransport = vi
.spyOn(SdkClientStdioLib, 'StdioClientTransport')