summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/utils
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/ui/utils')
-rw-r--r--packages/cli/src/ui/utils/computeStats.test.ts247
-rw-r--r--packages/cli/src/ui/utils/computeStats.ts84
-rw-r--r--packages/cli/src/ui/utils/displayUtils.test.ts58
-rw-r--r--packages/cli/src/ui/utils/displayUtils.ts32
-rw-r--r--packages/cli/src/ui/utils/formatters.ts2
5 files changed, 422 insertions, 1 deletions
diff --git a/packages/cli/src/ui/utils/computeStats.test.ts b/packages/cli/src/ui/utils/computeStats.test.ts
new file mode 100644
index 00000000..0e32ffe2
--- /dev/null
+++ b/packages/cli/src/ui/utils/computeStats.test.ts
@@ -0,0 +1,247 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect } from 'vitest';
+import {
+ calculateAverageLatency,
+ calculateCacheHitRate,
+ calculateErrorRate,
+ computeSessionStats,
+} from './computeStats.js';
+import { ModelMetrics, SessionMetrics } from '../contexts/SessionContext.js';
+
+describe('calculateErrorRate', () => {
+ it('should return 0 if totalRequests is 0', () => {
+ const metrics: ModelMetrics = {
+ api: { totalRequests: 0, totalErrors: 0, totalLatencyMs: 0 },
+ tokens: {
+ prompt: 0,
+ candidates: 0,
+ total: 0,
+ cached: 0,
+ thoughts: 0,
+ tool: 0,
+ },
+ };
+ expect(calculateErrorRate(metrics)).toBe(0);
+ });
+
+ it('should calculate the error rate correctly', () => {
+ const metrics: ModelMetrics = {
+ api: { totalRequests: 10, totalErrors: 2, totalLatencyMs: 0 },
+ tokens: {
+ prompt: 0,
+ candidates: 0,
+ total: 0,
+ cached: 0,
+ thoughts: 0,
+ tool: 0,
+ },
+ };
+ expect(calculateErrorRate(metrics)).toBe(20);
+ });
+});
+
+describe('calculateAverageLatency', () => {
+ it('should return 0 if totalRequests is 0', () => {
+ const metrics: ModelMetrics = {
+ api: { totalRequests: 0, totalErrors: 0, totalLatencyMs: 1000 },
+ tokens: {
+ prompt: 0,
+ candidates: 0,
+ total: 0,
+ cached: 0,
+ thoughts: 0,
+ tool: 0,
+ },
+ };
+ expect(calculateAverageLatency(metrics)).toBe(0);
+ });
+
+ it('should calculate the average latency correctly', () => {
+ const metrics: ModelMetrics = {
+ api: { totalRequests: 10, totalErrors: 0, totalLatencyMs: 1500 },
+ tokens: {
+ prompt: 0,
+ candidates: 0,
+ total: 0,
+ cached: 0,
+ thoughts: 0,
+ tool: 0,
+ },
+ };
+ expect(calculateAverageLatency(metrics)).toBe(150);
+ });
+});
+
+describe('calculateCacheHitRate', () => {
+ it('should return 0 if prompt tokens is 0', () => {
+ const metrics: ModelMetrics = {
+ api: { totalRequests: 0, totalErrors: 0, totalLatencyMs: 0 },
+ tokens: {
+ prompt: 0,
+ candidates: 0,
+ total: 0,
+ cached: 100,
+ thoughts: 0,
+ tool: 0,
+ },
+ };
+ expect(calculateCacheHitRate(metrics)).toBe(0);
+ });
+
+ it('should calculate the cache hit rate correctly', () => {
+ const metrics: ModelMetrics = {
+ api: { totalRequests: 0, totalErrors: 0, totalLatencyMs: 0 },
+ tokens: {
+ prompt: 200,
+ candidates: 0,
+ total: 0,
+ cached: 50,
+ thoughts: 0,
+ tool: 0,
+ },
+ };
+ expect(calculateCacheHitRate(metrics)).toBe(25);
+ });
+});
+
+describe('computeSessionStats', () => {
+ it('should return all zeros for initial empty metrics', () => {
+ const metrics: SessionMetrics = {
+ models: {},
+ tools: {
+ totalCalls: 0,
+ totalSuccess: 0,
+ totalFail: 0,
+ totalDurationMs: 0,
+ totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ byName: {},
+ },
+ };
+
+ const result = computeSessionStats(metrics);
+
+ expect(result).toEqual({
+ totalApiTime: 0,
+ totalToolTime: 0,
+ agentActiveTime: 0,
+ apiTimePercent: 0,
+ toolTimePercent: 0,
+ cacheEfficiency: 0,
+ totalDecisions: 0,
+ successRate: 0,
+ agreementRate: 0,
+ totalPromptTokens: 0,
+ totalCachedTokens: 0,
+ });
+ });
+
+ it('should correctly calculate API and tool time percentages', () => {
+ const metrics: SessionMetrics = {
+ models: {
+ 'gemini-pro': {
+ api: { totalRequests: 1, totalErrors: 0, totalLatencyMs: 750 },
+ tokens: {
+ prompt: 10,
+ candidates: 10,
+ total: 20,
+ cached: 0,
+ thoughts: 0,
+ tool: 0,
+ },
+ },
+ },
+ tools: {
+ totalCalls: 1,
+ totalSuccess: 1,
+ totalFail: 0,
+ totalDurationMs: 250,
+ totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ byName: {},
+ },
+ };
+
+ const result = computeSessionStats(metrics);
+
+ expect(result.totalApiTime).toBe(750);
+ expect(result.totalToolTime).toBe(250);
+ expect(result.agentActiveTime).toBe(1000);
+ expect(result.apiTimePercent).toBe(75);
+ expect(result.toolTimePercent).toBe(25);
+ });
+
+ it('should correctly calculate cache efficiency', () => {
+ const metrics: SessionMetrics = {
+ models: {
+ 'gemini-pro': {
+ api: { totalRequests: 2, totalErrors: 0, totalLatencyMs: 1000 },
+ tokens: {
+ prompt: 150,
+ candidates: 10,
+ total: 160,
+ cached: 50,
+ thoughts: 0,
+ tool: 0,
+ },
+ },
+ },
+ tools: {
+ totalCalls: 0,
+ totalSuccess: 0,
+ totalFail: 0,
+ totalDurationMs: 0,
+ totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ byName: {},
+ },
+ };
+
+ const result = computeSessionStats(metrics);
+
+ expect(result.cacheEfficiency).toBeCloseTo(33.33); // 50 / 150
+ });
+
+ it('should correctly calculate success and agreement rates', () => {
+ const metrics: SessionMetrics = {
+ models: {},
+ tools: {
+ totalCalls: 10,
+ totalSuccess: 8,
+ totalFail: 2,
+ totalDurationMs: 1000,
+ totalDecisions: { accept: 6, reject: 2, modify: 2 },
+ byName: {},
+ },
+ };
+
+ const result = computeSessionStats(metrics);
+
+ expect(result.successRate).toBe(80); // 8 / 10
+ expect(result.agreementRate).toBe(60); // 6 / 10
+ });
+
+ it('should handle division by zero gracefully', () => {
+ const metrics: SessionMetrics = {
+ models: {},
+ tools: {
+ totalCalls: 0,
+ totalSuccess: 0,
+ totalFail: 0,
+ totalDurationMs: 0,
+ totalDecisions: { accept: 0, reject: 0, modify: 0 },
+ byName: {},
+ },
+ };
+
+ const result = computeSessionStats(metrics);
+
+ expect(result.apiTimePercent).toBe(0);
+ expect(result.toolTimePercent).toBe(0);
+ expect(result.cacheEfficiency).toBe(0);
+ expect(result.successRate).toBe(0);
+ expect(result.agreementRate).toBe(0);
+ });
+});
diff --git a/packages/cli/src/ui/utils/computeStats.ts b/packages/cli/src/ui/utils/computeStats.ts
new file mode 100644
index 00000000..e0483c3b
--- /dev/null
+++ b/packages/cli/src/ui/utils/computeStats.ts
@@ -0,0 +1,84 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import {
+ SessionMetrics,
+ ComputedSessionStats,
+ ModelMetrics,
+} from '../contexts/SessionContext.js';
+
+export function calculateErrorRate(metrics: ModelMetrics): number {
+ if (metrics.api.totalRequests === 0) {
+ return 0;
+ }
+ return (metrics.api.totalErrors / metrics.api.totalRequests) * 100;
+}
+
+export function calculateAverageLatency(metrics: ModelMetrics): number {
+ if (metrics.api.totalRequests === 0) {
+ return 0;
+ }
+ return metrics.api.totalLatencyMs / metrics.api.totalRequests;
+}
+
+export function calculateCacheHitRate(metrics: ModelMetrics): number {
+ if (metrics.tokens.prompt === 0) {
+ return 0;
+ }
+ return (metrics.tokens.cached / metrics.tokens.prompt) * 100;
+}
+
+export const computeSessionStats = (
+ metrics: SessionMetrics,
+): ComputedSessionStats => {
+ const { models, tools } = metrics;
+ const totalApiTime = Object.values(models).reduce(
+ (acc, model) => acc + model.api.totalLatencyMs,
+ 0,
+ );
+ const totalToolTime = tools.totalDurationMs;
+ const agentActiveTime = totalApiTime + totalToolTime;
+ const apiTimePercent =
+ agentActiveTime > 0 ? (totalApiTime / agentActiveTime) * 100 : 0;
+ const toolTimePercent =
+ agentActiveTime > 0 ? (totalToolTime / agentActiveTime) * 100 : 0;
+
+ const totalCachedTokens = Object.values(models).reduce(
+ (acc, model) => acc + model.tokens.cached,
+ 0,
+ );
+ const totalPromptTokens = Object.values(models).reduce(
+ (acc, model) => acc + model.tokens.prompt,
+ 0,
+ );
+ const cacheEfficiency =
+ totalPromptTokens > 0 ? (totalCachedTokens / totalPromptTokens) * 100 : 0;
+
+ const totalDecisions =
+ tools.totalDecisions.accept +
+ tools.totalDecisions.reject +
+ tools.totalDecisions.modify;
+ const successRate =
+ tools.totalCalls > 0 ? (tools.totalSuccess / tools.totalCalls) * 100 : 0;
+ const agreementRate =
+ totalDecisions > 0
+ ? (tools.totalDecisions.accept / totalDecisions) * 100
+ : 0;
+
+ return {
+ totalApiTime,
+ totalToolTime,
+ agentActiveTime,
+ apiTimePercent,
+ toolTimePercent,
+ cacheEfficiency,
+ totalDecisions,
+ successRate,
+ agreementRate,
+ totalCachedTokens,
+ totalPromptTokens,
+ };
+};
diff --git a/packages/cli/src/ui/utils/displayUtils.test.ts b/packages/cli/src/ui/utils/displayUtils.test.ts
new file mode 100644
index 00000000..7dd9f0e8
--- /dev/null
+++ b/packages/cli/src/ui/utils/displayUtils.test.ts
@@ -0,0 +1,58 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { describe, it, expect } from 'vitest';
+import {
+ getStatusColor,
+ TOOL_SUCCESS_RATE_HIGH,
+ TOOL_SUCCESS_RATE_MEDIUM,
+ USER_AGREEMENT_RATE_HIGH,
+ USER_AGREEMENT_RATE_MEDIUM,
+ CACHE_EFFICIENCY_HIGH,
+ CACHE_EFFICIENCY_MEDIUM,
+} from './displayUtils.js';
+import { Colors } from '../colors.js';
+
+describe('displayUtils', () => {
+ describe('getStatusColor', () => {
+ const thresholds = {
+ green: 80,
+ yellow: 50,
+ };
+
+ it('should return green for values >= green threshold', () => {
+ expect(getStatusColor(90, thresholds)).toBe(Colors.AccentGreen);
+ expect(getStatusColor(80, thresholds)).toBe(Colors.AccentGreen);
+ });
+
+ it('should return yellow for values < green and >= yellow threshold', () => {
+ expect(getStatusColor(79, thresholds)).toBe(Colors.AccentYellow);
+ expect(getStatusColor(50, thresholds)).toBe(Colors.AccentYellow);
+ });
+
+ it('should return red for values < yellow threshold', () => {
+ expect(getStatusColor(49, thresholds)).toBe(Colors.AccentRed);
+ expect(getStatusColor(0, thresholds)).toBe(Colors.AccentRed);
+ });
+
+ it('should return defaultColor for values < yellow threshold when provided', () => {
+ expect(
+ getStatusColor(49, thresholds, { defaultColor: Colors.Foreground }),
+ ).toBe(Colors.Foreground);
+ });
+ });
+
+ describe('Threshold Constants', () => {
+ it('should have the correct values', () => {
+ expect(TOOL_SUCCESS_RATE_HIGH).toBe(95);
+ expect(TOOL_SUCCESS_RATE_MEDIUM).toBe(85);
+ expect(USER_AGREEMENT_RATE_HIGH).toBe(75);
+ expect(USER_AGREEMENT_RATE_MEDIUM).toBe(45);
+ expect(CACHE_EFFICIENCY_HIGH).toBe(40);
+ expect(CACHE_EFFICIENCY_MEDIUM).toBe(15);
+ });
+ });
+});
diff --git a/packages/cli/src/ui/utils/displayUtils.ts b/packages/cli/src/ui/utils/displayUtils.ts
new file mode 100644
index 00000000..a52c6ff0
--- /dev/null
+++ b/packages/cli/src/ui/utils/displayUtils.ts
@@ -0,0 +1,32 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import { Colors } from '../colors.js';
+
+// --- Thresholds ---
+export const TOOL_SUCCESS_RATE_HIGH = 95;
+export const TOOL_SUCCESS_RATE_MEDIUM = 85;
+
+export const USER_AGREEMENT_RATE_HIGH = 75;
+export const USER_AGREEMENT_RATE_MEDIUM = 45;
+
+export const CACHE_EFFICIENCY_HIGH = 40;
+export const CACHE_EFFICIENCY_MEDIUM = 15;
+
+// --- Color Logic ---
+export const getStatusColor = (
+ value: number,
+ thresholds: { green: number; yellow: number },
+ options: { defaultColor?: string } = {},
+) => {
+ if (value >= thresholds.green) {
+ return Colors.AccentGreen;
+ }
+ if (value >= thresholds.yellow) {
+ return Colors.AccentYellow;
+ }
+ return options.defaultColor || Colors.AccentRed;
+};
diff --git a/packages/cli/src/ui/utils/formatters.ts b/packages/cli/src/ui/utils/formatters.ts
index 82a78109..2b6af545 100644
--- a/packages/cli/src/ui/utils/formatters.ts
+++ b/packages/cli/src/ui/utils/formatters.ts
@@ -27,7 +27,7 @@ export const formatDuration = (milliseconds: number): string => {
}
if (milliseconds < 1000) {
- return `${milliseconds}ms`;
+ return `${Math.round(milliseconds)}ms`;
}
const totalSeconds = milliseconds / 1000;