From 29caa52bb7b50e580b8eaa47be010cf54dad0b06 Mon Sep 17 00:00:00 2001 From: Dan Zaharia <159156373+danzaharia1@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:49:14 -0400 Subject: [PATCH] fix(ui): hide model quota in /stats and refactor quota display (#24206) --- .../src/ui/components/HistoryItemDisplay.tsx | 14 - .../src/ui/components/ModelDialog.test.tsx | 10 + .../cli/src/ui/components/ModelDialog.tsx | 13 +- .../ui/components/ModelQuotaDisplay.test.tsx | 86 +++ .../src/ui/components/ModelQuotaDisplay.tsx | 219 +++++++ .../src/ui/components/ProgressBar.test.tsx | 39 ++ .../cli/src/ui/components/ProgressBar.tsx | 39 ++ .../src/ui/components/StatsDisplay.test.tsx | 186 +----- .../cli/src/ui/components/StatsDisplay.tsx | 541 +++--------------- .../ModelQuotaDisplay.test.tsx.snap | 10 + .../__snapshots__/ProgressBar.test.tsx.snap | 21 + .../SessionSummaryDisplay.test.tsx.snap | 9 +- .../__snapshots__/StatsDisplay.test.tsx.snap | 107 +--- ...mixed-content-lengths-correctly-2.snap.svg | 401 +++++++++++++ .../__snapshots__/TableRenderer.test.tsx.snap | 39 ++ 15 files changed, 986 insertions(+), 748 deletions(-) create mode 100644 packages/cli/src/ui/components/ModelQuotaDisplay.test.tsx create mode 100644 packages/cli/src/ui/components/ModelQuotaDisplay.tsx create mode 100644 packages/cli/src/ui/components/ProgressBar.test.tsx create mode 100644 packages/cli/src/ui/components/ProgressBar.tsx create mode 100644 packages/cli/src/ui/components/__snapshots__/ModelQuotaDisplay.test.tsx.snap create mode 100644 packages/cli/src/ui/components/__snapshots__/ProgressBar.test.tsx.snap create mode 100644 packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-complex-table-with-mixed-content-lengths-correctly-2.snap.svg diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx index dc98af93e8..cd978b7952 100644 --- a/packages/cli/src/ui/components/HistoryItemDisplay.tsx +++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx @@ -164,23 +164,9 @@ export const HistoryItemDisplay: React.FC = ({ {itemForDisplay.type === 'stats' && ( )} {itemForDisplay.type === 'model_stats' && ( diff --git a/packages/cli/src/ui/components/ModelDialog.test.tsx b/packages/cli/src/ui/components/ModelDialog.test.tsx index e40f39befc..e5796727f3 100644 --- a/packages/cli/src/ui/components/ModelDialog.test.tsx +++ b/packages/cli/src/ui/components/ModelDialog.test.tsx @@ -65,6 +65,15 @@ describe('', () => { getGemini31FlashLiteLaunchedSync: () => boolean; getProModelNoAccess: () => Promise; getProModelNoAccessSync: () => boolean; + getLastRetrievedQuota: () => + | { + buckets: Array<{ + modelId?: string; + remainingFraction?: number; + resetTime?: string; + }>; + } + | undefined; } const mockConfig: MockConfig = { @@ -76,6 +85,7 @@ describe('', () => { getGemini31FlashLiteLaunchedSync: mockGetGemini31FlashLiteLaunchedSync, getProModelNoAccess: mockGetProModelNoAccess, getProModelNoAccessSync: mockGetProModelNoAccessSync, + getLastRetrievedQuota: () => ({ buckets: [] }), }; beforeEach(() => { diff --git a/packages/cli/src/ui/components/ModelDialog.tsx b/packages/cli/src/ui/components/ModelDialog.tsx index 8724799a94..d38bd79b9f 100644 --- a/packages/cli/src/ui/components/ModelDialog.tsx +++ b/packages/cli/src/ui/components/ModelDialog.tsx @@ -7,6 +7,8 @@ import type React from 'react'; import { useCallback, useContext, useMemo, useState, useEffect } from 'react'; import { Box, Text } from 'ink'; +import { ModelQuotaDisplay } from './ModelQuotaDisplay.js'; +import { useUIState } from '../contexts/UIStateContext.js'; import { PREVIEW_GEMINI_MODEL, PREVIEW_GEMINI_3_1_MODEL, @@ -37,6 +39,7 @@ interface ModelDialogProps { export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element { const config = useContext(ConfigContext); const settings = useSettings(); + const { terminalWidth } = useUIState(); const [hasAccessToProModel, setHasAccessToProModel] = useState( () => !(config?.getProModelNoAccessSync() ?? false), ); @@ -338,20 +341,24 @@ export function ModelDialog({ onClose }: ModelDialogProps): React.JSX.Element { - + Remember model for future sessions:{' '} {persistMode ? 'true' : 'false'} + (Press Tab to toggle) - (Press Tab to toggle) - + {'> To use a specific Gemini model on startup, use the --model flag.'} + (Press Esc to close) diff --git a/packages/cli/src/ui/components/ModelQuotaDisplay.test.tsx b/packages/cli/src/ui/components/ModelQuotaDisplay.test.tsx new file mode 100644 index 0000000000..422806a22b --- /dev/null +++ b/packages/cli/src/ui/components/ModelQuotaDisplay.test.tsx @@ -0,0 +1,86 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { renderWithProviders } from '../../test-utils/render.js'; +import { describe, it, expect, vi } from 'vitest'; +import { ModelQuotaDisplay } from './ModelQuotaDisplay.js'; + +describe('', () => { + beforeEach(() => { + vi.stubEnv('TZ', 'UTC'); + }); + + afterEach(() => { + vi.unstubAllEnvs(); + }); + + it('renders quota information when buckets are provided', async () => { + const now = new Date('2025-01-01T12:00:00Z'); + vi.useFakeTimers(); + vi.setSystemTime(now); + + const resetTime = new Date(now.getTime() + 1000 * 60 * 90).toISOString(); // 1 hour 30 minutes from now + + const buckets = [ + { + modelId: 'gemini-2.5-pro', + remainingFraction: 0.75, + resetTime, + }, + ]; + + const { lastFrame } = await renderWithProviders( + , + { width: 100 }, + ); + const output = lastFrame(); + + expect(output).toContain('Model usage'); + expect(output).toContain('Pro'); + expect(output).toContain('25%'); + expect(output).toContain('Resets:'); + expect(output).toMatchSnapshot(); + + vi.useRealTimers(); + }); + + it('renders nothing when no buckets are provided', async () => { + const { lastFrame } = await renderWithProviders( + , + { width: 100 }, + ); + const output = lastFrame({ allowEmpty: true }); + expect(output).toBe(''); + }); + + it('filters models based on modelsToShow prop', async () => { + const buckets = [ + { + modelId: 'gemini-2.5-pro', + remainingFraction: 0.5, + resetTime: new Date().toISOString(), + }, + { + modelId: 'gemini-2.5-flash', + remainingFraction: 0.8, + resetTime: new Date().toISOString(), + }, + ]; + + const { lastFrame } = await renderWithProviders( + , + { width: 100 }, + ); + const output = lastFrame(); + + expect(output).toContain('Pro'); + expect(output).not.toContain('Flash'); + }); +}); diff --git a/packages/cli/src/ui/components/ModelQuotaDisplay.tsx b/packages/cli/src/ui/components/ModelQuotaDisplay.tsx new file mode 100644 index 0000000000..f967b2861f --- /dev/null +++ b/packages/cli/src/ui/components/ModelQuotaDisplay.tsx @@ -0,0 +1,219 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { useMemo } from 'react'; +import { Box, Text } from 'ink'; +import { ProgressBar } from './ProgressBar.js'; +import { theme } from '../semantic-colors.js'; +import { formatResetTime } from '../utils/formatters.js'; +import { getDisplayString } from '@google/gemini-cli-core'; +import { useConfig } from '../contexts/ConfigContext.js'; +import { useUIState } from '../contexts/UIStateContext.js'; + +interface LocalBucket { + modelId?: string; + remainingFraction?: number; + resetTime?: string; +} + +interface ModelQuotaDisplayProps { + buckets?: LocalBucket[]; + availableWidth?: number; + modelsToShow?: string[]; + title?: string; +} + +interface ModelUsageRowProps { + row: { + modelId: string; + name: string; + usedFraction: number; + usedPercentage: number; + resetTime?: string; + }; + availableWidth?: number; +} + +const ModelUsageRow = ({ row, availableWidth }: ModelUsageRowProps) => { + const { terminalWidth } = useUIState(); + + const nameColumnWidth = 12; + const percentageWidth = 4; + const resetColumnWidth = 26; + const usedPercentage = row.usedPercentage; + + const nameLabel = row.name; + const percentageLabel = `${usedPercentage.toFixed(0)}%`.padEnd( + percentageWidth, + ); + const resetLabel = row.resetTime + ? formatResetTime(row.resetTime, 'column') + .slice(0, resetColumnWidth) + .padEnd(resetColumnWidth) + : ''.padEnd(resetColumnWidth); + + // Calculate the exact width of all fixed adjacent siblings + const nameColWidth = nameColumnWidth; + const percentColWidth = percentageWidth + 1; // width + marginLeft + const resetColWidth = resetColumnWidth + 1; // width + marginLeft + + const fixedSiblingWidth = nameColWidth + percentColWidth + resetColWidth; + + const calcWidth = availableWidth ?? terminalWidth; + const defaultPadding = availableWidth != null ? 0 : 4; + + // Subtract fixed sibling widths from total width. + // We keep a small buffer (e.g., 3) to prevent edge-case wrapping. + const buffer = 3; + const barWidth = Math.max( + 0, + calcWidth - defaultPadding - fixedSiblingWidth - buffer, + ); + + let percentageColor = theme.text.primary; + if (usedPercentage >= 100) { + percentageColor = theme.status.error; + } else if (usedPercentage >= 80) { + percentageColor = theme.status.warning; + } + + return ( + + + + {nameLabel} + + + + + + + + + {percentageLabel} + + + + + {resetLabel.trim() ? `Resets: ${resetLabel}` : ''} + + + + ); +}; + +export const ModelQuotaDisplay = ({ + buckets, + availableWidth, + modelsToShow = ['all'], + title = 'Model usage', +}: ModelQuotaDisplayProps) => { + const config = useConfig(); + + const modelsWithQuotas = useMemo(() => { + if (!buckets) return []; + + let filteredBuckets = buckets.filter( + (b) => b.modelId && b.remainingFraction != null, + ); + + if (modelsToShow.includes('current')) { + const currentModel = config.getActiveModel?.() ?? config.getModel?.(); + filteredBuckets = filteredBuckets.filter( + (b) => b.modelId === currentModel, + ); + } else if (!modelsToShow.includes('all')) { + filteredBuckets = filteredBuckets.filter( + (b) => b.modelId && modelsToShow.includes(b.modelId), + ); + } + + const groupedByTier = new Map< + string, + { + modelId: string; + remainingFraction: number; + resetTime?: string; + name: string; + } + >(); + + filteredBuckets.forEach((b) => { + const modelId = b.modelId; + const remainingFraction = b.remainingFraction; + if (!modelId || remainingFraction == null) return; + + const tier = + config?.modelConfigService?.getModelDefinition(modelId)?.tier; + const groupKey = tier ?? modelId; + const existing = groupedByTier.get(groupKey); + + if (!existing || remainingFraction < existing.remainingFraction) { + const tierDisplayNames: Record = { + pro: 'Pro', + flash: 'Flash', + 'flash-lite': 'Flash Lite', + }; + const name = tier + ? (tierDisplayNames[tier] ?? tier) + : getDisplayString(modelId, config); + + groupedByTier.set(groupKey, { + modelId, + remainingFraction, + resetTime: b.resetTime, + name, + }); + } + }); + + return Array.from(groupedByTier.entries()).map(([key, data]) => { + const usedFraction = 1 - data.remainingFraction; + const usedPercentage = usedFraction * 100; + return { + modelId: key, + name: data.name, + usedFraction, + usedPercentage, + resetTime: data.resetTime, + }; + }); + }, [buckets, config, modelsToShow]); + + if (modelsWithQuotas.length === 0) { + return null; + } + + return ( + + {/* Rule Line */} + + + + + + {title} + + + + {modelsWithQuotas.map((row) => ( + + ))} + + + ); +}; diff --git a/packages/cli/src/ui/components/ProgressBar.test.tsx b/packages/cli/src/ui/components/ProgressBar.test.tsx new file mode 100644 index 0000000000..9040f56245 --- /dev/null +++ b/packages/cli/src/ui/components/ProgressBar.test.tsx @@ -0,0 +1,39 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { describe, it, expect } from 'vitest'; +import { ProgressBar } from './ProgressBar.js'; +import { renderWithProviders } from '../../test-utils/render.js'; + +describe('', () => { + it('renders 0% correctly', async () => { + const { lastFrame } = await renderWithProviders( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); + + it('renders 50% correctly', async () => { + const { lastFrame } = await renderWithProviders( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); + + it('renders warning threshold correctly', async () => { + const { lastFrame } = await renderWithProviders( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); + + it('renders error threshold correctly at 100%', async () => { + const { lastFrame } = await renderWithProviders( + , + ); + expect(lastFrame()).toMatchSnapshot(); + }); +}); diff --git a/packages/cli/src/ui/components/ProgressBar.tsx b/packages/cli/src/ui/components/ProgressBar.tsx new file mode 100644 index 0000000000..8c2c456645 --- /dev/null +++ b/packages/cli/src/ui/components/ProgressBar.tsx @@ -0,0 +1,39 @@ +/** + * @license + * Copyright 2026 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type React from 'react'; +import { Box, Text } from 'ink'; +import { theme } from '../semantic-colors.js'; + +interface ProgressBarProps { + value: number; // 0 to 100 + width: number; + warningThreshold?: number; +} + +export const ProgressBar: React.FC = ({ + value, + width, + warningThreshold = 80, +}) => { + const safeValue = Math.min(Math.max(value, 0), 100); + const activeChars = Math.ceil((safeValue / 100) * width); + const inactiveChars = width - activeChars; + + let color = theme.status.success; + if (safeValue >= 100) { + color = theme.status.error; + } else if (safeValue >= warningThreshold) { + color = theme.status.warning; + } + + return ( + + {'▬'.repeat(activeChars)} + {'▬'.repeat(inactiveChars)} + + ); +}; diff --git a/packages/cli/src/ui/components/StatsDisplay.test.tsx b/packages/cli/src/ui/components/StatsDisplay.test.tsx index 8c979afcc6..b34bf60298 100644 --- a/packages/cli/src/ui/components/StatsDisplay.test.tsx +++ b/packages/cli/src/ui/components/StatsDisplay.test.tsx @@ -9,10 +9,7 @@ import { describe, it, expect, vi } from 'vitest'; import { StatsDisplay } from './StatsDisplay.js'; import * as SessionContext from '../contexts/SessionContext.js'; import { type SessionMetrics } from '../contexts/SessionContext.js'; -import { - ToolCallDecision, - type RetrieveUserQuotaResponse, -} from '@google/gemini-cli-core'; +import { ToolCallDecision } from '@google/gemini-cli-core'; // Mock the context to provide controlled data for testing vi.mock('../contexts/SessionContext.js', async (importOriginal) => { @@ -124,10 +121,13 @@ describe('', () => { const { lastFrame } = await renderWithMockedStats(metrics); const output = lastFrame(); - expect(output).toContain('gemini-2.5-pro'); - expect(output).toContain('gemini-2.5-flash'); - expect(output).toContain('15,000'); - expect(output).toContain('10,000'); + expect(output).toContain('Performance'); + expect(output).toContain('Interaction Summary'); + expect(output).toContain('Model Usage'); + expect(output).toContain('Reqs'); + expect(output).toContain('Input Tokens'); + expect(output).toContain('Cache Reads'); + expect(output).toContain('Output Tokens'); expect(output).toMatchSnapshot(); }); @@ -182,7 +182,7 @@ describe('', () => { expect(output).toContain('Performance'); expect(output).toContain('Interaction Summary'); expect(output).toContain('User Agreement'); - expect(output).toContain('gemini-2.5-pro'); + expect(output).toContain('Model Usage'); expect(output).toMatchSnapshot(); }); @@ -406,174 +406,6 @@ describe('', () => { }); }); - describe('Quota Display', () => { - it('renders quota information when quotas are provided', async () => { - const now = new Date('2025-01-01T12:00:00Z'); - vi.useFakeTimers(); - vi.setSystemTime(now); - - const metrics = createTestMetrics({ - models: { - 'gemini-2.5-pro': { - api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 100 }, - tokens: { - input: 50, - prompt: 100, - candidates: 100, - total: 250, - cached: 50, - thoughts: 0, - tool: 0, - }, - roles: {}, - }, - }, - }); - - const resetTime = new Date(now.getTime() + 1000 * 60 * 90).toISOString(); // 1 hour 30 minutes from now - - const quotas: RetrieveUserQuotaResponse = { - buckets: [ - { - modelId: 'gemini-2.5-pro', - remainingAmount: '75', - remainingFraction: 0.75, - resetTime, - }, - ], - }; - - useSessionStatsMock.mockReturnValue({ - stats: { - sessionId: 'test-session-id', - sessionStartTime: new Date(), - metrics, - lastPromptTokenCount: 0, - promptCount: 5, - }, - - getPromptCount: () => 5, - startNewPrompt: vi.fn(), - }); - - const { lastFrame } = await renderWithProviders( - , - { width: 100 }, - ); - const output = lastFrame(); - - expect(output).toContain('Model usage'); - expect(output).toContain('25%'); - expect(output).toContain('Usage resets'); - expect(output).toMatchSnapshot(); - - vi.useRealTimers(); - }); - - it('renders pooled quota information for auto mode', async () => { - const now = new Date('2025-01-01T12:00:00Z'); - vi.useFakeTimers(); - vi.setSystemTime(now); - - const metrics = createTestMetrics(); - const quotas: RetrieveUserQuotaResponse = { - buckets: [ - { - modelId: 'gemini-2.5-pro', - remainingAmount: '10', - remainingFraction: 0.1, // limit = 100 - }, - { - modelId: 'gemini-2.5-flash', - remainingAmount: '700', - remainingFraction: 0.7, // limit = 1000 - }, - ], - }; - - useSessionStatsMock.mockReturnValue({ - stats: { - sessionId: 'test-session-id', - sessionStartTime: new Date(), - metrics, - lastPromptTokenCount: 0, - promptCount: 5, - }, - getPromptCount: () => 5, - startNewPrompt: vi.fn(), - }); - - const { lastFrame } = await renderWithProviders( - , - { width: 100 }, - ); - const output = lastFrame(); - - // (1 - 710/1100) * 100 = 35.5% - expect(output).toContain('35%'); - expect(output).toContain('Usage limit: 1,100'); - expect(output).toMatchSnapshot(); - - vi.useRealTimers(); - }); - - it('renders quota information for unused models', async () => { - const now = new Date('2025-01-01T12:00:00Z'); - vi.useFakeTimers(); - vi.setSystemTime(now); - - // No models in metrics, but a quota for gemini-2.5-flash - const metrics = createTestMetrics(); - - const resetTime = new Date(now.getTime() + 1000 * 60 * 120).toISOString(); // 2 hours from now - - const quotas: RetrieveUserQuotaResponse = { - buckets: [ - { - modelId: 'gemini-2.5-flash', - remainingAmount: '50', - remainingFraction: 0.5, - resetTime, - }, - ], - }; - - useSessionStatsMock.mockReturnValue({ - stats: { - sessionId: 'test-session-id', - sessionStartTime: new Date(), - metrics, - lastPromptTokenCount: 0, - promptCount: 5, - }, - getPromptCount: () => 5, - startNewPrompt: vi.fn(), - }); - - const { lastFrame } = await renderWithProviders( - , - { width: 100 }, - ); - const output = lastFrame(); - - expect(output).toContain('gemini-2.5-flash'); - expect(output).toContain('-'); // for requests - expect(output).toContain('50%'); - expect(output).toContain('Usage resets'); - expect(output).toMatchSnapshot(); - - vi.useRealTimers(); - }); - }); - describe('User Identity Display', () => { it('renders User row with Auth Method and Tier', async () => { const metrics = createTestMetrics(); diff --git a/packages/cli/src/ui/components/StatsDisplay.tsx b/packages/cli/src/ui/components/StatsDisplay.tsx index 5e1291b97a..4668a7a5a7 100644 --- a/packages/cli/src/ui/components/StatsDisplay.tsx +++ b/packages/cli/src/ui/components/StatsDisplay.tsx @@ -5,10 +5,10 @@ */ import type React from 'react'; -import { Box, Text, useStdout } from 'ink'; +import { Box, Text } from 'ink'; import { ThemedGradient } from './ThemedGradient.js'; import { theme } from '../semantic-colors.js'; -import { formatDuration, formatResetTime } from '../utils/formatters.js'; +import { formatDuration } from '../utils/formatters.js'; import { useSessionStats, type ModelMetrics, @@ -19,25 +19,10 @@ import { TOOL_SUCCESS_RATE_MEDIUM, USER_AGREEMENT_RATE_HIGH, 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 { - type Config, - type RetrieveUserQuotaResponse, - isActiveModel, - getDisplayString, - isAutoModel, - AuthType, -} from '@google/gemini-cli-core'; import { useSettings } from '../contexts/SettingsContext.js'; -import { useConfig } from '../contexts/ConfigContext.js'; import type { QuotaStats } from '../types.js'; -import { QuotaStatsInfo } from './QuotaStatsInfo.js'; // A more flexible and powerful StatRow component interface StatRowProps { @@ -87,433 +72,94 @@ const Section: React.FC = ({ title, children }) => ( ); // Logic for building the unified list of table rows -const buildModelRows = ( - models: Record, - config: Config, - quotas?: RetrieveUserQuotaResponse, - useGemini3_1 = false, - useGemini3_1FlashLite = false, - useCustomToolModel = false, -) => { - const getBaseModelName = (name: string) => name.replace('-001', ''); - const usedModelNames = new Set( - Object.keys(models) - .map(getBaseModelName) - .map((name) => getDisplayString(name, config)), - ); - // 1. Models with active usage - const activeRows = Object.entries(models).map(([name, metrics]) => { - const modelName = getBaseModelName(name); - const cachedTokens = metrics.tokens.cached; - const inputTokens = metrics.tokens.input; - return { - key: name, - modelName: getDisplayString(modelName, config), - requests: metrics.api.totalRequests, - cachedTokens: cachedTokens.toLocaleString(), - inputTokens: inputTokens.toLocaleString(), - outputTokens: metrics.tokens.candidates.toLocaleString(), - bucket: quotas?.buckets?.find((b) => b.modelId === modelName), - isActive: true, - }; - }); - - // 2. Models with quota only - const quotaRows = - quotas?.buckets - ?.filter( - (b) => - b.modelId && - isActiveModel( - b.modelId, - useGemini3_1, - useGemini3_1FlashLite, - useCustomToolModel, - ) && - !usedModelNames.has(getDisplayString(b.modelId, config)), - ) - .map((bucket) => ({ - key: bucket.modelId!, - modelName: getDisplayString(bucket.modelId!, config), - requests: '-', - cachedTokens: '-', - inputTokens: '-', - outputTokens: '-', - bucket, - isActive: false, - })) || []; - - return [...activeRows, ...quotaRows]; -}; - -const ModelUsageTable: React.FC<{ +interface ModelUsageTableProps { models: Record; - config: Config; - quotas?: RetrieveUserQuotaResponse; - cacheEfficiency: number; - totalCachedTokens: number; - currentModel?: string; - pooledRemaining?: number; - pooledLimit?: number; - pooledResetTime?: string; - useGemini3_1?: boolean; - useGemini3_1FlashLite?: boolean; - useCustomToolModel?: boolean; -}> = ({ - models, - config, - quotas, - cacheEfficiency, - totalCachedTokens, - currentModel, - pooledRemaining, - pooledLimit, - pooledResetTime, - useGemini3_1, - useGemini3_1FlashLite, - useCustomToolModel, -}) => { - const { stdout } = useStdout(); - const terminalWidth = stdout?.columns ?? 84; - const rows = buildModelRows( - models, - config, - quotas, - useGemini3_1, - useGemini3_1FlashLite, - useCustomToolModel, - ); +} - if (rows.length === 0) { - return null; - } - - const showQuotaColumn = !!quotas && rows.some((row) => !!row.bucket); - - const nameWidth = 23; - const requestsWidth = 5; - const uncachedWidth = 15; - const cachedWidth = 14; - const outputTokensWidth = 15; - const percentageWidth = showQuotaColumn ? 6 : 0; - const resetWidth = 22; - - // 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. - // 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 = Math.max(0, totalSteps - filledSteps); - return ( - - - {'▬'.repeat(filledSteps)} - {'▬'.repeat(emptySteps)} - - - ); - }; - - const cacheEfficiencyColor = getStatusColor(cacheEfficiency, { - green: CACHE_EFFICIENCY_HIGH, - yellow: CACHE_EFFICIENCY_MEDIUM, - }); - - const totalWidth = - nameWidth + - requestsWidth + - (showQuotaColumn - ? usageLimitWidth + percentageWidth + resetWidth - : uncachedWidth + cachedWidth + outputTokensWidth); - - const isAuto = currentModel && isAutoModel(currentModel); +const ModelUsageTable: React.FC = ({ models }) => { + const nameWidth = 28; + const requestsWidth = 8; + const inputTokensWidth = 14; + const cacheReadsWidth = 14; + const outputTokensWidth = 14; return ( - - {isAuto && - showQuotaColumn && - pooledRemaining !== undefined && - pooledLimit !== undefined && - pooledLimit > 0 && ( - - - - For a full token breakdown, run `/stats model`. - - - )} + + + Model Usage + + + Use /model to view model quota information + + - - - - Model - - - - - Reqs - - - - {!showQuotaColumn && ( - <> - - - Input Tokens - - - - - Cache Reads - - - - - Output Tokens - - - - )} - {showQuotaColumn && ( - <> - - - Model usage - - - - - - Usage resets - - - - )} - - - {/* Divider */} + {/* Header */} - - {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 ( - - - - {row.modelName} - - - - - {row.requests} - - - {!showQuotaColumn && ( - <> - - - {row.inputTokens} - - - - {row.cachedTokens} - - - - {row.outputTokens} - - - - )} - {showQuotaColumn && ( - <> - - {row.bucket && row.bucket.remainingFraction != null && ( - - {renderProgressBar( - effectiveUsedFraction, - statusColor, - progressBarWidth, - )} - - )} - - - {row.bucket && row.bucket.remainingFraction != null && ( - - {row.bucket.remainingFraction === 0 ? ( - - Limit - - ) : ( - - {percentageText} - - )} - - )} - - - - {row.bucket?.resetTime && - formatResetTime(row.bucket.resetTime, 'column') - ? formatResetTime(row.bucket.resetTime, 'column') - : ''} - - - - )} - - ); - })} - - {cacheEfficiency > 0 && !showQuotaColumn && ( - - - Savings Highlight:{' '} - {totalCachedTokens.toLocaleString()} ( - - {cacheEfficiency.toFixed(1)}% - - ) of input tokens were served from the cache, reducing costs. + > + + + Model - )} + + + Reqs + + + + + Input Tokens + + + + + Cache Reads + + + + + Output Tokens + + + + + {/* Rows */} + {Object.entries(models).map(([name, modelMetrics]) => ( + + + + {name} + + + + + {modelMetrics.api.totalRequests} + + + + + {modelMetrics.tokens.prompt.toLocaleString()} + + + + + {modelMetrics.tokens.cached.toLocaleString()} + + + + + {modelMetrics.tokens.candidates.toLocaleString()} + + + + ))} ); }; @@ -521,7 +167,6 @@ const ModelUsageTable: React.FC<{ interface StatsDisplayProps { duration: string; title?: string; - quotas?: RetrieveUserQuotaResponse; footer?: string; selectedAuthType?: string; userEmail?: string; @@ -534,30 +179,17 @@ interface StatsDisplayProps { export const StatsDisplay: React.FC = ({ duration, title, - quotas, footer, selectedAuthType, userEmail, tier, - currentModel, - quotaStats, creditBalance, }) => { const { stats } = useSessionStats(); const { metrics } = stats; - const { models, tools, files } = metrics; + const { tools, files, models } = metrics; const computed = computeSessionStats(metrics); const settings = useSettings(); - const config = useConfig(); - const useGemini3_1 = config.getGemini31LaunchedSync?.() ?? false; - const useGemini3_1FlashLite = - config.getGemini31FlashLiteLaunchedSync?.() ?? false; - const useCustomToolModel = - useGemini3_1 && - config.getContentGeneratorConfig().authType === AuthType.USE_GEMINI; - const pooledRemaining = quotaStats?.remaining; - const pooledLimit = quotaStats?.limit; - const pooledResetTime = quotaStats?.resetTime; const showUserIdentity = settings.merged.ui.showUserIdentity; @@ -697,20 +329,9 @@ export const StatsDisplay: React.FC = ({ - + + {Object.keys(models).length > 0 && } + {renderFooter()} ); diff --git a/packages/cli/src/ui/components/__snapshots__/ModelQuotaDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ModelQuotaDisplay.test.tsx.snap new file mode 100644 index 0000000000..cc601cffbd --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/ModelQuotaDisplay.test.tsx.snap @@ -0,0 +1,10 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` > renders quota information when buckets are provided 1`] = ` +" +──────────────────────────────────────────────────────────────────────────────────────────────────── +Model usage + +Pro ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 25% Resets: 1:30 PM (1h 30m) +" +`; diff --git a/packages/cli/src/ui/components/__snapshots__/ProgressBar.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/ProgressBar.test.tsx.snap new file mode 100644 index 0000000000..cb4fd02cba --- /dev/null +++ b/packages/cli/src/ui/components/__snapshots__/ProgressBar.test.tsx.snap @@ -0,0 +1,21 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[` > renders 0% correctly 1`] = ` +"▬▬▬▬▬▬▬▬▬▬ +" +`; + +exports[` > renders 50% correctly 1`] = ` +"▬▬▬▬▬▬▬▬▬▬ +" +`; + +exports[` > renders error threshold correctly at 100% 1`] = ` +"▬▬▬▬▬▬▬▬▬▬ +" +`; + +exports[` > renders warning threshold correctly 1`] = ` +"▬▬▬▬▬▬▬▬▬▬ +" +`; diff --git a/packages/cli/src/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap b/packages/cli/src/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap index e6d61e64e5..f23344a116 100644 --- a/packages/cli/src/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap @@ -17,12 +17,13 @@ exports[` > renders the summary display with a title 1` │ » API Time: 50.2s (100.0%) │ │ » Tool Time: 0s (0.0%) │ │ │ -│ 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. │ +│ Model Usage │ +│ Use /model to view model quota information │ │ │ +│ Model Reqs Input Tokens Cache Reads Output Tokens │ +│ ────────────────────────────────────────────────────────────────────────────────────────────── │ +│ gemini-2.5-pro 10 1,000 500 2,000 │ │ To resume this session: gemini --resume test-session │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ " 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 8f876cc44b..a06587aaaf 100644 --- a/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap @@ -117,10 +117,13 @@ exports[` > Conditional Rendering Tests > hides Efficiency secti │ » API Time: 100ms (100.0%) │ │ » Tool Time: 0s (0.0%) │ │ │ -│ Model Reqs Input Tokens Cache Reads Output Tokens │ -│ ──────────────────────────────────────────────────────────────────────── │ -│ gemini-2.5-pro 1 100 0 100 │ │ │ +│ Model Usage │ +│ Use /model to view model quota information │ +│ │ +│ Model Reqs Input Tokens Cache Reads Output Tokens │ +│ ────────────────────────────────────────────────────────────────────────────────────────────── │ +│ gemini-2.5-pro 1 100 0 100 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ " `; @@ -145,84 +148,6 @@ exports[` > Conditional Rendering Tests > hides User Agreement w " `; -exports[` > Quota Display > renders pooled quota information for auto mode 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Interaction Summary │ -│ Session ID: test-session-id │ -│ Tool Calls: 0 ( ✓ 0 x 0 ) │ -│ Success Rate: 0.0% │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ Agent Active: 0s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ │ -│ 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 Usage resets │ -│ ──────────────────────────────────────────────────────────────────────────────── │ -│ gemini-2.5-pro - ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 90% │ -│ gemini-2.5-flash - ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 30% │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -" -`; - -exports[` > Quota Display > renders quota information for unused models 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Interaction Summary │ -│ Session ID: test-session-id │ -│ Tool Calls: 0 ( ✓ 0 x 0 ) │ -│ Success Rate: 0.0% │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ Agent Active: 0s │ -│ » API Time: 0s (0.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ │ -│ Model Reqs Model usage Usage resets │ -│ ──────────────────────────────────────────────────────────────────────────────── │ -│ gemini-2.5-flash - ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 50% 2:00 PM (2h) │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -" -`; - -exports[` > Quota Display > renders quota information when quotas are provided 1`] = ` -"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ -│ │ -│ Session Stats │ -│ │ -│ Interaction Summary │ -│ Session ID: test-session-id │ -│ Tool Calls: 0 ( ✓ 0 x 0 ) │ -│ Success Rate: 0.0% │ -│ │ -│ Performance │ -│ Wall Time: 1s │ -│ Agent Active: 100ms │ -│ » API Time: 100ms (100.0%) │ -│ » Tool Time: 0s (0.0%) │ -│ │ -│ Model Reqs Model usage Usage resets │ -│ ──────────────────────────────────────────────────────────────────────────────── │ -│ gemini-2.5-pro 1 ▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬▬ 25% 1:30 PM (1h 30m) │ -│ │ -╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ -" -`; - exports[` > Title Rendering > renders the custom title when a title prop is provided 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │ │ @@ -279,13 +204,14 @@ exports[` > renders a table with two models correctly 1`] = ` │ » API Time: 19.5s (100.0%) │ │ » Tool Time: 0s (0.0%) │ │ │ -│ 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. │ +│ Model Usage │ +│ Use /model to view model quota information │ │ │ +│ Model Reqs Input Tokens Cache Reads Output Tokens │ +│ ────────────────────────────────────────────────────────────────────────────────────────────── │ +│ gemini-2.5-pro 3 1,000 500 2,000 │ +│ gemini-2.5-flash 5 25,000 10,000 15,000 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ " `; @@ -307,12 +233,13 @@ exports[` > renders all sections when all data is present 1`] = │ » API Time: 100ms (44.8%) │ │ » Tool Time: 123ms (55.2%) │ │ │ -│ 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. │ +│ Model Usage │ +│ Use /model to view model quota information │ │ │ +│ Model Reqs Input Tokens Cache Reads Output Tokens │ +│ ────────────────────────────────────────────────────────────────────────────────────────────── │ +│ gemini-2.5-pro 1 100 50 100 │ ╰──────────────────────────────────────────────────────────────────────────────────────────────────╯ " `; diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-complex-table-with-mixed-content-lengths-correctly-2.snap.svg b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-complex-table-with-mixed-content-lengths-correctly-2.snap.svg new file mode 100644 index 0000000000..73c93ab257 --- /dev/null +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer-TableRenderer-renders-a-complex-table-with-mixed-content-lengths-correctly-2.snap.svg @@ -0,0 +1,401 @@ + + + + + ┌─────────────────────────────┬──────────────────────────────┬─────────────────────────────┬──────────────────────────────┬─────┬────────┬─────────┬───────┐ + + Comprehensive Architectural + + Implementation Details for + + Longitudinal Performance + + Strategic Security Framework + + Key + + Status + + Version + + Owner + + + Specification for the + + the High-Throughput + + Analysis Across + + for Mitigating Sophisticated + + + + + + + Distributed Infrastructure + + Asynchronous Message + + Multi-Regional Cloud + + Cross-Site Scripting + + + + + + + Layer + + Processing Pipeline with + + Deployment Clusters + + Vulnerabilities + + + + + + + + Extended Scalability + + + + + + + + + + Features and Redundancy + + + + + + + + + + Protocols + + + + + + + + ├─────────────────────────────┼──────────────────────────────┼─────────────────────────────┼──────────────────────────────┼─────┼────────┼─────────┼───────┤ + + The primary architecture + + Each message is processed + + Historical data indicates a + + A multi-layered defense + + INF + + Active + + v2.4 + + J. + + + utilizes a decoupled + + through a series of + + significant reduction in + + strategy incorporates + + + + + Doe + + + microservices approach, + + specialized workers that + + tail latency when utilizing + + content security policies, + + + + + + + leveraging container + + handle data transformation, + + edge computing nodes closer + + input sanitization + + + + + + + orchestration for + + validation, and persistent + + to the geographic location + + libraries, and regular + + + + + + + scalability and fault + + storage using a persistent + + of the end-user base. + + automated penetration + + + + + + + tolerance in high-load + + queue. + + + testing routines. + + + + + + + scenarios. + + + Monitoring tools have + + + + + + + + + The pipeline features + + captured a steady increase + + Developers are required to + + + + + + + This layer provides the + + built-in retry mechanisms + + in throughput efficiency + + undergo mandatory security + + + + + + + fundamental building blocks + + with exponential backoff to + + since the introduction of + + training focusing on the + + + + + + + for service discovery, load + + ensure message delivery + + the vectorized query engine + + OWASP Top Ten to ensure that + + + + + + + balancing, and + + integrity even during + + in the primary data + + security is integrated into + + + + + + + inter-service communication + + transient network or service + + warehouse. + + the initial design phase. + + + + + + + via highly efficient + + failures. + + + + + + + + + protocol buffers. + + + Resource utilization + + The implementation of a + + + + + + + + Horizontal autoscaling is + + metrics demonstrate that + + robust Identity and Access + + + + + + + Advanced telemetry and + + triggered automatically + + the transition to + + Management system ensures + + + + + + + logging integrations allow + + based on the depth of the + + serverless compute for + + that the principle of least + + + + + + + for real-time monitoring of + + processing queue, ensuring + + intermittent tasks has + + privilege is strictly + + + + + + + system health and rapid + + consistent performance + + resulted in a thirty + + enforced across all + + + + + + + identification of + + during unexpected traffic + + percent cost optimization. + + environments. + + + + + + + bottlenecks within the + + spikes. + + + + + + + + + service mesh. + + + + + + + + + └─────────────────────────────┴──────────────────────────────┴─────────────────────────────┴──────────────────────────────┴─────┴────────┴─────────┴───────┘ + + \ No newline at end of file diff --git a/packages/cli/src/ui/utils/__snapshots__/TableRenderer.test.tsx.snap b/packages/cli/src/ui/utils/__snapshots__/TableRenderer.test.tsx.snap index b3737888ec..fa1d83081d 100644 --- a/packages/cli/src/ui/utils/__snapshots__/TableRenderer.test.tsx.snap +++ b/packages/cli/src/ui/utils/__snapshots__/TableRenderer.test.tsx.snap @@ -165,6 +165,45 @@ exports[`TableRenderer > renders a complex table with mixed content lengths corr └─────────────────────────────┴──────────────────────────────┴─────────────────────────────┴──────────────────────────────┴─────┴────────┴─────────┴───────┘" `; +exports[`TableRenderer > renders a complex table with mixed content lengths correctly 2`] = ` +" +┌─────────────────────────────┬──────────────────────────────┬─────────────────────────────┬──────────────────────────────┬─────┬────────┬─────────┬───────┐ +│ Comprehensive Architectural │ Implementation Details for │ Longitudinal Performance │ Strategic Security Framework │ Key │ Status │ Version │ Owner │ +│ Specification for the │ the High-Throughput │ Analysis Across │ for Mitigating Sophisticated │ │ │ │ │ +│ Distributed Infrastructure │ Asynchronous Message │ Multi-Regional Cloud │ Cross-Site Scripting │ │ │ │ │ +│ Layer │ Processing Pipeline with │ Deployment Clusters │ Vulnerabilities │ │ │ │ │ +│ │ Extended Scalability │ │ │ │ │ │ │ +│ │ Features and Redundancy │ │ │ │ │ │ │ +│ │ Protocols │ │ │ │ │ │ │ +├─────────────────────────────┼──────────────────────────────┼─────────────────────────────┼──────────────────────────────┼─────┼────────┼─────────┼───────┤ +│ The primary architecture │ Each message is processed │ Historical data indicates a │ A multi-layered defense │ INF │ Active │ v2.4 │ J. │ +│ utilizes a decoupled │ through a series of │ significant reduction in │ strategy incorporates │ │ │ │ Doe │ +│ microservices approach, │ specialized workers that │ tail latency when utilizing │ content security policies, │ │ │ │ │ +│ leveraging container │ handle data transformation, │ edge computing nodes closer │ input sanitization │ │ │ │ │ +│ orchestration for │ validation, and persistent │ to the geographic location │ libraries, and regular │ │ │ │ │ +│ scalability and fault │ storage using a persistent │ of the end-user base. │ automated penetration │ │ │ │ │ +│ tolerance in high-load │ queue. │ │ testing routines. │ │ │ │ │ +│ scenarios. │ │ Monitoring tools have │ │ │ │ │ │ +│ │ The pipeline features │ captured a steady increase │ Developers are required to │ │ │ │ │ +│ This layer provides the │ built-in retry mechanisms │ in throughput efficiency │ undergo mandatory security │ │ │ │ │ +│ fundamental building blocks │ with exponential backoff to │ since the introduction of │ training focusing on the │ │ │ │ │ +│ for service discovery, load │ ensure message delivery │ the vectorized query engine │ OWASP Top Ten to ensure that │ │ │ │ │ +│ balancing, and │ integrity even during │ in the primary data │ security is integrated into │ │ │ │ │ +│ inter-service communication │ transient network or service │ warehouse. │ the initial design phase. │ │ │ │ │ +│ via highly efficient │ failures. │ │ │ │ │ │ │ +│ protocol buffers. │ │ Resource utilization │ The implementation of a │ │ │ │ │ +│ │ Horizontal autoscaling is │ metrics demonstrate that │ robust Identity and Access │ │ │ │ │ +│ Advanced telemetry and │ triggered automatically │ the transition to │ Management system ensures │ │ │ │ │ +│ logging integrations allow │ based on the depth of the │ serverless compute for │ that the principle of least │ │ │ │ │ +│ for real-time monitoring of │ processing queue, ensuring │ intermittent tasks has │ privilege is strictly │ │ │ │ │ +│ system health and rapid │ consistent performance │ resulted in a thirty │ enforced across all │ │ │ │ │ +│ identification of │ during unexpected traffic │ percent cost optimization. │ environments. │ │ │ │ │ +│ bottlenecks within the │ spikes. │ │ │ │ │ │ │ +│ service mesh. │ │ │ │ │ │ │ │ +└─────────────────────────────┴──────────────────────────────┴─────────────────────────────┴──────────────────────────────┴─────┴────────┴─────────┴───────┘ +" +`; + exports[`TableRenderer > renders a table with long headers and 4 columns correctly 1`] = ` "┌───────────────┬───────────────┬──────────────────┬──────────────────┐ │ Very Long │ Very Long │ Very Long Column │ Very Long Column │