From 8f8082fe3da9e1972f8b8226c68fa14e326a3d8a Mon Sep 17 00:00:00 2001 From: Arya Gummadi Date: Mon, 18 Aug 2025 22:57:53 -0700 Subject: feat: add file change tracking to session metrics (#6094) Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Jacob Richman --- .../ui/components/SessionSummaryDisplay.test.tsx | 4 + .../cli/src/ui/components/StatsDisplay.test.tsx | 88 ++++++++++++++++++++++ packages/cli/src/ui/components/StatsDisplay.tsx | 47 +++++++----- .../SessionSummaryDisplay.test.tsx.snap | 1 + .../__snapshots__/StatsDisplay.test.tsx.snap | 41 ++++++++++ 5 files changed, 164 insertions(+), 17 deletions(-) (limited to 'packages/cli/src/ui/components') diff --git a/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx b/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx index 38400caf..816948f2 100644 --- a/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx +++ b/packages/cli/src/ui/components/SessionSummaryDisplay.test.tsx @@ -60,6 +60,10 @@ describe('', () => { totalDecisions: { accept: 0, reject: 0, modify: 0 }, byName: {}, }, + files: { + totalLinesAdded: 42, + totalLinesRemoved: 15, + }, }; const { lastFrame } = renderWithMockedStats(metrics); diff --git a/packages/cli/src/ui/components/StatsDisplay.test.tsx b/packages/cli/src/ui/components/StatsDisplay.test.tsx index eed105e3..6d6fa809 100644 --- a/packages/cli/src/ui/components/StatsDisplay.test.tsx +++ b/packages/cli/src/ui/components/StatsDisplay.test.tsx @@ -50,6 +50,10 @@ describe('', () => { totalDecisions: { accept: 0, reject: 0, modify: 0 }, byName: {}, }, + files: { + totalLinesAdded: 0, + totalLinesRemoved: 0, + }, }; const { lastFrame } = renderWithMockedStats(zeroMetrics); @@ -96,6 +100,10 @@ describe('', () => { totalDecisions: { accept: 0, reject: 0, modify: 0 }, byName: {}, }, + files: { + totalLinesAdded: 0, + totalLinesRemoved: 0, + }, }; const { lastFrame } = renderWithMockedStats(metrics); @@ -139,6 +147,10 @@ describe('', () => { }, }, }, + files: { + totalLinesAdded: 0, + totalLinesRemoved: 0, + }, }; const { lastFrame } = renderWithMockedStats(metrics); @@ -172,6 +184,10 @@ describe('', () => { }, }, }, + files: { + totalLinesAdded: 0, + totalLinesRemoved: 0, + }, }; const { lastFrame } = renderWithMockedStats(metrics); @@ -206,6 +222,10 @@ describe('', () => { totalDecisions: { accept: 0, reject: 0, modify: 0 }, byName: {}, }, + files: { + totalLinesAdded: 0, + totalLinesRemoved: 0, + }, }; const { lastFrame } = renderWithMockedStats(metrics); @@ -228,6 +248,10 @@ describe('', () => { totalDecisions: { accept: 0, reject: 0, modify: 0 }, byName: {}, }, + files: { + totalLinesAdded: 0, + totalLinesRemoved: 0, + }, }; const { lastFrame } = renderWithMockedStats(metrics); expect(lastFrame()).toMatchSnapshot(); @@ -244,6 +268,10 @@ describe('', () => { totalDecisions: { accept: 0, reject: 0, modify: 0 }, byName: {}, }, + files: { + totalLinesAdded: 0, + totalLinesRemoved: 0, + }, }; const { lastFrame } = renderWithMockedStats(metrics); expect(lastFrame()).toMatchSnapshot(); @@ -260,12 +288,68 @@ describe('', () => { totalDecisions: { accept: 0, reject: 0, modify: 0 }, byName: {}, }, + files: { + totalLinesAdded: 0, + totalLinesRemoved: 0, + }, }; const { lastFrame } = renderWithMockedStats(metrics); expect(lastFrame()).toMatchSnapshot(); }); }); + describe('Code Changes Display', () => { + it('displays Code Changes when line counts are present', () => { + const metrics: SessionMetrics = { + models: {}, + tools: { + totalCalls: 1, + totalSuccess: 1, + totalFail: 0, + totalDurationMs: 100, + totalDecisions: { accept: 0, reject: 0, modify: 0 }, + byName: {}, + }, + files: { + totalLinesAdded: 42, + totalLinesRemoved: 18, + }, + }; + + const { lastFrame } = renderWithMockedStats(metrics); + const output = lastFrame(); + + expect(output).toContain('Code Changes:'); + expect(output).toContain('+42'); + expect(output).toContain('-18'); + expect(output).toMatchSnapshot(); + }); + + it('hides Code Changes when no lines are added or removed', () => { + const metrics: SessionMetrics = { + models: {}, + tools: { + totalCalls: 1, + totalSuccess: 1, + totalFail: 0, + totalDurationMs: 100, + totalDecisions: { accept: 0, reject: 0, modify: 0 }, + byName: {}, + }, + files: { + totalLinesAdded: 0, + totalLinesRemoved: 0, + }, + }; + + const { lastFrame } = renderWithMockedStats(metrics); + const output = lastFrame(); + + expect(output).not.toContain('Code Changes:'); + expect(output).toMatchSnapshot(); + }); + }); + describe('Title Rendering', () => { const zeroMetrics: SessionMetrics = { models: {}, @@ -277,6 +361,10 @@ describe('', () => { totalDecisions: { accept: 0, reject: 0, modify: 0 }, byName: {}, }, + files: { + totalLinesAdded: 0, + totalLinesRemoved: 0, + }, }; it('renders the default title when no title prop is provided', () => { diff --git a/packages/cli/src/ui/components/StatsDisplay.tsx b/packages/cli/src/ui/components/StatsDisplay.tsx index 71c88aef..8dd00efd 100644 --- a/packages/cli/src/ui/components/StatsDisplay.tsx +++ b/packages/cli/src/ui/components/StatsDisplay.tsx @@ -7,7 +7,7 @@ import React from 'react'; import { Box, Text } from 'ink'; import Gradient from 'ink-gradient'; -import { Colors } from '../colors.js'; +import { theme } from '../semantic-colors.js'; import { formatDuration } from '../utils/formatters.js'; import { useSessionStats, ModelMetrics } from '../contexts/SessionContext.js'; import { @@ -29,7 +29,7 @@ const StatRow: React.FC = ({ title, children }) => ( {/* Fixed width for the label creates a clean "gutter" for alignment */} - {title} + {title} {children} @@ -111,12 +111,12 @@ const ModelUsageTable: React.FC<{ {modelMetrics.api.totalRequests} - + {modelMetrics.tokens.prompt.toLocaleString()} - + {modelMetrics.tokens.candidates.toLocaleString()} @@ -125,12 +125,12 @@ const ModelUsageTable: React.FC<{ {cacheEfficiency > 0 && ( - Savings Highlight:{' '} + Savings Highlight:{' '} {totalCachedTokens.toLocaleString()} ({cacheEfficiency.toFixed(1)} %) of input tokens were served from the cache, reducing costs. - + » Tip: For a full token breakdown, run `/stats model`. @@ -150,7 +150,7 @@ export const StatsDisplay: React.FC = ({ }) => { const { stats } = useSessionStats(); const { metrics } = stats; - const { models, tools } = metrics; + const { models, tools, files } = metrics; const computed = computeSessionStats(metrics); const successThresholds = { @@ -169,18 +169,18 @@ export const StatsDisplay: React.FC = ({ const renderTitle = () => { if (title) { - return Colors.GradientColors && Colors.GradientColors.length > 0 ? ( - + return theme.ui.gradient && theme.ui.gradient.length > 0 ? ( + {title} ) : ( - + {title} ); } return ( - + Session Stats ); @@ -189,7 +189,7 @@ export const StatsDisplay: React.FC = ({ return ( = ({ {tools.totalCalls} ({' '} - ✔ {tools.totalSuccess}{' '} - ✖ {tools.totalFail} ) + ✔ {tools.totalSuccess}{' '} + ✖ {tools.totalFail} ) @@ -215,12 +215,25 @@ export const StatsDisplay: React.FC = ({ {computed.agreementRate.toFixed(1)}%{' '} - + ({computed.totalDecisions} reviewed) )} + {files && + (files.totalLinesAdded > 0 || files.totalLinesRemoved > 0) && ( + + + + +{files.totalLinesAdded} + {' '} + + -{files.totalLinesRemoved} + + + + )}
@@ -233,7 +246,7 @@ export const StatsDisplay: React.FC = ({ {formatDuration(computed.totalApiTime)}{' '} - + ({computed.apiTimePercent.toFixed(1)}%) @@ -241,7 +254,7 @@ export const StatsDisplay: React.FC = ({ {formatDuration(computed.totalToolTime)}{' '} - + ({computed.toolTimePercent.toFixed(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 98e7722e..97a0b525 100644 --- a/packages/cli/src/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/SessionSummaryDisplay.test.tsx.snap @@ -9,6 +9,7 @@ exports[` > renders the summary display with a title 1` │ Session ID: │ │ Tool Calls: 0 ( ✔ 0 ✖ 0 ) │ │ Success Rate: 0.0% │ +│ Code Changes: +42 -15 │ │ │ │ Performance │ │ Wall Time: 1h 23m 45s │ 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 09202599..d6842188 100644 --- a/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap +++ b/packages/cli/src/ui/components/__snapshots__/StatsDisplay.test.tsx.snap @@ -1,5 +1,46 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[` > Code Changes Display > displays Code Changes when line counts are present 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Interaction Summary │ +│ Session ID: test-session-id │ +│ Tool Calls: 1 ( ✔ 1 ✖ 0 ) │ +│ Success Rate: 100.0% │ +│ Code Changes: +42 -18 │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ Agent Active: 100ms │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 100ms (100.0%) │ +│ │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + +exports[` > Code Changes Display > hides Code Changes when no lines are added or removed 1`] = ` +"╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ +│ │ +│ Session Stats │ +│ │ +│ Interaction Summary │ +│ Session ID: test-session-id │ +│ Tool Calls: 1 ( ✔ 1 ✖ 0 ) │ +│ Success Rate: 100.0% │ +│ │ +│ Performance │ +│ Wall Time: 1s │ +│ Agent Active: 100ms │ +│ » API Time: 0s (0.0%) │ +│ » Tool Time: 100ms (100.0%) │ +│ │ +│ │ +╰──────────────────────────────────────────────────────────────────────────────────────────────────╯" +`; + exports[` > Conditional Color Tests > renders success rate in green for high values 1`] = ` "╭──────────────────────────────────────────────────────────────────────────────────────────────────╮ │ │ -- cgit v1.2.3