mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 05:12:55 -07:00
test(cli): fix flaky QuotaDisplay snapshot and env leakage in StatusDisplay
This commit is contained in:
@@ -169,7 +169,7 @@ jobs:
|
|||||||
npm run test:ci --workspace @google/gemini-cli
|
npm run test:ci --workspace @google/gemini-cli
|
||||||
else
|
else
|
||||||
# Explicitly list non-cli packages to ensure they are sharded correctly
|
# 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
|
npm run test:scripts
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
|||||||
@@ -183,7 +183,6 @@ describe('resolveWorkspacePolicyState', () => {
|
|||||||
setAutoAcceptWorkspacePolicies(originalValue);
|
setAutoAcceptWorkspacePolicies(originalValue);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not return workspace policies if cwd is the home directory', async () => {
|
it('should not return workspace policies if cwd is the home directory', async () => {
|
||||||
const policiesDir = path.join(tempDir, '.gemini', 'policies');
|
const policiesDir = path.join(tempDir, '.gemini', 'policies');
|
||||||
fs.mkdirSync(policiesDir, { recursive: true });
|
fs.mkdirSync(policiesDir, { recursive: true });
|
||||||
|
|||||||
@@ -235,8 +235,8 @@ describe('<Footer />', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
await waitUntilReady();
|
await waitUntilReady();
|
||||||
expect(lastFrame()).toContain('15%');
|
expect(lastFrame()).toContain('85%');
|
||||||
expect(normalizeFrame(lastFrame())).toMatchSnapshot();
|
expect(lastFrame()).toMatchSnapshot();
|
||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,20 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { render } from '../../test-utils/render.js';
|
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';
|
import { QuotaDisplay } from './QuotaDisplay.js';
|
||||||
|
|
||||||
describe('QuotaDisplay', () => {
|
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 () => {
|
it('should not render when remaining is undefined', async () => {
|
||||||
const { lastFrame, waitUntilReady, unmount } = render(
|
const { lastFrame, waitUntilReady, unmount } = render(
|
||||||
<QuotaDisplay remaining={undefined} limit={100} />,
|
<QuotaDisplay remaining={undefined} limit={100} />,
|
||||||
@@ -36,7 +46,7 @@ describe('QuotaDisplay', () => {
|
|||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not render when usage > 20%', async () => {
|
it('should not render when usage < 80%', async () => {
|
||||||
const { lastFrame, waitUntilReady, unmount } = render(
|
const { lastFrame, waitUntilReady, unmount } = render(
|
||||||
<QuotaDisplay remaining={85} limit={100} />,
|
<QuotaDisplay remaining={85} limit={100} />,
|
||||||
);
|
);
|
||||||
@@ -45,7 +55,7 @@ describe('QuotaDisplay', () => {
|
|||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render yellow when usage < 20%', async () => {
|
it('should render warning when used >= 80%', async () => {
|
||||||
const { lastFrame, waitUntilReady, unmount } = render(
|
const { lastFrame, waitUntilReady, unmount } = render(
|
||||||
<QuotaDisplay remaining={15} limit={100} />,
|
<QuotaDisplay remaining={15} limit={100} />,
|
||||||
);
|
);
|
||||||
@@ -54,7 +64,7 @@ describe('QuotaDisplay', () => {
|
|||||||
unmount();
|
unmount();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should render red when usage < 5%', async () => {
|
it('should render critical when used >= 95%', async () => {
|
||||||
const { lastFrame, waitUntilReady, unmount } = render(
|
const { lastFrame, waitUntilReady, unmount } = render(
|
||||||
<QuotaDisplay remaining={4} limit={100} />,
|
<QuotaDisplay remaining={4} limit={100} />,
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -7,9 +7,9 @@
|
|||||||
import type React from 'react';
|
import type React from 'react';
|
||||||
import { Text } from 'ink';
|
import { Text } from 'ink';
|
||||||
import {
|
import {
|
||||||
getStatusColor,
|
getUsedStatusColor,
|
||||||
QUOTA_THRESHOLD_HIGH,
|
QUOTA_USED_WARNING_THRESHOLD,
|
||||||
QUOTA_THRESHOLD_MEDIUM,
|
QUOTA_USED_CRITICAL_THRESHOLD,
|
||||||
} from '../utils/displayUtils.js';
|
} from '../utils/displayUtils.js';
|
||||||
import { formatResetTime } from '../utils/formatters.js';
|
import { formatResetTime } from '../utils/formatters.js';
|
||||||
|
|
||||||
@@ -34,32 +34,34 @@ export const QuotaDisplay: React.FC<QuotaDisplayProps> = ({
|
|||||||
return null;
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const color = getStatusColor(percentage, {
|
const color = getUsedStatusColor(usedPercentage, {
|
||||||
green: QUOTA_THRESHOLD_HIGH,
|
warning: QUOTA_USED_WARNING_THRESHOLD,
|
||||||
yellow: QUOTA_THRESHOLD_MEDIUM,
|
critical: QUOTA_USED_CRITICAL_THRESHOLD,
|
||||||
});
|
});
|
||||||
|
|
||||||
const resetInfo =
|
let text: string;
|
||||||
!terse && resetTime ? `, ${formatResetTime(resetTime)}` : '';
|
|
||||||
|
|
||||||
if (remaining === 0) {
|
if (remaining === 0) {
|
||||||
let text = terse
|
const resetMsg = resetTime
|
||||||
? 'Limit reached'
|
? `, resets in ${formatResetTime(resetTime, true)}`
|
||||||
: `/stats Limit reached${resetInfo}${!terse && '. /auth to continue.'}`;
|
: '';
|
||||||
if (lowercase) text = text.toLowerCase();
|
text = terse ? 'Limit reached' : `Limit reached${resetMsg}`;
|
||||||
return <Text color={color}>{text}</Text>;
|
} else {
|
||||||
|
text = terse
|
||||||
|
? `${usedPercentage.toFixed(0)}%`
|
||||||
|
: `${usedPercentage.toFixed(0)}% used${
|
||||||
|
resetTime ? ` (Limit resets in ${formatResetTime(resetTime)})` : ''
|
||||||
|
}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
let text = terse
|
if (lowercase) {
|
||||||
? `${percentage.toFixed(0)}%`
|
text = text.toLowerCase();
|
||||||
: `/stats ${percentage.toFixed(0)}% usage remaining${resetInfo}`;
|
}
|
||||||
if (lowercase) text = text.toLowerCase();
|
|
||||||
|
|
||||||
return <Text color={color}>{text}</Text>;
|
return <Text color={color}>{text}</Text>;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -9,9 +9,9 @@ import { Box, Text } from 'ink';
|
|||||||
import { theme } from '../semantic-colors.js';
|
import { theme } from '../semantic-colors.js';
|
||||||
import { formatResetTime } from '../utils/formatters.js';
|
import { formatResetTime } from '../utils/formatters.js';
|
||||||
import {
|
import {
|
||||||
getStatusColor,
|
getUsedStatusColor,
|
||||||
QUOTA_THRESHOLD_HIGH,
|
QUOTA_USED_WARNING_THRESHOLD,
|
||||||
QUOTA_THRESHOLD_MEDIUM,
|
QUOTA_USED_CRITICAL_THRESHOLD,
|
||||||
} from '../utils/displayUtils.js';
|
} from '../utils/displayUtils.js';
|
||||||
|
|
||||||
interface QuotaStatsInfoProps {
|
interface QuotaStatsInfoProps {
|
||||||
@@ -31,19 +31,24 @@ export const QuotaStatsInfo: React.FC<QuotaStatsInfoProps> = ({
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const percentage = (remaining / limit) * 100;
|
const usedPercentage = 100 - (remaining / limit) * 100;
|
||||||
const color = getStatusColor(percentage, {
|
const color = getUsedStatusColor(usedPercentage, {
|
||||||
green: QUOTA_THRESHOLD_HIGH,
|
warning: QUOTA_USED_WARNING_THRESHOLD,
|
||||||
yellow: QUOTA_THRESHOLD_MEDIUM,
|
critical: QUOTA_USED_CRITICAL_THRESHOLD,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" marginTop={0} marginBottom={0}>
|
<Box flexDirection="column" marginTop={0} marginBottom={0}>
|
||||||
<Text color={color}>
|
<Text color={color}>
|
||||||
{remaining === 0
|
{remaining === 0
|
||||||
? `Limit reached`
|
? `Limit reached${
|
||||||
: `${percentage.toFixed(0)}% usage remaining`}
|
resetTime ? `, resets in ${formatResetTime(resetTime, true)}` : ''
|
||||||
{resetTime && `, ${formatResetTime(resetTime)}`}
|
}`
|
||||||
|
: `${usedPercentage.toFixed(0)}% used${
|
||||||
|
resetTime
|
||||||
|
? ` (Limit resets in ${formatResetTime(resetTime)})`
|
||||||
|
: ''
|
||||||
|
}`}
|
||||||
</Text>
|
</Text>
|
||||||
{showDetails && (
|
{showDetails && (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -465,9 +465,9 @@ describe('<StatsDisplay />', () => {
|
|||||||
await waitUntilReady();
|
await waitUntilReady();
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
|
|
||||||
expect(output).toContain('Usage remaining');
|
expect(output).toContain('Model usage');
|
||||||
expect(output).toContain('75.0%');
|
expect(output).toContain('25% used');
|
||||||
expect(output).toContain('resets in 1h 30m');
|
expect(output).toContain('Limit resets in');
|
||||||
expect(output).toMatchSnapshot();
|
expect(output).toMatchSnapshot();
|
||||||
|
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
@@ -521,8 +521,8 @@ describe('<StatsDisplay />', () => {
|
|||||||
await waitUntilReady();
|
await waitUntilReady();
|
||||||
const output = lastFrame();
|
const output = lastFrame();
|
||||||
|
|
||||||
// (10 + 700) / (100 + 1000) = 710 / 1100 = 64.5%
|
// (1 - 710/1100) * 100 = 35.5%
|
||||||
expect(output).toContain('65% usage remaining');
|
expect(output).toContain('35% used');
|
||||||
expect(output).toContain('Usage limit: 1,100');
|
expect(output).toContain('Usage limit: 1,100');
|
||||||
expect(output).toMatchSnapshot();
|
expect(output).toMatchSnapshot();
|
||||||
|
|
||||||
@@ -571,8 +571,8 @@ describe('<StatsDisplay />', () => {
|
|||||||
|
|
||||||
expect(output).toContain('gemini-2.5-flash');
|
expect(output).toContain('gemini-2.5-flash');
|
||||||
expect(output).toContain('-'); // for requests
|
expect(output).toContain('-'); // for requests
|
||||||
expect(output).toContain('50.0%');
|
expect(output).toContain('50% used');
|
||||||
expect(output).toContain('resets in 2h');
|
expect(output).toContain('Limit resets in');
|
||||||
expect(output).toMatchSnapshot();
|
expect(output).toMatchSnapshot();
|
||||||
|
|
||||||
vi.useRealTimers();
|
vi.useRealTimers();
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ import {
|
|||||||
USER_AGREEMENT_RATE_MEDIUM,
|
USER_AGREEMENT_RATE_MEDIUM,
|
||||||
CACHE_EFFICIENCY_HIGH,
|
CACHE_EFFICIENCY_HIGH,
|
||||||
CACHE_EFFICIENCY_MEDIUM,
|
CACHE_EFFICIENCY_MEDIUM,
|
||||||
|
getUsedStatusColor,
|
||||||
|
QUOTA_USED_WARNING_THRESHOLD,
|
||||||
|
QUOTA_USED_CRITICAL_THRESHOLD,
|
||||||
} from '../utils/displayUtils.js';
|
} from '../utils/displayUtils.js';
|
||||||
import { computeSessionStats } from '../utils/computeStats.js';
|
import { computeSessionStats } from '../utils/computeStats.js';
|
||||||
import {
|
import {
|
||||||
@@ -168,7 +171,26 @@ const ModelUsageTable: React.FC<{
|
|||||||
const uncachedWidth = 15;
|
const uncachedWidth = 15;
|
||||||
const cachedWidth = 14;
|
const cachedWidth = 14;
|
||||||
const outputTokensWidth = 15;
|
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 (
|
||||||
|
<Box flexDirection="row">
|
||||||
|
<Text color={color}>{'▬'.repeat(filledSteps)}</Text>
|
||||||
|
<Text color={theme.border.default}>{'▬'.repeat(emptySteps)}</Text>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const cacheEfficiencyColor = getStatusColor(cacheEfficiency, {
|
const cacheEfficiencyColor = getStatusColor(cacheEfficiency, {
|
||||||
green: CACHE_EFFICIENCY_HIGH,
|
green: CACHE_EFFICIENCY_HIGH,
|
||||||
@@ -183,19 +205,21 @@ const ModelUsageTable: React.FC<{
|
|||||||
: uncachedWidth + cachedWidth + outputTokensWidth);
|
: uncachedWidth + cachedWidth + outputTokensWidth);
|
||||||
|
|
||||||
const isAuto = currentModel && isAutoModel(currentModel);
|
const isAuto = currentModel && isAutoModel(currentModel);
|
||||||
const modelUsageTitle = isAuto
|
const modelUsageTitle = isAuto ? (
|
||||||
? `${getDisplayString(currentModel)} Usage`
|
<Text color={theme.text.primary} wrap="truncate-end">
|
||||||
: `Model Usage`;
|
<Text bold>Model Usage:</Text> {getDisplayString(currentModel)}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text bold color={theme.text.primary} wrap="truncate-end">
|
||||||
|
Model Usage
|
||||||
|
</Text>
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" marginBottom={1}>
|
<Box flexDirection="column" marginBottom={1}>
|
||||||
{/* Header */}
|
{/* Header */}
|
||||||
<Box alignItems="flex-end">
|
<Box alignItems="flex-end">
|
||||||
<Box width={nameWidth}>
|
<Box width={totalWidth}>{modelUsageTitle}</Box>
|
||||||
<Text bold color={theme.text.primary} wrap="truncate-end">
|
|
||||||
{modelUsageTitle}
|
|
||||||
</Text>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
{isAuto &&
|
{isAuto &&
|
||||||
@@ -270,10 +294,11 @@ const ModelUsageTable: React.FC<{
|
|||||||
<Box
|
<Box
|
||||||
width={usageLimitWidth}
|
width={usageLimitWidth}
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
alignItems="flex-end"
|
alignItems="flex-start"
|
||||||
|
paddingLeft={4}
|
||||||
>
|
>
|
||||||
<Text bold color={theme.text.primary}>
|
<Text bold color={theme.text.primary}>
|
||||||
Usage remaining
|
Model usage
|
||||||
</Text>
|
</Text>
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@@ -355,16 +380,62 @@ const ModelUsageTable: React.FC<{
|
|||||||
<Box
|
<Box
|
||||||
width={usageLimitWidth}
|
width={usageLimitWidth}
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
alignItems="flex-end"
|
alignItems="flex-start"
|
||||||
|
paddingLeft={4}
|
||||||
>
|
>
|
||||||
{row.bucket &&
|
{row.bucket && row.bucket.remainingFraction != null && (
|
||||||
row.bucket.remainingFraction != null &&
|
<Box flexDirection="row">
|
||||||
row.bucket.resetTime && (
|
{(() => {
|
||||||
<Text color={theme.text.secondary} wrap="truncate-end">
|
const actualUsedFraction = 1 - row.bucket.remainingFraction;
|
||||||
{(row.bucket.remainingFraction * 100).toFixed(1)}%{' '}
|
// If we have session activity but 0% server usage, show 0.1% as a hint.
|
||||||
{formatResetTime(row.bucket.resetTime)}
|
const effectiveUsedFraction =
|
||||||
</Text>
|
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)}
|
||||||
|
<Box marginLeft={1}>
|
||||||
|
<Text wrap="truncate-end">
|
||||||
|
{row.bucket.remainingFraction === 0 ? (
|
||||||
|
<Text color={theme.status.error}>
|
||||||
|
Limit reached
|
||||||
|
{row.bucket.resetTime &&
|
||||||
|
`, resets in ${formatResetTime(row.bucket.resetTime, true)}`}
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Text color={statusColor}>{percentageText}</Text>
|
||||||
|
<Text color={theme.text.secondary}>
|
||||||
|
{row.bucket.resetTime &&
|
||||||
|
formatResetTime(row.bucket.resetTime)
|
||||||
|
? ` (Limit resets in ${formatResetTime(row.bucket.resetTime)})`
|
||||||
|
: ''}
|
||||||
|
</Text>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</Text>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
})()}
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ exports[`<Footer /> > displays "Limit reached" message when remaining is 0 1`] =
|
|||||||
|
|
||||||
exports[`<Footer /> > displays the usage indicator when usage is low 1`] = `
|
exports[`<Footer /> > displays the usage indicator when usage is low 1`] = `
|
||||||
" workspace (/directory) sandbox /model /stats
|
" 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 /> > footer configuration filtering (golden snapshots) > render
|
|||||||
|
|
||||||
exports[`<Footer /> > hides the usage indicator when usage is not near limit 1`] = `
|
exports[`<Footer /> > hides the usage indicator when usage is not near limit 1`] = `
|
||||||
" workspace (/directory) sandbox /model /stats
|
" 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%
|
||||||
"
|
"
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||||
|
|
||||||
exports[`QuotaDisplay > should NOT render reset time when terse is true 1`] = `
|
exports[`QuotaDisplay > should NOT render reset time when terse is true 1`] = `
|
||||||
"15%
|
"85%
|
||||||
"
|
"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`QuotaDisplay > should render red when usage < 5% 1`] = `
|
exports[`QuotaDisplay > should render critical when used >= 95% 1`] = `
|
||||||
"/stats 4% usage remaining
|
"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`] = `
|
exports[`QuotaDisplay > should render warning when used >= 80% 1`] = `
|
||||||
"/stats 15% usage remaining, resets in 1h
|
"85% used
|
||||||
"
|
"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
exports[`QuotaDisplay > should render yellow when usage < 20% 1`] = `
|
exports[`QuotaDisplay > should render with reset time when provided 1`] = `
|
||||||
"/stats 15% usage remaining
|
"85% used (Limit resets in 1 hour at 1:29 PM PST)
|
||||||
"
|
"
|
||||||
`;
|
`;
|
||||||
|
|||||||
@@ -162,16 +162,16 @@ exports[`<StatsDisplay /> > Quota Display > renders pooled quota information for
|
|||||||
│ » API Time: 0s (0.0%) │
|
│ » API Time: 0s (0.0%) │
|
||||||
│ » Tool Time: 0s (0.0%) │
|
│ » Tool Time: 0s (0.0%) │
|
||||||
│ │
|
│ │
|
||||||
│ auto Usage │
|
│ Model Usage: auto │
|
||||||
│ 65% usage remaining │
|
│ 35% used │
|
||||||
│ Usage limit: 1,100 │
|
│ Usage limit: 1,100 │
|
||||||
│ Usage limits span all sessions and reset daily. │
|
│ Usage limits span all sessions and reset daily. │
|
||||||
│ For a full token breakdown, run \`/stats model\`. │
|
│ For a full token breakdown, run \`/stats model\`. │
|
||||||
│ │
|
│ │
|
||||||
│ Model Reqs Usage remaining │
|
│ Model Reqs Model usage │
|
||||||
│ ──────────────────────────────────────────────────────────── │
|
│ ────────────────────────────────────────────────────────────────────────────────────────────────│
|
||||||
│ gemini-2.5-pro - │
|
│ gemini-2.5-pro - ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 90% used │
|
||||||
│ gemini-2.5-flash - │
|
│ gemini-2.5-flash - ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 30% used │
|
||||||
│ │
|
│ │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
"
|
"
|
||||||
@@ -194,9 +194,9 @@ exports[`<StatsDisplay /> > Quota Display > renders quota information for unused
|
|||||||
│ » Tool Time: 0s (0.0%) │
|
│ » Tool Time: 0s (0.0%) │
|
||||||
│ │
|
│ │
|
||||||
│ Model Usage │
|
│ Model Usage │
|
||||||
│ Model Reqs Usage remaining │
|
│ Model Reqs Model usage │
|
||||||
│ ──────────────────────────────────────────────────────────── │
|
│ ────────────────────────────────────────────────────────────────────────────────────────────────│
|
||||||
│ gemini-2.5-flash - 50.0% resets in 2h │
|
│ gemini-2.5-flash - ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 50% used (Limit resets in 2 hours at 6:00 AM│
|
||||||
│ │
|
│ │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
"
|
"
|
||||||
@@ -219,9 +219,9 @@ exports[`<StatsDisplay /> > Quota Display > renders quota information when quota
|
|||||||
│ » Tool Time: 0s (0.0%) │
|
│ » Tool Time: 0s (0.0%) │
|
||||||
│ │
|
│ │
|
||||||
│ Model Usage │
|
│ Model Usage │
|
||||||
│ Model Reqs Usage remaining │
|
│ Model Reqs Model usage │
|
||||||
│ ──────────────────────────────────────────────────────────── │
|
│ ────────────────────────────────────────────────────────────────────────────────────────────────│
|
||||||
│ gemini-2.5-pro 1 75.0% resets in 1h 30m │
|
│ gemini-2.5-pro 1 ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 25% used (Limit resets in 1 hour 30 minutes │
|
||||||
│ │
|
│ │
|
||||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||||
"
|
"
|
||||||
|
|||||||
@@ -60,6 +60,7 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
|||||||
originalRequestName,
|
originalRequestName,
|
||||||
progress,
|
progress,
|
||||||
progressTotal,
|
progressTotal,
|
||||||
|
progressPercent,
|
||||||
}) => {
|
}) => {
|
||||||
const isThisShellFocused = checkIsShellFocused(
|
const isThisShellFocused = checkIsShellFocused(
|
||||||
name,
|
name,
|
||||||
@@ -98,6 +99,8 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
|||||||
status={status}
|
status={status}
|
||||||
description={description}
|
description={description}
|
||||||
emphasis={emphasis}
|
emphasis={emphasis}
|
||||||
|
progressMessage={progressMessage}
|
||||||
|
progressPercent={progressPercent}
|
||||||
originalRequestName={originalRequestName}
|
originalRequestName={originalRequestName}
|
||||||
/>
|
/>
|
||||||
<FocusHint
|
<FocusHint
|
||||||
|
|||||||
@@ -192,6 +192,8 @@ type ToolInfoProps = {
|
|||||||
description: string;
|
description: string;
|
||||||
status: CoreToolCallStatus;
|
status: CoreToolCallStatus;
|
||||||
emphasis: TextEmphasis;
|
emphasis: TextEmphasis;
|
||||||
|
progressMessage?: string;
|
||||||
|
progressPercent?: number;
|
||||||
originalRequestName?: string;
|
originalRequestName?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -200,6 +202,8 @@ export const ToolInfo: React.FC<ToolInfoProps> = ({
|
|||||||
description,
|
description,
|
||||||
status: coreStatus,
|
status: coreStatus,
|
||||||
emphasis,
|
emphasis,
|
||||||
|
progressMessage: _progressMessage,
|
||||||
|
progressPercent: _progressPercent,
|
||||||
originalRequestName,
|
originalRequestName,
|
||||||
}) => {
|
}) => {
|
||||||
const status = mapCoreStatusToDisplayStatus(coreStatus);
|
const status = mapCoreStatusToDisplayStatus(coreStatus);
|
||||||
|
|||||||
@@ -325,7 +325,6 @@ describe('toolMapping', () => {
|
|||||||
const result = mapToDisplay(toolCall);
|
const result = mapToDisplay(toolCall);
|
||||||
expect(result.tools[0].originalRequestName).toBe('original_tool');
|
expect(result.tools[0].originalRequestName).toBe('original_tool');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('propagates isClientInitiated from tool request', () => {
|
it('propagates isClientInitiated from tool request', () => {
|
||||||
const clientInitiatedTool: ScheduledToolCall = {
|
const clientInitiatedTool: ScheduledToolCall = {
|
||||||
status: CoreToolCallStatus.Scheduled,
|
status: CoreToolCallStatus.Scheduled,
|
||||||
|
|||||||
@@ -117,6 +117,20 @@ export function useToolScheduler(
|
|||||||
const handler = (event: ToolCallsUpdateMessage) => {
|
const handler = (event: ToolCallsUpdateMessage) => {
|
||||||
const isRoot = event.schedulerId === ROOT_SCHEDULER_ID;
|
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) => {
|
setToolCallsMap((prev) => {
|
||||||
const prevCalls = prev[event.schedulerId] ?? [];
|
const prevCalls = prev[event.schedulerId] ?? [];
|
||||||
const prevCallIds = new Set(prevCalls.map((tc) => tc.request.callId));
|
const prevCallIds = new Set(prevCalls.map((tc) => tc.request.callId));
|
||||||
@@ -151,20 +165,6 @@ export function useToolScheduler(
|
|||||||
[event.schedulerId]: adapted,
|
[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);
|
messageBus.subscribe(MessageBusType.TOOL_CALLS_UPDATE, handler);
|
||||||
|
|||||||
@@ -115,6 +115,7 @@ export interface IndividualToolCallDisplay {
|
|||||||
originalRequestName?: string;
|
originalRequestName?: string;
|
||||||
progress?: number;
|
progress?: number;
|
||||||
progressTotal?: number;
|
progressTotal?: number;
|
||||||
|
progressPercent?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CompressionProps {
|
export interface CompressionProps {
|
||||||
|
|||||||
@@ -19,6 +19,9 @@ export const CACHE_EFFICIENCY_MEDIUM = 15;
|
|||||||
export const QUOTA_THRESHOLD_HIGH = 20;
|
export const QUOTA_THRESHOLD_HIGH = 20;
|
||||||
export const QUOTA_THRESHOLD_MEDIUM = 5;
|
export const QUOTA_THRESHOLD_MEDIUM = 5;
|
||||||
|
|
||||||
|
export const QUOTA_USED_WARNING_THRESHOLD = 80;
|
||||||
|
export const QUOTA_USED_CRITICAL_THRESHOLD = 95;
|
||||||
|
|
||||||
// --- Color Logic ---
|
// --- Color Logic ---
|
||||||
export const getStatusColor = (
|
export const getStatusColor = (
|
||||||
value: number,
|
value: number,
|
||||||
@@ -36,3 +39,19 @@ export const getStatusColor = (
|
|||||||
}
|
}
|
||||||
return options.defaultColor ?? theme.status.error;
|
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;
|
||||||
|
};
|
||||||
|
|||||||
@@ -98,26 +98,44 @@ export function stripReferenceContent(text: string): string {
|
|||||||
return text.replace(pattern, '').trim();
|
return text.replace(pattern, '').trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
export const formatResetTime = (resetTime: string): string => {
|
export const formatResetTime = (
|
||||||
const diff = new Date(resetTime).getTime() - Date.now();
|
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 '';
|
if (diff <= 0) return '';
|
||||||
|
|
||||||
const totalMinutes = Math.ceil(diff / (1000 * 60));
|
const totalMinutes = Math.ceil(diff / (1000 * 60));
|
||||||
const hours = Math.floor(totalMinutes / 60);
|
const hours = Math.floor(totalMinutes / 60);
|
||||||
const minutes = totalMinutes % 60;
|
const minutes = totalMinutes % 60;
|
||||||
|
|
||||||
const fmt = (val: number, unit: 'hour' | 'minute') =>
|
if (terse) {
|
||||||
new Intl.NumberFormat('en', {
|
const hoursStr = hours > 0 ? `${hours}hr` : '';
|
||||||
style: 'unit',
|
const minutesStr = minutes > 0 ? `${minutes}m` : '';
|
||||||
unit,
|
return hoursStr && minutesStr
|
||||||
unitDisplay: 'narrow',
|
? `${hoursStr} ${minutesStr}`
|
||||||
}).format(val);
|
: hoursStr || minutesStr;
|
||||||
|
|
||||||
if (hours > 0 && minutes > 0) {
|
|
||||||
return `resets in ${fmt(hours, 'hour')} ${fmt(minutes, 'minute')}`;
|
|
||||||
} else if (hours > 0) {
|
|
||||||
return `resets in ${fmt(hours, 'hour')}`;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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}`;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -3107,7 +3107,6 @@ describe('PolicyEngine', () => {
|
|||||||
expect(checkers[0].checker.name).toBe('c2');
|
expect(checkers[0].checker.name).toBe('c2');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Tool Annotations', () => {
|
describe('Tool Annotations', () => {
|
||||||
it('should match tools by semantic annotations', async () => {
|
it('should match tools by semantic annotations', async () => {
|
||||||
engine = new PolicyEngine({
|
engine = new PolicyEngine({
|
||||||
@@ -3171,7 +3170,6 @@ describe('PolicyEngine', () => {
|
|||||||
).toBe(PolicyDecision.ALLOW);
|
).toBe(PolicyDecision.ALLOW);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('hook checkers', () => {
|
describe('hook checkers', () => {
|
||||||
it('should add and retrieve hook checkers in priority order', () => {
|
it('should add and retrieve hook checkers in priority order', () => {
|
||||||
engine.addHookChecker({
|
engine.addHookChecker({
|
||||||
|
|||||||
@@ -2053,7 +2053,6 @@ describe('mcp-client', () => {
|
|||||||
expect(callArgs.env!['GEMINI_CLI_EXT_VAR']).toBe('ext-value');
|
expect(callArgs.env!['GEMINI_CLI_EXT_VAR']).toBe('ext-value');
|
||||||
expect(callArgs.env!['RESOLVED_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 () => {
|
it('should expand environment variables in mcpServerConfig.env and not redact them', async () => {
|
||||||
const mockedTransport = vi
|
const mockedTransport = vi
|
||||||
.spyOn(SdkClientStdioLib, 'StdioClientTransport')
|
.spyOn(SdkClientStdioLib, 'StdioClientTransport')
|
||||||
|
|||||||
Reference in New Issue
Block a user