summaryrefslogtreecommitdiff
path: root/packages/cli/src/ui/components/ToolStatsDisplay.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'packages/cli/src/ui/components/ToolStatsDisplay.tsx')
-rw-r--r--packages/cli/src/ui/components/ToolStatsDisplay.tsx208
1 files changed, 208 insertions, 0 deletions
diff --git a/packages/cli/src/ui/components/ToolStatsDisplay.tsx b/packages/cli/src/ui/components/ToolStatsDisplay.tsx
new file mode 100644
index 00000000..f2335d9e
--- /dev/null
+++ b/packages/cli/src/ui/components/ToolStatsDisplay.tsx
@@ -0,0 +1,208 @@
+/**
+ * @license
+ * Copyright 2025 Google LLC
+ * SPDX-License-Identifier: Apache-2.0
+ */
+
+import React from 'react';
+import { Box, Text } from 'ink';
+import { Colors } from '../colors.js';
+import { formatDuration } from '../utils/formatters.js';
+import {
+ getStatusColor,
+ TOOL_SUCCESS_RATE_HIGH,
+ TOOL_SUCCESS_RATE_MEDIUM,
+ USER_AGREEMENT_RATE_HIGH,
+ USER_AGREEMENT_RATE_MEDIUM,
+} from '../utils/displayUtils.js';
+import { useSessionStats } from '../contexts/SessionContext.js';
+import { ToolCallStats } from '@google/gemini-cli-core';
+
+const TOOL_NAME_COL_WIDTH = 25;
+const CALLS_COL_WIDTH = 8;
+const SUCCESS_RATE_COL_WIDTH = 15;
+const AVG_DURATION_COL_WIDTH = 15;
+
+const StatRow: React.FC<{
+ name: string;
+ stats: ToolCallStats;
+}> = ({ name, stats }) => {
+ const successRate = stats.count > 0 ? (stats.success / stats.count) * 100 : 0;
+ const avgDuration = stats.count > 0 ? stats.durationMs / stats.count : 0;
+ const successColor = getStatusColor(successRate, {
+ green: TOOL_SUCCESS_RATE_HIGH,
+ yellow: TOOL_SUCCESS_RATE_MEDIUM,
+ });
+
+ return (
+ <Box>
+ <Box width={TOOL_NAME_COL_WIDTH}>
+ <Text color={Colors.LightBlue}>{name}</Text>
+ </Box>
+ <Box width={CALLS_COL_WIDTH} justifyContent="flex-end">
+ <Text>{stats.count}</Text>
+ </Box>
+ <Box width={SUCCESS_RATE_COL_WIDTH} justifyContent="flex-end">
+ <Text color={successColor}>{successRate.toFixed(1)}%</Text>
+ </Box>
+ <Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end">
+ <Text>{formatDuration(avgDuration)}</Text>
+ </Box>
+ </Box>
+ );
+};
+
+export const ToolStatsDisplay: React.FC = () => {
+ const { stats } = useSessionStats();
+ const { tools } = stats.metrics;
+ const activeTools = Object.entries(tools.byName).filter(
+ ([, metrics]) => metrics.count > 0,
+ );
+
+ if (activeTools.length === 0) {
+ return (
+ <Box
+ borderStyle="round"
+ borderColor={Colors.Gray}
+ paddingY={1}
+ paddingX={2}
+ >
+ <Text>No tool calls have been made in this session.</Text>
+ </Box>
+ );
+ }
+
+ const totalDecisions = Object.values(tools.byName).reduce(
+ (acc, tool) => {
+ acc.accept += tool.decisions.accept;
+ acc.reject += tool.decisions.reject;
+ acc.modify += tool.decisions.modify;
+ return acc;
+ },
+ { accept: 0, reject: 0, modify: 0 },
+ );
+
+ const totalReviewed =
+ totalDecisions.accept + totalDecisions.reject + totalDecisions.modify;
+ const agreementRate =
+ totalReviewed > 0 ? (totalDecisions.accept / totalReviewed) * 100 : 0;
+ const agreementColor = getStatusColor(agreementRate, {
+ green: USER_AGREEMENT_RATE_HIGH,
+ yellow: USER_AGREEMENT_RATE_MEDIUM,
+ });
+
+ return (
+ <Box
+ borderStyle="round"
+ borderColor={Colors.Gray}
+ flexDirection="column"
+ paddingY={1}
+ paddingX={2}
+ width={70}
+ >
+ <Text bold color={Colors.AccentPurple}>
+ Tool Stats For Nerds
+ </Text>
+ <Box height={1} />
+
+ {/* Header */}
+ <Box>
+ <Box width={TOOL_NAME_COL_WIDTH}>
+ <Text bold>Tool Name</Text>
+ </Box>
+ <Box width={CALLS_COL_WIDTH} justifyContent="flex-end">
+ <Text bold>Calls</Text>
+ </Box>
+ <Box width={SUCCESS_RATE_COL_WIDTH} justifyContent="flex-end">
+ <Text bold>Success Rate</Text>
+ </Box>
+ <Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end">
+ <Text bold>Avg Duration</Text>
+ </Box>
+ </Box>
+
+ {/* Divider */}
+ <Box
+ borderStyle="single"
+ borderBottom={true}
+ borderTop={false}
+ borderLeft={false}
+ borderRight={false}
+ width="100%"
+ />
+
+ {/* Tool Rows */}
+ {activeTools.map(([name, stats]) => (
+ <StatRow key={name} name={name} stats={stats as ToolCallStats} />
+ ))}
+
+ <Box height={1} />
+
+ {/* User Decision Summary */}
+ <Text bold>User Decision Summary</Text>
+ <Box>
+ <Box
+ width={TOOL_NAME_COL_WIDTH + CALLS_COL_WIDTH + SUCCESS_RATE_COL_WIDTH}
+ >
+ <Text color={Colors.LightBlue}>Total Reviewed Suggestions:</Text>
+ </Box>
+ <Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end">
+ <Text>{totalReviewed}</Text>
+ </Box>
+ </Box>
+ <Box>
+ <Box
+ width={TOOL_NAME_COL_WIDTH + CALLS_COL_WIDTH + SUCCESS_RATE_COL_WIDTH}
+ >
+ <Text> » Accepted:</Text>
+ </Box>
+ <Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end">
+ <Text color={Colors.AccentGreen}>{totalDecisions.accept}</Text>
+ </Box>
+ </Box>
+ <Box>
+ <Box
+ width={TOOL_NAME_COL_WIDTH + CALLS_COL_WIDTH + SUCCESS_RATE_COL_WIDTH}
+ >
+ <Text> » Rejected:</Text>
+ </Box>
+ <Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end">
+ <Text color={Colors.AccentRed}>{totalDecisions.reject}</Text>
+ </Box>
+ </Box>
+ <Box>
+ <Box
+ width={TOOL_NAME_COL_WIDTH + CALLS_COL_WIDTH + SUCCESS_RATE_COL_WIDTH}
+ >
+ <Text> » Modified:</Text>
+ </Box>
+ <Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end">
+ <Text color={Colors.AccentYellow}>{totalDecisions.modify}</Text>
+ </Box>
+ </Box>
+
+ {/* Divider */}
+ <Box
+ borderStyle="single"
+ borderBottom={true}
+ borderTop={false}
+ borderLeft={false}
+ borderRight={false}
+ width="100%"
+ />
+
+ <Box>
+ <Box
+ width={TOOL_NAME_COL_WIDTH + CALLS_COL_WIDTH + SUCCESS_RATE_COL_WIDTH}
+ >
+ <Text> Overall Agreement Rate:</Text>
+ </Box>
+ <Box width={AVG_DURATION_COL_WIDTH} justifyContent="flex-end">
+ <Text bold color={totalReviewed > 0 ? agreementColor : undefined}>
+ {totalReviewed > 0 ? `${agreementRate.toFixed(1)}%` : '--'}
+ </Text>
+ </Box>
+ </Box>
+ </Box>
+ );
+};