Polish UX to be width agnostic and generic code review fixes

using /review-frontend
This commit is contained in:
jacob314
2026-03-04 00:43:08 -08:00
parent 355f5c070d
commit 077a730f25
12 changed files with 293 additions and 204 deletions
@@ -48,14 +48,16 @@ export const QuotaDisplay: React.FC<QuotaDisplayProps> = ({
let text: string; let text: string;
if (remaining === 0) { if (remaining === 0) {
const resetMsg = resetTime const resetMsg = resetTime
? `, resets in ${formatResetTime(resetTime, true)}` ? `, resets in ${formatResetTime(resetTime, 'terse')}`
: ''; : '';
text = terse ? 'Limit reached' : `Limit reached${resetMsg}`; text = terse ? 'Limit reached' : `Limit reached${resetMsg}`;
} else { } else {
text = terse text = terse
? `${usedPercentage.toFixed(0)}%` ? `${usedPercentage.toFixed(0)}%`
: `${usedPercentage.toFixed(0)}% used${ : `${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}> <Text color={color}>
{remaining === 0 {remaining === 0
? `Limit reached${ ? `Limit reached${
resetTime ? `, resets in ${formatResetTime(resetTime, true)}` : '' resetTime
? `, resets in ${formatResetTime(resetTime, 'terse')}`
: ''
}` }`
: `${usedPercentage.toFixed(0)}% used${ : `${usedPercentage.toFixed(0)}% used${
resetTime resetTime
? ` (Limit resets in ${formatResetTime(resetTime)})` ? ` (Limit resets in ${formatResetTime(resetTime, 'terse')})`
: '' : ''
}`} }`}
</Text> </Text>
@@ -68,6 +68,14 @@ const createTestMetrics = (
}); });
describe('<StatsDisplay />', () => { describe('<StatsDisplay />', () => {
beforeEach(() => {
vi.stubEnv('TZ', 'UTC');
});
afterEach(() => {
vi.unstubAllEnvs();
});
it('renders only the Performance section in its zero state', async () => { it('renders only the Performance section in its zero state', async () => {
const zeroMetrics = createTestMetrics(); const zeroMetrics = createTestMetrics();
@@ -466,8 +474,8 @@ describe('<StatsDisplay />', () => {
const output = lastFrame(); const output = lastFrame();
expect(output).toContain('Model usage'); expect(output).toContain('Model usage');
expect(output).toContain('25% used'); expect(output).toContain('25%');
expect(output).toContain('Limit resets in'); expect(output).toContain('Usage resets');
expect(output).toMatchSnapshot(); expect(output).toMatchSnapshot();
vi.useRealTimers(); vi.useRealTimers();
@@ -522,7 +530,7 @@ describe('<StatsDisplay />', () => {
const output = lastFrame(); const output = lastFrame();
// (1 - 710/1100) * 100 = 35.5% // (1 - 710/1100) * 100 = 35.5%
expect(output).toContain('35% used'); expect(output).toContain('35%');
expect(output).toContain('Usage limit: 1,100'); expect(output).toContain('Usage limit: 1,100');
expect(output).toMatchSnapshot(); expect(output).toMatchSnapshot();
@@ -571,8 +579,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% used'); expect(output).toContain('50%');
expect(output).toContain('Limit resets in'); expect(output).toContain('Usage resets');
expect(output).toMatchSnapshot(); expect(output).toMatchSnapshot();
vi.useRealTimers(); vi.useRealTimers();
+112 -73
View File
@@ -5,7 +5,7 @@
*/ */
import type React from 'react'; import type React from 'react';
import { Box, Text } from 'ink'; import { Box, Text, useStdout } from 'ink';
import { ThemedGradient } from './ThemedGradient.js'; import { ThemedGradient } from './ThemedGradient.js';
import { theme } from '../semantic-colors.js'; import { theme } from '../semantic-colors.js';
import { formatDuration, formatResetTime } from '../utils/formatters.js'; import { formatDuration, formatResetTime } from '../utils/formatters.js';
@@ -158,6 +158,8 @@ const ModelUsageTable: React.FC<{
useGemini3_1, useGemini3_1,
useCustomToolModel, useCustomToolModel,
}) => { }) => {
const { stdout } = useStdout();
const terminalWidth = stdout?.columns ?? 84;
const rows = buildModelRows(models, quotas, useGemini3_1, useCustomToolModel); const rows = buildModelRows(models, quotas, useGemini3_1, useCustomToolModel);
if (rows.length === 0) { if (rows.length === 0) {
@@ -166,15 +168,29 @@ const ModelUsageTable: React.FC<{
const showQuotaColumn = !!quotas && rows.some((row) => !!row.bucket); const showQuotaColumn = !!quotas && rows.some((row) => !!row.bucket);
const nameWidth = 25; const nameWidth = 23;
const requestsWidth = 7; const requestsWidth = 5;
const uncachedWidth = 15; const uncachedWidth = 15;
const cachedWidth = 14; const cachedWidth = 14;
const outputTokensWidth = 15; const outputTokensWidth = 15;
const usageLimitWidth = showQuotaColumn ? 85 : 0; const percentageWidth = showQuotaColumn ? 6 : 0;
const resetWidth = 22;
const renderProgressBar = (usedFraction: number, color: string) => { // Total width of other columns (including parent box paddingX={2})
const totalSteps = 20; 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); let filledSteps = Math.round(usedFraction * totalSteps);
// If something is used (fraction > 0) but rounds to 0, show 1 tick. // 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); filledSteps = Math.min(Math.max(filledSteps, 1), totalSteps - 1);
} }
const emptySteps = totalSteps - filledSteps; const emptySteps = Math.max(0, totalSteps - filledSteps);
return ( return (
<Box flexDirection="row"> <Box flexDirection="row" flexShrink={0}>
<Text wrap="truncate-end">
<Text color={color}>{'▬'.repeat(filledSteps)}</Text> <Text color={color}>{'▬'.repeat(filledSteps)}</Text>
<Text color={theme.border.default}>{'▬'.repeat(emptySteps)}</Text> <Text color={theme.border.default}>{'▬'.repeat(emptySteps)}</Text>
</Text>
</Box> </Box>
); );
}; };
@@ -201,27 +219,13 @@ const ModelUsageTable: React.FC<{
nameWidth + nameWidth +
requestsWidth + requestsWidth +
(showQuotaColumn (showQuotaColumn
? usageLimitWidth ? usageLimitWidth + percentageWidth + resetWidth
: uncachedWidth + cachedWidth + outputTokensWidth); : uncachedWidth + cachedWidth + outputTokensWidth);
const isAuto = currentModel && isAutoModel(currentModel); 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 ( return (
<Box flexDirection="column" marginBottom={1}> <Box flexDirection="column" marginBottom={1}>
{/* Header */}
<Box alignItems="flex-end">
<Box width={totalWidth}>{modelUsageTitle}</Box>
</Box>
{isAuto && {isAuto &&
showQuotaColumn && showQuotaColumn &&
pooledRemaining !== undefined && pooledRemaining !== undefined &&
@@ -240,7 +244,7 @@ const ModelUsageTable: React.FC<{
)} )}
<Box alignItems="flex-end"> <Box alignItems="flex-end">
<Box width={nameWidth}> <Box width={nameWidth} flexShrink={0}>
<Text bold color={theme.text.primary}> <Text bold color={theme.text.primary}>
Model Model
</Text> </Text>
@@ -291,16 +295,31 @@ const ModelUsageTable: React.FC<{
</> </>
)} )}
{showQuotaColumn && ( {showQuotaColumn && (
<>
<Box <Box
width={usageLimitWidth} width={usageLimitWidth}
flexDirection="column" flexDirection="column"
alignItems="flex-start" alignItems="flex-start"
paddingLeft={4} paddingLeft={4}
flexShrink={0}
> >
<Text bold color={theme.text.primary}> <Text bold color={theme.text.primary}>
Model usage Model usage
</Text> </Text>
</Box> </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> </Box>
@@ -315,9 +334,33 @@ const ModelUsageTable: React.FC<{
width={totalWidth} width={totalWidth}
></Box> ></Box>
{rows.map((row) => ( {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 key={row.key}>
<Box width={nameWidth}> <Box width={nameWidth} flexShrink={0}>
<Text <Text
color={row.isActive ? theme.text.primary : theme.text.secondary} color={row.isActive ? theme.text.primary : theme.text.secondary}
wrap="truncate-end" wrap="truncate-end"
@@ -377,68 +420,64 @@ const ModelUsageTable: React.FC<{
</Box> </Box>
</> </>
)} )}
{showQuotaColumn && (
<>
<Box <Box
width={usageLimitWidth} width={usageLimitWidth}
flexDirection="column" flexDirection="column"
alignItems="flex-start" alignItems="flex-start"
paddingLeft={4} paddingLeft={4}
flexShrink={0}
> >
{row.bucket && row.bucket.remainingFraction != null && ( {row.bucket && row.bucket.remainingFraction != null && (
<Box flexDirection="row"> <Box flexDirection="row" flexShrink={0}>
{(() => { {renderProgressBar(
const actualUsedFraction = 1 - row.bucket.remainingFraction; effectiveUsedFraction,
// If we have session activity but 0% server usage, show 0.1% as a hint. statusColor,
const effectiveUsedFraction = progressBarWidth,
actualUsedFraction === 0 && row.isActive )}
? 0.001 </Box>
: actualUsedFraction; )}
</Box>
const usedPercentage = effectiveUsedFraction * 100; <Box
width={percentageWidth}
const statusColor = flexDirection="column"
getUsedStatusColor(usedPercentage, { alignItems="flex-end"
warning: QUOTA_USED_WARNING_THRESHOLD, flexShrink={0}
critical: QUOTA_USED_CRITICAL_THRESHOLD, >
}) ?? {row.bucket && row.bucket.remainingFraction != null && (
(row.isActive ? theme.text.primary : theme.ui.comment); <Box>
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 ? ( {row.bucket.remainingFraction === 0 ? (
<Text color={theme.status.error}> <Text color={theme.status.error} wrap="truncate-end">
Limit reached Limit
{row.bucket.resetTime &&
`, resets in ${formatResetTime(row.bucket.resetTime, true)}`}
</Text> </Text>
) : ( ) : (
<> <Text color={statusColor} wrap="truncate-end">
<Text color={statusColor}>{percentageText}</Text> {percentageText}
<Text color={theme.text.secondary}> </Text>
{row.bucket.resetTime && )}
formatResetTime(row.bucket.resetTime) </Box>
? ` (Limit resets in ${formatResetTime(row.bucket.resetTime)})` )}
</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> </Text>
</>
)}
</Text>
</Box> </Box>
</> </>
)}
</Box>
); );
})()} })}
</Box>
)}
</Box>
</Box>
))}
{cacheEfficiency > 0 && !showQuotaColumn && ( {cacheEfficiency > 0 && !showQuotaColumn && (
<Box flexDirection="column" marginTop={1}> <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`] = ` 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,9 +17,8 @@ exports[`<SessionSummaryDisplay /> > renders the summary display with a title 1`
│ » API Time: 50.2s (100.0%) │ │ » API Time: 50.2s (100.0%) │
│ » Tool Time: 0s (0.0%) │ │ » Tool Time: 0s (0.0%) │
│ │ │ │
│ Model Usage │
│ Model Reqs Input Tokens Cache Reads Output Tokens │ │ Model Reqs Input Tokens Cache Reads Output Tokens │
│ ──────────────────────────────────────────────────────────────────────────── │ ────────────────────────────────────────────────────────────────────────
│ gemini-2.5-pro 10 500 500 2,000 │ │ gemini-2.5-pro 10 500 500 2,000 │
│ │ │ │
│ Savings Highlight: 500 (50.0%) of input tokens were served from the cache, reducing costs. │ │ Savings Highlight: 500 (50.0%) of input tokens were served from the cache, reducing costs. │
@@ -117,9 +117,8 @@ exports[`<StatsDisplay /> > Conditional Rendering Tests > hides Efficiency secti
│ » API Time: 100ms (100.0%) │ │ » API Time: 100ms (100.0%) │
│ » Tool Time: 0s (0.0%) │ │ » Tool Time: 0s (0.0%) │
│ │ │ │
│ Model Usage │
│ Model Reqs Input Tokens Cache Reads Output Tokens │ │ Model Reqs Input Tokens Cache Reads Output Tokens │
│ ──────────────────────────────────────────────────────────────────────────── │ ────────────────────────────────────────────────────────────────────────
│ gemini-2.5-pro 1 100 0 100 │ │ 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%) │ │ » API Time: 0s (0.0%) │
│ » Tool Time: 0s (0.0%) │ │ » Tool Time: 0s (0.0%) │
│ │ │ │
│ Model Usage: auto │
│ 35% used │ │ 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 Model usage │ Model Reqs Model usage Usage resets
│ ──────────────────────────────────────────────────────────────────────────────────────────────── │ ────────────────────────────────────────────────────────────────────────────────
│ gemini-2.5-pro - ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 90% used │ gemini-2.5-pro - ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 90%
│ gemini-2.5-flash - ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 30% used │ gemini-2.5-flash - ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 30%
│ │ │ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
" "
@@ -193,10 +191,9 @@ exports[`<StatsDisplay /> > Quota Display > renders quota information for unused
│ » API Time: 0s (0.0%) │ │ » API Time: 0s (0.0%) │
│ » Tool Time: 0s (0.0%) │ │ » Tool Time: 0s (0.0%) │
│ │ │ │
│ Model Usage │ Model Reqs Model usage Usage resets
Model Reqs Model usage ────────────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────────────────────────── gemini-2.5-flash - ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 50% 2:00 PM (2h)
│ gemini-2.5-flash - ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 50% used (Limit resets in 2 hours at 6:00 AM│
│ │ │ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
" "
@@ -218,10 +215,9 @@ exports[`<StatsDisplay /> > Quota Display > renders quota information when quota
│ » API Time: 100ms (100.0%) │ │ » API Time: 100ms (100.0%) │
│ » Tool Time: 0s (0.0%) │ │ » Tool Time: 0s (0.0%) │
│ │ │ │
│ Model Usage │ Model Reqs Model usage Usage resets
Model Reqs Model usage ────────────────────────────────────────────────────────────────────────────────
──────────────────────────────────────────────────────────────────────────────────────────────── gemini-2.5-pro 1 ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 25% 1:30 PM (1h 30m)
│ gemini-2.5-pro 1 ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 25% used (Limit resets in 1 hour 30 minutes │
│ │ │ │
╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯
" "
@@ -283,9 +279,8 @@ exports[`<StatsDisplay /> > renders a table with two models correctly 1`] = `
│ » API Time: 19.5s (100.0%) │ │ » API Time: 19.5s (100.0%) │
│ » Tool Time: 0s (0.0%) │ │ » Tool Time: 0s (0.0%) │
│ │ │ │
│ Model Usage │
│ Model Reqs Input Tokens Cache Reads Output Tokens │ │ Model Reqs Input Tokens Cache Reads Output Tokens │
│ ──────────────────────────────────────────────────────────────────────────── │ ────────────────────────────────────────────────────────────────────────
│ gemini-2.5-pro 3 500 500 2,000 │ │ gemini-2.5-pro 3 500 500 2,000 │
│ gemini-2.5-flash 5 15,000 10,000 15,000 │ │ gemini-2.5-flash 5 15,000 10,000 15,000 │
│ │ │ │
@@ -312,9 +307,8 @@ exports[`<StatsDisplay /> > renders all sections when all data is present 1`] =
│ » API Time: 100ms (44.8%) │ │ » API Time: 100ms (44.8%) │
│ » Tool Time: 123ms (55.2%) │ │ » Tool Time: 123ms (55.2%) │
│ │ │ │
│ Model Usage │
│ Model Reqs Input Tokens Cache Reads Output Tokens │ │ Model Reqs Input Tokens Cache Reads Output Tokens │
│ ──────────────────────────────────────────────────────────────────────────── │ ────────────────────────────────────────────────────────────────────────
│ gemini-2.5-pro 1 50 50 100 │ │ gemini-2.5-pro 1 50 50 100 │
│ │ │ │
│ Savings Highlight: 50 (50.0%) of input tokens were served from the cache, reducing costs. │ │ 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, originalRequestName,
progress, progress,
progressTotal, progressTotal,
progressPercent,
}) => { }) => {
const isThisShellFocused = checkIsShellFocused( const isThisShellFocused = checkIsShellFocused(
name, name,
@@ -100,7 +99,6 @@ export const ToolMessage: React.FC<ToolMessageProps> = ({
description={description} description={description}
emphasis={emphasis} emphasis={emphasis}
progressMessage={progressMessage} progressMessage={progressMessage}
progressPercent={progressPercent}
originalRequestName={originalRequestName} originalRequestName={originalRequestName}
/> />
<FocusHint <FocusHint
@@ -193,7 +193,6 @@ type ToolInfoProps = {
status: CoreToolCallStatus; status: CoreToolCallStatus;
emphasis: TextEmphasis; emphasis: TextEmphasis;
progressMessage?: string; progressMessage?: string;
progressPercent?: number;
originalRequestName?: string; originalRequestName?: string;
}; };
@@ -203,7 +202,6 @@ export const ToolInfo: React.FC<ToolInfoProps> = ({
status: coreStatus, status: coreStatus,
emphasis, emphasis,
progressMessage: _progressMessage, progressMessage: _progressMessage,
progressPercent: _progressPercent,
originalRequestName, originalRequestName,
}) => { }) => {
const status = mapCoreStatusToDisplayStatus(coreStatus); const status = mapCoreStatusToDisplayStatus(coreStatus);
-1
View File
@@ -115,7 +115,6 @@ export interface IndividualToolCallDisplay {
originalRequestName?: string; originalRequestName?: string;
progress?: number; progress?: number;
progressTotal?: number; progressTotal?: number;
progressPercent?: number;
} }
export interface CompressionProps { export interface CompressionProps {
@@ -10,9 +10,45 @@ import {
formatBytes, formatBytes,
formatTimeAgo, formatTimeAgo,
stripReferenceContent, stripReferenceContent,
formatResetTime,
} from './formatters.js'; } from './formatters.js';
describe('formatters', () => { 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', () => { describe('formatBytes', () => {
it('should format bytes into KB', () => { it('should format bytes into KB', () => {
expect(formatBytes(12345)).toBe('12.1 KB'); expect(formatBytes(12345)).toBe('12.1 KB');
+18 -4
View File
@@ -100,7 +100,7 @@ export function stripReferenceContent(text: string): string {
export const formatResetTime = ( export const formatResetTime = (
resetTime: string | undefined, resetTime: string | undefined,
terse = false, format: 'terse' | 'column' | 'full' = 'full',
): string => { ): string => {
if (!resetTime) return ''; if (!resetTime) return '';
const resetDate = new Date(resetTime); const resetDate = new Date(resetTime);
@@ -113,12 +113,26 @@ export const formatResetTime = (
const hours = Math.floor(totalMinutes / 60); const hours = Math.floor(totalMinutes / 60);
const minutes = totalMinutes % 60; const minutes = totalMinutes % 60;
if (terse) { const isTerse = format === 'terse';
const hoursStr = hours > 0 ? `${hours}hr` : ''; const isColumn = format === 'column';
if (isTerse || isColumn) {
const hoursStr = hours > 0 ? `${hours}h` : '';
const minutesStr = minutes > 0 ? `${minutes}m` : ''; const minutesStr = minutes > 0 ? `${minutes}m` : '';
return hoursStr && minutesStr const duration =
hoursStr && minutesStr
? `${hoursStr} ${minutesStr}` ? `${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 = ''; let duration = '';