mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-19 09:33:53 +00:00
feat: Add token stats in footer (#909)
This commit is contained in:
@@ -53,7 +53,10 @@ import {
|
|||||||
} from '@gemini-cli/core';
|
} from '@gemini-cli/core';
|
||||||
import { useLogger } from './hooks/useLogger.js';
|
import { useLogger } from './hooks/useLogger.js';
|
||||||
import { StreamingContext } from './contexts/StreamingContext.js';
|
import { StreamingContext } from './contexts/StreamingContext.js';
|
||||||
import { SessionStatsProvider } from './contexts/SessionContext.js';
|
import {
|
||||||
|
SessionStatsProvider,
|
||||||
|
useSessionStats,
|
||||||
|
} from './contexts/SessionContext.js';
|
||||||
import { useGitBranchName } from './hooks/useGitBranchName.js';
|
import { useGitBranchName } from './hooks/useGitBranchName.js';
|
||||||
import { useTextBuffer } from './components/shared/text-buffer.js';
|
import { useTextBuffer } from './components/shared/text-buffer.js';
|
||||||
import * as fs from 'fs';
|
import * as fs from 'fs';
|
||||||
@@ -79,6 +82,7 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
|
|||||||
handleNewMessage,
|
handleNewMessage,
|
||||||
clearConsoleMessages: clearConsoleMessagesState,
|
clearConsoleMessages: clearConsoleMessagesState,
|
||||||
} = useConsoleMessages();
|
} = useConsoleMessages();
|
||||||
|
const { stats: sessionStats } = useSessionStats();
|
||||||
const [staticNeedsRefresh, setStaticNeedsRefresh] = useState(false);
|
const [staticNeedsRefresh, setStaticNeedsRefresh] = useState(false);
|
||||||
const [staticKey, setStaticKey] = useState(0);
|
const [staticKey, setStaticKey] = useState(0);
|
||||||
const refreshStatic = useCallback(() => {
|
const refreshStatic = useCallback(() => {
|
||||||
@@ -648,6 +652,11 @@ const App = ({ config, settings, startupWarnings = [] }: AppProps) => {
|
|||||||
showMemoryUsage={
|
showMemoryUsage={
|
||||||
config.getDebugMode() || config.getShowMemoryUsage()
|
config.getDebugMode() || config.getShowMemoryUsage()
|
||||||
}
|
}
|
||||||
|
promptTokenCount={sessionStats.currentResponse.promptTokenCount}
|
||||||
|
candidatesTokenCount={
|
||||||
|
sessionStats.currentResponse.candidatesTokenCount
|
||||||
|
}
|
||||||
|
totalTokenCount={sessionStats.currentResponse.totalTokenCount}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Box, Text } from 'ink';
|
import { Box, Text } from 'ink';
|
||||||
import { Colors } from '../colors.js';
|
import { Colors } from '../colors.js';
|
||||||
import { shortenPath, tildeifyPath } from '@gemini-cli/core';
|
import { shortenPath, tildeifyPath, tokenLimit } from '@gemini-cli/core';
|
||||||
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
|
import { ConsoleSummaryDisplay } from './ConsoleSummaryDisplay.js';
|
||||||
import process from 'node:process';
|
import process from 'node:process';
|
||||||
import { MemoryUsageDisplay } from './MemoryUsageDisplay.js';
|
import { MemoryUsageDisplay } from './MemoryUsageDisplay.js';
|
||||||
@@ -22,6 +22,9 @@ interface FooterProps {
|
|||||||
errorCount: number;
|
errorCount: number;
|
||||||
showErrorDetails: boolean;
|
showErrorDetails: boolean;
|
||||||
showMemoryUsage?: boolean;
|
showMemoryUsage?: boolean;
|
||||||
|
promptTokenCount: number;
|
||||||
|
candidatesTokenCount: number;
|
||||||
|
totalTokenCount: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Footer: React.FC<FooterProps> = ({
|
export const Footer: React.FC<FooterProps> = ({
|
||||||
@@ -34,63 +37,75 @@ export const Footer: React.FC<FooterProps> = ({
|
|||||||
errorCount,
|
errorCount,
|
||||||
showErrorDetails,
|
showErrorDetails,
|
||||||
showMemoryUsage,
|
showMemoryUsage,
|
||||||
}) => (
|
totalTokenCount,
|
||||||
<Box marginTop={1} justifyContent="space-between" width="100%">
|
}) => {
|
||||||
<Box>
|
const limit = tokenLimit(model);
|
||||||
<Text color={Colors.LightBlue}>
|
const percentage = totalTokenCount / limit;
|
||||||
{shortenPath(tildeifyPath(targetDir), 70)}
|
|
||||||
{branchName && <Text color={Colors.Gray}> ({branchName}*)</Text>}
|
|
||||||
</Text>
|
|
||||||
{debugMode && (
|
|
||||||
<Text color={Colors.AccentRed}>
|
|
||||||
{' ' + (debugMessage || '--debug')}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Middle Section: Centered Sandbox Info */}
|
return (
|
||||||
<Box
|
<Box marginTop={1} justifyContent="space-between" width="100%">
|
||||||
flexGrow={1}
|
<Box>
|
||||||
alignItems="center"
|
<Text color={Colors.LightBlue}>
|
||||||
justifyContent="center"
|
{shortenPath(tildeifyPath(targetDir), 70)}
|
||||||
display="flex"
|
{branchName && <Text color={Colors.Gray}> ({branchName}*)</Text>}
|
||||||
>
|
|
||||||
{process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec' ? (
|
|
||||||
<Text color="green">
|
|
||||||
{process.env.SANDBOX.replace(/^gemini-(?:cli-)?/, '')}
|
|
||||||
</Text>
|
</Text>
|
||||||
) : process.env.SANDBOX === 'sandbox-exec' ? (
|
{debugMode && (
|
||||||
<Text color={Colors.AccentYellow}>
|
<Text color={Colors.AccentRed}>
|
||||||
MacOS Seatbelt{' '}
|
{' ' + (debugMessage || '--debug')}
|
||||||
<Text color={Colors.Gray}>({process.env.SEATBELT_PROFILE})</Text>
|
</Text>
|
||||||
</Text>
|
)}
|
||||||
) : (
|
</Box>
|
||||||
<Text color={Colors.AccentRed}>
|
|
||||||
no sandbox <Text color={Colors.Gray}>(see docs)</Text>
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
{/* Right Section: Gemini Label and Console Summary */}
|
{/* Middle Section: Centered Sandbox Info */}
|
||||||
<Box alignItems="center">
|
<Box
|
||||||
<Text color={Colors.AccentBlue}> {model} </Text>
|
flexGrow={1}
|
||||||
{corgiMode && (
|
alignItems="center"
|
||||||
<Text>
|
justifyContent="center"
|
||||||
<Text color={Colors.Gray}>| </Text>
|
display="flex"
|
||||||
<Text color={Colors.AccentRed}>▼</Text>
|
>
|
||||||
<Text color={Colors.Foreground}>(´</Text>
|
{process.env.SANDBOX && process.env.SANDBOX !== 'sandbox-exec' ? (
|
||||||
<Text color={Colors.AccentRed}>ᴥ</Text>
|
<Text color="green">
|
||||||
<Text color={Colors.Foreground}>`)</Text>
|
{process.env.SANDBOX.replace(/^gemini-(?:cli-)?/, '')}
|
||||||
<Text color={Colors.AccentRed}>▼ </Text>
|
</Text>
|
||||||
|
) : process.env.SANDBOX === 'sandbox-exec' ? (
|
||||||
|
<Text color={Colors.AccentYellow}>
|
||||||
|
MacOS Seatbelt{' '}
|
||||||
|
<Text color={Colors.Gray}>({process.env.SEATBELT_PROFILE})</Text>
|
||||||
|
</Text>
|
||||||
|
) : (
|
||||||
|
<Text color={Colors.AccentRed}>
|
||||||
|
no sandbox <Text color={Colors.Gray}>(see docs)</Text>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
{/* Right Section: Gemini Label and Console Summary */}
|
||||||
|
<Box alignItems="center">
|
||||||
|
<Text color={Colors.AccentBlue}>
|
||||||
|
{' '}
|
||||||
|
{model}{' '}
|
||||||
|
<Text color={Colors.Gray}>
|
||||||
|
({((1 - percentage) * 100).toFixed(0)}% context left)
|
||||||
|
</Text>
|
||||||
</Text>
|
</Text>
|
||||||
)}
|
{corgiMode && (
|
||||||
{!showErrorDetails && errorCount > 0 && (
|
<Text>
|
||||||
<Box>
|
<Text color={Colors.Gray}>| </Text>
|
||||||
<Text color={Colors.Gray}>| </Text>
|
<Text color={Colors.AccentRed}>▼</Text>
|
||||||
<ConsoleSummaryDisplay errorCount={errorCount} />
|
<Text color={Colors.Foreground}>(´</Text>
|
||||||
</Box>
|
<Text color={Colors.AccentRed}>ᴥ</Text>
|
||||||
)}
|
<Text color={Colors.Foreground}>`)</Text>
|
||||||
{showMemoryUsage && <MemoryUsageDisplay />}
|
<Text color={Colors.AccentRed}>▼ </Text>
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
{!showErrorDetails && errorCount > 0 && (
|
||||||
|
<Box>
|
||||||
|
<Text color={Colors.Gray}>| </Text>
|
||||||
|
<ConsoleSummaryDisplay errorCount={errorCount} />
|
||||||
|
</Box>
|
||||||
|
)}
|
||||||
|
{showMemoryUsage && <MemoryUsageDisplay />}
|
||||||
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
</Box>
|
);
|
||||||
);
|
};
|
||||||
|
|||||||
@@ -177,6 +177,51 @@ describe('SessionStatsContext', () => {
|
|||||||
expect(stats?.currentTurn.apiTimeMs).toBe(100 + 50);
|
expect(stats?.currentTurn.apiTimeMs).toBe(100 + 50);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should overwrite currentResponse with each API call', () => {
|
||||||
|
const contextRef: MutableRefObject<
|
||||||
|
ReturnType<typeof useSessionStats> | undefined
|
||||||
|
> = { current: undefined };
|
||||||
|
|
||||||
|
render(
|
||||||
|
<SessionStatsProvider>
|
||||||
|
<TestHarness contextRef={contextRef} />
|
||||||
|
</SessionStatsProvider>,
|
||||||
|
);
|
||||||
|
|
||||||
|
// 1. First API call
|
||||||
|
act(() => {
|
||||||
|
contextRef.current?.addUsage({ ...mockMetadata1, apiTimeMs: 100 });
|
||||||
|
});
|
||||||
|
|
||||||
|
let stats = contextRef.current?.stats;
|
||||||
|
|
||||||
|
// currentResponse should match the first call
|
||||||
|
expect(stats?.currentResponse.totalTokenCount).toBe(300);
|
||||||
|
expect(stats?.currentResponse.apiTimeMs).toBe(100);
|
||||||
|
|
||||||
|
// 2. Second API call
|
||||||
|
act(() => {
|
||||||
|
contextRef.current?.addUsage({ ...mockMetadata2, apiTimeMs: 50 });
|
||||||
|
});
|
||||||
|
|
||||||
|
stats = contextRef.current?.stats;
|
||||||
|
|
||||||
|
// currentResponse should now match the second call
|
||||||
|
expect(stats?.currentResponse.totalTokenCount).toBe(30);
|
||||||
|
expect(stats?.currentResponse.apiTimeMs).toBe(50);
|
||||||
|
|
||||||
|
// 3. Start a new turn
|
||||||
|
act(() => {
|
||||||
|
contextRef.current?.startNewTurn();
|
||||||
|
});
|
||||||
|
|
||||||
|
stats = contextRef.current?.stats;
|
||||||
|
|
||||||
|
// currentResponse should be reset
|
||||||
|
expect(stats?.currentResponse.totalTokenCount).toBe(0);
|
||||||
|
expect(stats?.currentResponse.apiTimeMs).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
it('should throw an error when useSessionStats is used outside of a provider', () => {
|
it('should throw an error when useSessionStats is used outside of a provider', () => {
|
||||||
// Suppress the expected console error during this test.
|
// Suppress the expected console error during this test.
|
||||||
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ interface SessionStatsState {
|
|||||||
sessionStartTime: Date;
|
sessionStartTime: Date;
|
||||||
cumulative: CumulativeStats;
|
cumulative: CumulativeStats;
|
||||||
currentTurn: CumulativeStats;
|
currentTurn: CumulativeStats;
|
||||||
|
currentResponse: CumulativeStats;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Defines the final "value" of our context, including the state
|
// Defines the final "value" of our context, including the state
|
||||||
@@ -97,6 +98,16 @@ export const SessionStatsProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
thoughtsTokenCount: 0,
|
thoughtsTokenCount: 0,
|
||||||
apiTimeMs: 0,
|
apiTimeMs: 0,
|
||||||
},
|
},
|
||||||
|
currentResponse: {
|
||||||
|
turnCount: 0,
|
||||||
|
promptTokenCount: 0,
|
||||||
|
candidatesTokenCount: 0,
|
||||||
|
totalTokenCount: 0,
|
||||||
|
cachedContentTokenCount: 0,
|
||||||
|
toolUsePromptTokenCount: 0,
|
||||||
|
thoughtsTokenCount: 0,
|
||||||
|
apiTimeMs: 0,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
// A single, internal worker function to handle all metadata aggregation.
|
// A single, internal worker function to handle all metadata aggregation.
|
||||||
@@ -107,15 +118,27 @@ export const SessionStatsProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
setStats((prevState) => {
|
setStats((prevState) => {
|
||||||
const newCumulative = { ...prevState.cumulative };
|
const newCumulative = { ...prevState.cumulative };
|
||||||
const newCurrentTurn = { ...prevState.currentTurn };
|
const newCurrentTurn = { ...prevState.currentTurn };
|
||||||
|
const newCurrentResponse = {
|
||||||
|
turnCount: 0,
|
||||||
|
promptTokenCount: 0,
|
||||||
|
candidatesTokenCount: 0,
|
||||||
|
totalTokenCount: 0,
|
||||||
|
cachedContentTokenCount: 0,
|
||||||
|
toolUsePromptTokenCount: 0,
|
||||||
|
thoughtsTokenCount: 0,
|
||||||
|
apiTimeMs: 0,
|
||||||
|
};
|
||||||
|
|
||||||
// Add all tokens to the current turn's stats as well as cumulative stats.
|
// Add all tokens to the current turn's stats as well as cumulative stats.
|
||||||
addTokens(newCurrentTurn, metadata);
|
addTokens(newCurrentTurn, metadata);
|
||||||
addTokens(newCumulative, metadata);
|
addTokens(newCumulative, metadata);
|
||||||
|
addTokens(newCurrentResponse, metadata);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...prevState,
|
...prevState,
|
||||||
cumulative: newCumulative,
|
cumulative: newCumulative,
|
||||||
currentTurn: newCurrentTurn,
|
currentTurn: newCurrentTurn,
|
||||||
|
currentResponse: newCurrentResponse,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
@@ -139,6 +162,16 @@ export const SessionStatsProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
thoughtsTokenCount: 0,
|
thoughtsTokenCount: 0,
|
||||||
apiTimeMs: 0,
|
apiTimeMs: 0,
|
||||||
},
|
},
|
||||||
|
currentResponse: {
|
||||||
|
turnCount: 0,
|
||||||
|
promptTokenCount: 0,
|
||||||
|
candidatesTokenCount: 0,
|
||||||
|
totalTokenCount: 0,
|
||||||
|
cachedContentTokenCount: 0,
|
||||||
|
toolUsePromptTokenCount: 0,
|
||||||
|
thoughtsTokenCount: 0,
|
||||||
|
apiTimeMs: 0,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ export * from './core/contentGenerator.js';
|
|||||||
export * from './core/geminiChat.js';
|
export * from './core/geminiChat.js';
|
||||||
export * from './core/logger.js';
|
export * from './core/logger.js';
|
||||||
export * from './core/prompts.js';
|
export * from './core/prompts.js';
|
||||||
|
export * from './core/tokenLimits.js';
|
||||||
export * from './core/turn.js';
|
export * from './core/turn.js';
|
||||||
export * from './core/geminiRequest.js';
|
export * from './core/geminiRequest.js';
|
||||||
export * from './core/coreToolScheduler.js';
|
export * from './core/coreToolScheduler.js';
|
||||||
|
|||||||
Reference in New Issue
Block a user