mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-18 01:51:20 -07:00
Polish UX to be width agnostic and generic code review fixes
using /review-frontend
This commit is contained in:
@@ -48,14 +48,16 @@ export const QuotaDisplay: React.FC<QuotaDisplayProps> = ({
|
||||
let text: string;
|
||||
if (remaining === 0) {
|
||||
const resetMsg = resetTime
|
||||
? `, resets in ${formatResetTime(resetTime, true)}`
|
||||
? `, resets in ${formatResetTime(resetTime, 'terse')}`
|
||||
: '';
|
||||
text = terse ? 'Limit reached' : `Limit reached${resetMsg}`;
|
||||
} else {
|
||||
text = terse
|
||||
? `${usedPercentage.toFixed(0)}%`
|
||||
: `${usedPercentage.toFixed(0)}% used${
|
||||
resetTime ? ` (Limit resets in ${formatResetTime(resetTime)})` : ''
|
||||
resetTime
|
||||
? ` (Limit resets in ${formatResetTime(resetTime, 'terse')})`
|
||||
: ''
|
||||
}`;
|
||||
}
|
||||
|
||||
|
||||
@@ -42,11 +42,13 @@ export const QuotaStatsInfo: React.FC<QuotaStatsInfoProps> = ({
|
||||
<Text color={color}>
|
||||
{remaining === 0
|
||||
? `Limit reached${
|
||||
resetTime ? `, resets in ${formatResetTime(resetTime, true)}` : ''
|
||||
resetTime
|
||||
? `, resets in ${formatResetTime(resetTime, 'terse')}`
|
||||
: ''
|
||||
}`
|
||||
: `${usedPercentage.toFixed(0)}% used${
|
||||
resetTime
|
||||
? ` (Limit resets in ${formatResetTime(resetTime)})`
|
||||
? ` (Limit resets in ${formatResetTime(resetTime, 'terse')})`
|
||||
: ''
|
||||
}`}
|
||||
</Text>
|
||||
|
||||
@@ -68,6 +68,14 @@ const createTestMetrics = (
|
||||
});
|
||||
|
||||
describe('<StatsDisplay />', () => {
|
||||
beforeEach(() => {
|
||||
vi.stubEnv('TZ', 'UTC');
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
});
|
||||
|
||||
it('renders only the Performance section in its zero state', async () => {
|
||||
const zeroMetrics = createTestMetrics();
|
||||
|
||||
@@ -466,8 +474,8 @@ describe('<StatsDisplay />', () => {
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('Model usage');
|
||||
expect(output).toContain('25% used');
|
||||
expect(output).toContain('Limit resets in');
|
||||
expect(output).toContain('25%');
|
||||
expect(output).toContain('Usage resets');
|
||||
expect(output).toMatchSnapshot();
|
||||
|
||||
vi.useRealTimers();
|
||||
@@ -522,7 +530,7 @@ describe('<StatsDisplay />', () => {
|
||||
const output = lastFrame();
|
||||
|
||||
// (1 - 710/1100) * 100 = 35.5%
|
||||
expect(output).toContain('35% used');
|
||||
expect(output).toContain('35%');
|
||||
expect(output).toContain('Usage limit: 1,100');
|
||||
expect(output).toMatchSnapshot();
|
||||
|
||||
@@ -571,8 +579,8 @@ describe('<StatsDisplay />', () => {
|
||||
|
||||
expect(output).toContain('gemini-2.5-flash');
|
||||
expect(output).toContain('-'); // for requests
|
||||
expect(output).toContain('50% used');
|
||||
expect(output).toContain('Limit resets in');
|
||||
expect(output).toContain('50%');
|
||||
expect(output).toContain('Usage resets');
|
||||
expect(output).toMatchSnapshot();
|
||||
|
||||
vi.useRealTimers();
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
*/
|
||||
|
||||
import type React from 'react';
|
||||
import { Box, Text } from 'ink';
|
||||
import { Box, Text, useStdout } from 'ink';
|
||||
import { ThemedGradient } from './ThemedGradient.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { formatDuration, formatResetTime } from '../utils/formatters.js';
|
||||
@@ -158,6 +158,8 @@ const ModelUsageTable: React.FC<{
|
||||
useGemini3_1,
|
||||
useCustomToolModel,
|
||||
}) => {
|
||||
const { stdout } = useStdout();
|
||||
const terminalWidth = stdout?.columns ?? 84;
|
||||
const rows = buildModelRows(models, quotas, useGemini3_1, useCustomToolModel);
|
||||
|
||||
if (rows.length === 0) {
|
||||
@@ -166,15 +168,29 @@ const ModelUsageTable: React.FC<{
|
||||
|
||||
const showQuotaColumn = !!quotas && rows.some((row) => !!row.bucket);
|
||||
|
||||
const nameWidth = 25;
|
||||
const requestsWidth = 7;
|
||||
const nameWidth = 23;
|
||||
const requestsWidth = 5;
|
||||
const uncachedWidth = 15;
|
||||
const cachedWidth = 14;
|
||||
const outputTokensWidth = 15;
|
||||
const usageLimitWidth = showQuotaColumn ? 85 : 0;
|
||||
const percentageWidth = showQuotaColumn ? 6 : 0;
|
||||
const resetWidth = 22;
|
||||
|
||||
const renderProgressBar = (usedFraction: number, color: string) => {
|
||||
const totalSteps = 20;
|
||||
// Total width of other columns (including parent box paddingX={2})
|
||||
const fixedWidth = nameWidth + requestsWidth + percentageWidth + resetWidth;
|
||||
const outerPadding = 4;
|
||||
const availableForUsage = terminalWidth - outerPadding - fixedWidth;
|
||||
|
||||
const usageLimitWidth = showQuotaColumn
|
||||
? Math.max(10, Math.min(24, availableForUsage))
|
||||
: 0;
|
||||
const progressBarWidth = Math.max(2, usageLimitWidth - 4);
|
||||
|
||||
const renderProgressBar = (
|
||||
usedFraction: number,
|
||||
color: string,
|
||||
totalSteps = 20,
|
||||
) => {
|
||||
let filledSteps = Math.round(usedFraction * totalSteps);
|
||||
|
||||
// If something is used (fraction > 0) but rounds to 0, show 1 tick.
|
||||
@@ -183,11 +199,13 @@ const ModelUsageTable: React.FC<{
|
||||
filledSteps = Math.min(Math.max(filledSteps, 1), totalSteps - 1);
|
||||
}
|
||||
|
||||
const emptySteps = totalSteps - filledSteps;
|
||||
const emptySteps = Math.max(0, totalSteps - filledSteps);
|
||||
return (
|
||||
<Box flexDirection="row">
|
||||
<Text color={color}>{'▬'.repeat(filledSteps)}</Text>
|
||||
<Text color={theme.border.default}>{'▬'.repeat(emptySteps)}</Text>
|
||||
<Box flexDirection="row" flexShrink={0}>
|
||||
<Text wrap="truncate-end">
|
||||
<Text color={color}>{'▬'.repeat(filledSteps)}</Text>
|
||||
<Text color={theme.border.default}>{'▬'.repeat(emptySteps)}</Text>
|
||||
</Text>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
@@ -201,27 +219,13 @@ const ModelUsageTable: React.FC<{
|
||||
nameWidth +
|
||||
requestsWidth +
|
||||
(showQuotaColumn
|
||||
? usageLimitWidth
|
||||
? usageLimitWidth + percentageWidth + resetWidth
|
||||
: uncachedWidth + cachedWidth + outputTokensWidth);
|
||||
|
||||
const isAuto = currentModel && isAutoModel(currentModel);
|
||||
const modelUsageTitle = isAuto ? (
|
||||
<Text color={theme.text.primary} wrap="truncate-end">
|
||||
<Text bold>Model Usage:</Text> {getDisplayString(currentModel)}
|
||||
</Text>
|
||||
) : (
|
||||
<Text bold color={theme.text.primary} wrap="truncate-end">
|
||||
Model Usage
|
||||
</Text>
|
||||
);
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" marginBottom={1}>
|
||||
{/* Header */}
|
||||
<Box alignItems="flex-end">
|
||||
<Box width={totalWidth}>{modelUsageTitle}</Box>
|
||||
</Box>
|
||||
|
||||
{isAuto &&
|
||||
showQuotaColumn &&
|
||||
pooledRemaining !== undefined &&
|
||||
@@ -240,7 +244,7 @@ const ModelUsageTable: React.FC<{
|
||||
)}
|
||||
|
||||
<Box alignItems="flex-end">
|
||||
<Box width={nameWidth}>
|
||||
<Box width={nameWidth} flexShrink={0}>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Model
|
||||
</Text>
|
||||
@@ -291,16 +295,31 @@ const ModelUsageTable: React.FC<{
|
||||
</>
|
||||
)}
|
||||
{showQuotaColumn && (
|
||||
<Box
|
||||
width={usageLimitWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-start"
|
||||
paddingLeft={4}
|
||||
>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Model usage
|
||||
</Text>
|
||||
</Box>
|
||||
<>
|
||||
<Box
|
||||
width={usageLimitWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-start"
|
||||
paddingLeft={4}
|
||||
flexShrink={0}
|
||||
>
|
||||
<Text bold color={theme.text.primary}>
|
||||
Model usage
|
||||
</Text>
|
||||
</Box>
|
||||
<Box width={percentageWidth} flexShrink={0} />
|
||||
<Box
|
||||
width={resetWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-start"
|
||||
paddingLeft={2}
|
||||
flexShrink={0}
|
||||
>
|
||||
<Text bold color={theme.text.primary} wrap="truncate-end">
|
||||
Usage resets
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
|
||||
@@ -315,130 +334,150 @@ const ModelUsageTable: React.FC<{
|
||||
width={totalWidth}
|
||||
></Box>
|
||||
|
||||
{rows.map((row) => (
|
||||
<Box key={row.key}>
|
||||
<Box width={nameWidth}>
|
||||
<Text
|
||||
color={row.isActive ? theme.text.primary : theme.text.secondary}
|
||||
wrap="truncate-end"
|
||||
{rows.map((row) => {
|
||||
let effectiveUsedFraction = 0;
|
||||
let usedPercentage = 0;
|
||||
let statusColor = theme.ui.comment;
|
||||
let percentageText = '';
|
||||
|
||||
if (row.bucket && row.bucket.remainingFraction != null) {
|
||||
const actualUsedFraction = 1 - row.bucket.remainingFraction;
|
||||
effectiveUsedFraction =
|
||||
actualUsedFraction === 0 && row.isActive
|
||||
? 0.001
|
||||
: actualUsedFraction;
|
||||
usedPercentage = effectiveUsedFraction * 100;
|
||||
statusColor =
|
||||
getUsedStatusColor(usedPercentage, {
|
||||
warning: QUOTA_USED_WARNING_THRESHOLD,
|
||||
critical: QUOTA_USED_CRITICAL_THRESHOLD,
|
||||
}) ?? (row.isActive ? theme.text.primary : theme.ui.comment);
|
||||
percentageText =
|
||||
usedPercentage > 0 && usedPercentage < 1
|
||||
? `${usedPercentage.toFixed(1)}%`
|
||||
: `${usedPercentage.toFixed(0)}%`;
|
||||
}
|
||||
|
||||
return (
|
||||
<Box key={row.key}>
|
||||
<Box width={nameWidth} flexShrink={0}>
|
||||
<Text
|
||||
color={row.isActive ? theme.text.primary : theme.text.secondary}
|
||||
wrap="truncate-end"
|
||||
>
|
||||
{row.modelName}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
width={requestsWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-end"
|
||||
flexShrink={0}
|
||||
>
|
||||
{row.modelName}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
width={requestsWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-end"
|
||||
flexShrink={0}
|
||||
>
|
||||
<Text
|
||||
color={row.isActive ? theme.text.primary : theme.text.secondary}
|
||||
>
|
||||
{row.requests}
|
||||
</Text>
|
||||
</Box>
|
||||
{!showQuotaColumn && (
|
||||
<>
|
||||
<Box
|
||||
width={uncachedWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-end"
|
||||
flexShrink={0}
|
||||
<Text
|
||||
color={row.isActive ? theme.text.primary : theme.text.secondary}
|
||||
>
|
||||
<Text
|
||||
color={
|
||||
row.isActive ? theme.text.primary : theme.text.secondary
|
||||
}
|
||||
{row.requests}
|
||||
</Text>
|
||||
</Box>
|
||||
{!showQuotaColumn && (
|
||||
<>
|
||||
<Box
|
||||
width={uncachedWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-end"
|
||||
flexShrink={0}
|
||||
>
|
||||
{row.inputTokens}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
width={cachedWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-end"
|
||||
flexShrink={0}
|
||||
>
|
||||
<Text color={theme.text.secondary}>{row.cachedTokens}</Text>
|
||||
</Box>
|
||||
<Box
|
||||
width={outputTokensWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-end"
|
||||
flexShrink={0}
|
||||
>
|
||||
<Text
|
||||
color={
|
||||
row.isActive ? theme.text.primary : theme.text.secondary
|
||||
}
|
||||
<Text
|
||||
color={
|
||||
row.isActive ? theme.text.primary : theme.text.secondary
|
||||
}
|
||||
>
|
||||
{row.inputTokens}
|
||||
</Text>
|
||||
</Box>
|
||||
<Box
|
||||
width={cachedWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-end"
|
||||
flexShrink={0}
|
||||
>
|
||||
{row.outputTokens}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
<Box
|
||||
width={usageLimitWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-start"
|
||||
paddingLeft={4}
|
||||
>
|
||||
{row.bucket && row.bucket.remainingFraction != null && (
|
||||
<Box flexDirection="row">
|
||||
{(() => {
|
||||
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)}
|
||||
<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 color={theme.text.secondary}>{row.cachedTokens}</Text>
|
||||
</Box>
|
||||
<Box
|
||||
width={outputTokensWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-end"
|
||||
flexShrink={0}
|
||||
>
|
||||
<Text
|
||||
color={
|
||||
row.isActive ? theme.text.primary : theme.text.secondary
|
||||
}
|
||||
>
|
||||
{row.outputTokens}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
{showQuotaColumn && (
|
||||
<>
|
||||
<Box
|
||||
width={usageLimitWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-start"
|
||||
paddingLeft={4}
|
||||
flexShrink={0}
|
||||
>
|
||||
{row.bucket && row.bucket.remainingFraction != null && (
|
||||
<Box flexDirection="row" flexShrink={0}>
|
||||
{renderProgressBar(
|
||||
effectiveUsedFraction,
|
||||
statusColor,
|
||||
progressBarWidth,
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
width={percentageWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-end"
|
||||
flexShrink={0}
|
||||
>
|
||||
{row.bucket && row.bucket.remainingFraction != null && (
|
||||
<Box>
|
||||
{row.bucket.remainingFraction === 0 ? (
|
||||
<Text color={theme.status.error} wrap="truncate-end">
|
||||
Limit
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</Box>
|
||||
) : (
|
||||
<Text color={statusColor} wrap="truncate-end">
|
||||
{percentageText}
|
||||
</Text>
|
||||
)}
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
<Box
|
||||
width={resetWidth}
|
||||
flexDirection="column"
|
||||
alignItems="flex-start"
|
||||
paddingLeft={2}
|
||||
flexShrink={0}
|
||||
>
|
||||
<Text color={theme.text.secondary} wrap="truncate-end">
|
||||
{row.bucket?.resetTime &&
|
||||
formatResetTime(row.bucket.resetTime, 'column')
|
||||
? formatResetTime(row.bucket.resetTime, 'column')
|
||||
: ''}
|
||||
</Text>
|
||||
</Box>
|
||||
</>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
))}
|
||||
);
|
||||
})}
|
||||
|
||||
{cacheEfficiency > 0 && !showQuotaColumn && (
|
||||
<Box flexDirection="column" marginTop={1}>
|
||||
|
||||
@@ -21,6 +21,6 @@ exports[`QuotaDisplay > should render warning when used >= 80% 1`] = `
|
||||
`;
|
||||
|
||||
exports[`QuotaDisplay > should render with reset time when provided 1`] = `
|
||||
"85% used (Limit resets in 1 hour at 1:29 PM PST)
|
||||
"85% used (Limit resets in 1h)
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -17,10 +17,9 @@ exports[`<SessionSummaryDisplay /> > renders the summary display with a title 1`
|
||||
│ » API Time: 50.2s (100.0%) │
|
||||
│ » Tool Time: 0s (0.0%) │
|
||||
│ │
|
||||
│ Model Usage │
|
||||
│ Model Reqs Input Tokens Cache Reads Output Tokens │
|
||||
│ ──────────────────────────────────────────────────────────────────────────── │
|
||||
│ gemini-2.5-pro 10 500 500 2,000 │
|
||||
│ Model Reqs Input Tokens Cache Reads Output Tokens │
|
||||
│ ──────────────────────────────────────────────────────────────────────── │
|
||||
│ gemini-2.5-pro 10 500 500 2,000 │
|
||||
│ │
|
||||
│ Savings Highlight: 500 (50.0%) of input tokens were served from the cache, reducing costs. │
|
||||
│ │
|
||||
|
||||
@@ -117,10 +117,9 @@ exports[`<StatsDisplay /> > Conditional Rendering Tests > hides Efficiency secti
|
||||
│ » API Time: 100ms (100.0%) │
|
||||
│ » Tool Time: 0s (0.0%) │
|
||||
│ │
|
||||
│ Model Usage │
|
||||
│ Model Reqs Input Tokens Cache Reads Output Tokens │
|
||||
│ ──────────────────────────────────────────────────────────────────────────── │
|
||||
│ gemini-2.5-pro 1 100 0 100 │
|
||||
│ Model Reqs Input Tokens Cache Reads Output Tokens │
|
||||
│ ──────────────────────────────────────────────────────────────────────── │
|
||||
│ gemini-2.5-pro 1 100 0 100 │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
@@ -162,16 +161,15 @@ exports[`<StatsDisplay /> > Quota Display > renders pooled quota information for
|
||||
│ » API Time: 0s (0.0%) │
|
||||
│ » Tool Time: 0s (0.0%) │
|
||||
│ │
|
||||
│ 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 Model usage │
|
||||
│ ────────────────────────────────────────────────────────────────────────────────────────────────│
|
||||
│ gemini-2.5-pro - ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 90% used │
|
||||
│ gemini-2.5-flash - ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 30% used │
|
||||
│ Model Reqs Model usage Usage resets │
|
||||
│ ──────────────────────────────────────────────────────────────────────────────── │
|
||||
│ gemini-2.5-pro - ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 90% │
|
||||
│ gemini-2.5-flash - ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 30% │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
@@ -193,10 +191,9 @@ exports[`<StatsDisplay /> > Quota Display > renders quota information for unused
|
||||
│ » API Time: 0s (0.0%) │
|
||||
│ » Tool Time: 0s (0.0%) │
|
||||
│ │
|
||||
│ Model Usage │
|
||||
│ Model Reqs Model usage │
|
||||
│ ────────────────────────────────────────────────────────────────────────────────────────────────│
|
||||
│ gemini-2.5-flash - ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 50% used (Limit resets in 2 hours at 6:00 AM│
|
||||
│ Model Reqs Model usage Usage resets │
|
||||
│ ──────────────────────────────────────────────────────────────────────────────── │
|
||||
│ gemini-2.5-flash - ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 50% 2:00 PM (2h) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
@@ -218,10 +215,9 @@ exports[`<StatsDisplay /> > Quota Display > renders quota information when quota
|
||||
│ » API Time: 100ms (100.0%) │
|
||||
│ » Tool Time: 0s (0.0%) │
|
||||
│ │
|
||||
│ Model Usage │
|
||||
│ Model Reqs Model usage │
|
||||
│ ────────────────────────────────────────────────────────────────────────────────────────────────│
|
||||
│ gemini-2.5-pro 1 ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 25% used (Limit resets in 1 hour 30 minutes │
|
||||
│ Model Reqs Model usage Usage resets │
|
||||
│ ──────────────────────────────────────────────────────────────────────────────── │
|
||||
│ gemini-2.5-pro 1 ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 25% 1:30 PM (1h 30m) │
|
||||
│ │
|
||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
|
||||
"
|
||||
@@ -283,11 +279,10 @@ exports[`<StatsDisplay /> > renders a table with two models correctly 1`] = `
|
||||
│ » API Time: 19.5s (100.0%) │
|
||||
│ » Tool Time: 0s (0.0%) │
|
||||
│ │
|
||||
│ Model Usage │
|
||||
│ Model Reqs Input Tokens Cache Reads Output Tokens │
|
||||
│ ──────────────────────────────────────────────────────────────────────────── │
|
||||
│ gemini-2.5-pro 3 500 500 2,000 │
|
||||
│ gemini-2.5-flash 5 15,000 10,000 15,000 │
|
||||
│ Model Reqs Input Tokens Cache Reads Output Tokens │
|
||||
│ ──────────────────────────────────────────────────────────────────────── │
|
||||
│ gemini-2.5-pro 3 500 500 2,000 │
|
||||
│ gemini-2.5-flash 5 15,000 10,000 15,000 │
|
||||
│ │
|
||||
│ Savings Highlight: 10,500 (40.4%) of input tokens were served from the cache, reducing costs. │
|
||||
│ │
|
||||
@@ -312,10 +307,9 @@ exports[`<StatsDisplay /> > renders all sections when all data is present 1`] =
|
||||
│ » API Time: 100ms (44.8%) │
|
||||
│ » Tool Time: 123ms (55.2%) │
|
||||
│ │
|
||||
│ Model Usage │
|
||||
│ Model Reqs Input Tokens Cache Reads Output Tokens │
|
||||
│ ──────────────────────────────────────────────────────────────────────────── │
|
||||
│ gemini-2.5-pro 1 50 50 100 │
|
||||
│ Model Reqs Input Tokens Cache Reads Output Tokens │
|
||||
│ ──────────────────────────────────────────────────────────────────────── │
|
||||
│ gemini-2.5-pro 1 50 50 100 │
|
||||
│ │
|
||||
│ Savings Highlight: 50 (50.0%) of input tokens were served from the cache, reducing costs. │
|
||||
│ │
|
||||
|
||||
@@ -60,7 +60,6 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
originalRequestName,
|
||||
progress,
|
||||
progressTotal,
|
||||
progressPercent,
|
||||
}) => {
|
||||
const isThisShellFocused = checkIsShellFocused(
|
||||
name,
|
||||
@@ -100,7 +99,6 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
|
||||
description={description}
|
||||
emphasis={emphasis}
|
||||
progressMessage={progressMessage}
|
||||
progressPercent={progressPercent}
|
||||
originalRequestName={originalRequestName}
|
||||
/>
|
||||
<FocusHint
|
||||
|
||||
@@ -193,7 +193,6 @@ type ToolInfoProps = {
|
||||
status: CoreToolCallStatus;
|
||||
emphasis: TextEmphasis;
|
||||
progressMessage?: string;
|
||||
progressPercent?: number;
|
||||
originalRequestName?: string;
|
||||
};
|
||||
|
||||
@@ -203,7 +202,6 @@ export const ToolInfo: React.FC<ToolInfoProps> = ({
|
||||
status: coreStatus,
|
||||
emphasis,
|
||||
progressMessage: _progressMessage,
|
||||
progressPercent: _progressPercent,
|
||||
originalRequestName,
|
||||
}) => {
|
||||
const status = mapCoreStatusToDisplayStatus(coreStatus);
|
||||
|
||||
@@ -115,7 +115,6 @@ export interface IndividualToolCallDisplay {
|
||||
originalRequestName?: string;
|
||||
progress?: number;
|
||||
progressTotal?: number;
|
||||
progressPercent?: number;
|
||||
}
|
||||
|
||||
export interface CompressionProps {
|
||||
|
||||
@@ -10,9 +10,45 @@ import {
|
||||
formatBytes,
|
||||
formatTimeAgo,
|
||||
stripReferenceContent,
|
||||
formatResetTime,
|
||||
} from './formatters.js';
|
||||
|
||||
describe('formatters', () => {
|
||||
describe('formatResetTime', () => {
|
||||
const NOW = new Date('2025-01-01T12:00:00Z');
|
||||
|
||||
beforeEach(() => {
|
||||
vi.useFakeTimers();
|
||||
vi.setSystemTime(NOW);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
it('should format full time correctly', () => {
|
||||
const resetTime = new Date(NOW.getTime() + 90 * 60 * 1000).toISOString(); // 1h 30m
|
||||
const result = formatResetTime(resetTime);
|
||||
expect(result).toMatch(/1 hour 30 minutes at \d{1,2}:\d{2} [AP]M/);
|
||||
});
|
||||
|
||||
it('should format terse time correctly', () => {
|
||||
const resetTime = new Date(NOW.getTime() + 90 * 60 * 1000).toISOString(); // 1h 30m
|
||||
expect(formatResetTime(resetTime, 'terse')).toBe('1h 30m');
|
||||
});
|
||||
|
||||
it('should format column time correctly', () => {
|
||||
const resetTime = new Date(NOW.getTime() + 90 * 60 * 1000).toISOString(); // 1h 30m
|
||||
const result = formatResetTime(resetTime, 'column');
|
||||
expect(result).toMatch(/\d{1,2}:\d{2} [AP]M \(1h 30m\)/);
|
||||
});
|
||||
|
||||
it('should handle zero or negative diff by returning empty string', () => {
|
||||
const resetTime = new Date(NOW.getTime() - 1000).toISOString();
|
||||
expect(formatResetTime(resetTime)).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatBytes', () => {
|
||||
it('should format bytes into KB', () => {
|
||||
expect(formatBytes(12345)).toBe('12.1 KB');
|
||||
|
||||
@@ -100,7 +100,7 @@ export function stripReferenceContent(text: string): string {
|
||||
|
||||
export const formatResetTime = (
|
||||
resetTime: string | undefined,
|
||||
terse = false,
|
||||
format: 'terse' | 'column' | 'full' = 'full',
|
||||
): string => {
|
||||
if (!resetTime) return '';
|
||||
const resetDate = new Date(resetTime);
|
||||
@@ -113,12 +113,26 @@ export const formatResetTime = (
|
||||
const hours = Math.floor(totalMinutes / 60);
|
||||
const minutes = totalMinutes % 60;
|
||||
|
||||
if (terse) {
|
||||
const hoursStr = hours > 0 ? `${hours}hr` : '';
|
||||
const isTerse = format === 'terse';
|
||||
const isColumn = format === 'column';
|
||||
|
||||
if (isTerse || isColumn) {
|
||||
const hoursStr = hours > 0 ? `${hours}h` : '';
|
||||
const minutesStr = minutes > 0 ? `${minutes}m` : '';
|
||||
return hoursStr && minutesStr
|
||||
? `${hoursStr} ${minutesStr}`
|
||||
: hoursStr || minutesStr;
|
||||
const duration =
|
||||
hoursStr && minutesStr
|
||||
? `${hoursStr} ${minutesStr}`
|
||||
: hoursStr || minutesStr;
|
||||
|
||||
if (isColumn) {
|
||||
const timeStr = new Intl.DateTimeFormat('en-US', {
|
||||
hour: 'numeric',
|
||||
minute: 'numeric',
|
||||
}).format(resetDate);
|
||||
return duration ? `${timeStr} (${duration})` : timeStr;
|
||||
}
|
||||
|
||||
return duration;
|
||||
}
|
||||
|
||||
let duration = '';
|
||||
|
||||
Reference in New Issue
Block a user