mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-06-12 04:17:15 -07:00
feat(ui): redesign context and compression UI for a more seamless experience
- Added ui.showContextWindowWarning setting (default false). - Implemented forced auto-compression on context overflow when warning is disabled. - Redesigned compression messages to be subtle (gray, no icon, left border). - Removed automatic context usage percentage from the minimal/focus UI. - Changed ui.hideContextSummary default to true. - Updated and verified all relevant tests. Note: Includes a behavioral change where the CLI now attempts to force-compress history when the context window is full rather than blocking by default.
This commit is contained in:
@@ -2103,6 +2103,52 @@ describe('loadCliConfig compressionThreshold', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadCliConfig showContextWindowWarning', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
|
||||
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
|
||||
vi.spyOn(ExtensionManager.prototype, 'getExtensions').mockReturnValue([]);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
vi.unstubAllEnvs();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('should pass showContextWindowWarning from settings to config (true)', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const settings = createTestMergedSettings({
|
||||
ui: {
|
||||
showContextWindowWarning: true,
|
||||
},
|
||||
});
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getShowContextWindowWarning()).toBe(true);
|
||||
});
|
||||
|
||||
it('should pass showContextWindowWarning from settings to config (false)', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const settings = createTestMergedSettings({
|
||||
ui: {
|
||||
showContextWindowWarning: false,
|
||||
},
|
||||
});
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getShowContextWindowWarning()).toBe(false);
|
||||
});
|
||||
|
||||
it('should default to false if not in settings', async () => {
|
||||
process.argv = ['node', 'script.js'];
|
||||
const argv = await parseArguments(createTestMergedSettings());
|
||||
const settings = createTestMergedSettings();
|
||||
const config = await loadCliConfig(settings, 'test-session', argv);
|
||||
expect(config.getShowContextWindowWarning()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadCliConfig useRipgrep', () => {
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
|
||||
@@ -955,6 +955,7 @@ export async function loadCliConfig(
|
||||
bugCommand: settings.advanced?.bugCommand,
|
||||
model: resolvedModel,
|
||||
maxSessionTurns: settings.model?.maxSessionTurns,
|
||||
showContextWindowWarning: settings.ui?.showContextWindowWarning,
|
||||
|
||||
listExtensions: argv.listExtensions || false,
|
||||
listSessions: argv.listSessions || false,
|
||||
|
||||
@@ -575,11 +575,21 @@ const SETTINGS_SCHEMA = {
|
||||
label: 'Hide Context Summary',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
default: true,
|
||||
description:
|
||||
'Hide the context summary (GEMINI.md, MCP servers) above the input.',
|
||||
showInDialog: true,
|
||||
},
|
||||
showContextWindowWarning: {
|
||||
type: 'boolean',
|
||||
label: 'Show Context Window Warning',
|
||||
category: 'UI',
|
||||
requiresRestart: false,
|
||||
default: false,
|
||||
description:
|
||||
'Show a warning message when the context window limit is nearly reached.',
|
||||
showInDialog: true,
|
||||
},
|
||||
footer: {
|
||||
type: 'object',
|
||||
label: 'Footer',
|
||||
|
||||
@@ -39,6 +39,7 @@ describe('compressCommand', () => {
|
||||
originalTokenCount: null,
|
||||
newTokenCount: null,
|
||||
compressionStatus: null,
|
||||
model: 'test-model',
|
||||
},
|
||||
};
|
||||
await compressCommand.action!(context, '');
|
||||
@@ -70,6 +71,7 @@ describe('compressCommand', () => {
|
||||
compressionStatus: null,
|
||||
originalTokenCount: null,
|
||||
newTokenCount: null,
|
||||
model: 'test-model',
|
||||
},
|
||||
});
|
||||
|
||||
@@ -86,6 +88,7 @@ describe('compressCommand', () => {
|
||||
compressionStatus: CompressionStatus.COMPRESSED,
|
||||
originalTokenCount: 200,
|
||||
newTokenCount: 100,
|
||||
model: 'test-model',
|
||||
},
|
||||
},
|
||||
expect.any(Number),
|
||||
|
||||
@@ -33,6 +33,7 @@ export const compressCommand: SlashCommand = {
|
||||
originalTokenCount: null,
|
||||
newTokenCount: null,
|
||||
compressionStatus: null,
|
||||
model: context.services.config?.getModel(),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -53,6 +54,7 @@ export const compressCommand: SlashCommand = {
|
||||
originalTokenCount: compressed.originalTokenCount,
|
||||
newTokenCount: compressed.newTokenCount,
|
||||
compressionStatus: compressed.compressionStatus,
|
||||
model: context.services.config?.getModel(),
|
||||
},
|
||||
} as HistoryItemCompression,
|
||||
Date.now(),
|
||||
|
||||
@@ -17,11 +17,7 @@ import {
|
||||
import { ConfigContext } from '../contexts/ConfigContext.js';
|
||||
import { SettingsContext } from '../contexts/SettingsContext.js';
|
||||
import { createMockSettings } from '../../test-utils/settings.js';
|
||||
import {
|
||||
ApprovalMode,
|
||||
tokenLimit,
|
||||
CoreToolCallStatus,
|
||||
} from '@google/gemini-cli-core';
|
||||
import { ApprovalMode, CoreToolCallStatus } from '@google/gemini-cli-core';
|
||||
import type { Config } from '@google/gemini-cli-core';
|
||||
import { StreamingState } from '../types.js';
|
||||
import { TransientMessageType } from '../../utils/events.js';
|
||||
@@ -733,37 +729,6 @@ describe('Composer', () => {
|
||||
expect(output).toContain('Press Esc again to rewind.');
|
||||
expect(output).not.toContain('ContextSummaryDisplay');
|
||||
});
|
||||
|
||||
it('shows context usage bleed-through when over 60%', async () => {
|
||||
const model = 'gemini-2.5-pro';
|
||||
const uiState = createMockUIState({
|
||||
cleanUiDetailsVisible: false,
|
||||
currentModel: model,
|
||||
sessionStats: {
|
||||
sessionId: 'test-session',
|
||||
sessionStartTime: new Date(),
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
metrics: {} as any,
|
||||
lastPromptTokenCount: Math.floor(tokenLimit(model) * 0.7),
|
||||
promptCount: 0,
|
||||
},
|
||||
});
|
||||
const settings = createMockSettings({
|
||||
ui: {
|
||||
footer: { hideContextPercentage: false },
|
||||
},
|
||||
});
|
||||
|
||||
const { lastFrame } = await renderComposer(uiState, settings);
|
||||
|
||||
await act(async () => {
|
||||
await vi.advanceTimersByTimeAsync(250);
|
||||
});
|
||||
|
||||
// StatusDisplay (which contains ContextUsageDisplay) should bleed through in minimal mode
|
||||
expect(lastFrame()).toContain('StatusDisplay');
|
||||
expect(lastFrame()).toContain('70% used');
|
||||
});
|
||||
});
|
||||
|
||||
describe('Error Details Display', () => {
|
||||
|
||||
@@ -20,13 +20,11 @@ import { useVimMode } from '../contexts/VimModeContext.js';
|
||||
import { useAlternateBuffer } from '../hooks/useAlternateBuffer.js';
|
||||
import { useTerminalSize } from '../hooks/useTerminalSize.js';
|
||||
import { isNarrowWidth } from '../utils/isNarrowWidth.js';
|
||||
import { isContextUsageHigh } from '../utils/contextUsage.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
import { GENERIC_WORKING_LABEL } from '../textConstants.js';
|
||||
import { INTERACTIVE_SHELL_WAITING_PHRASE } from '../hooks/usePhraseCycler.js';
|
||||
import { StreamingState, type HistoryItemToolGroup } from '../types.js';
|
||||
import { LoadingIndicator } from './LoadingIndicator.js';
|
||||
import { ContextUsageDisplay } from './ContextUsageDisplay.js';
|
||||
import { StatusDisplay } from './StatusDisplay.js';
|
||||
import { HorizontalLine } from './shared/HorizontalLine.js';
|
||||
import { ToastDisplay, shouldShowToast } from './ToastDisplay.js';
|
||||
@@ -257,11 +255,6 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
const miniMode_ShowShortcuts = shouldReserveSpaceForShortcutsHint;
|
||||
const miniMode_ShowStatus = showLoadingIndicator || hasAnyHooks;
|
||||
const miniMode_ShowTip = showTipLine;
|
||||
const miniMode_ShowContext = isContextUsageHigh(
|
||||
uiState.sessionStats.lastPromptTokenCount,
|
||||
uiState.currentModel,
|
||||
settings.merged.model?.compressionThreshold,
|
||||
);
|
||||
|
||||
// Composite Mini Mode Triggers
|
||||
const showRow1_MiniMode =
|
||||
@@ -270,7 +263,7 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
miniMode_ShowShortcuts ||
|
||||
miniMode_ShowTip;
|
||||
|
||||
const showRow2_MiniMode = miniMode_ShowApprovalMode || miniMode_ShowContext;
|
||||
const showRow2_MiniMode = miniMode_ShowApprovalMode;
|
||||
|
||||
// Final Display Rules (Stable Footer Architecture)
|
||||
const showRow1 = showUiDetails || showRow1_MiniMode;
|
||||
@@ -488,22 +481,9 @@ export const Composer = ({ isFocused = true }: { isFocused?: boolean }) => {
|
||||
alignItems="center"
|
||||
marginLeft={isNarrow ? 1 : 0}
|
||||
>
|
||||
{(showUiDetails || miniMode_ShowContext) && (
|
||||
{showUiDetails && (
|
||||
<StatusDisplay hideContextSummary={hideContextSummary} />
|
||||
)}
|
||||
{miniMode_ShowContext && !showUiDetails && (
|
||||
<Box marginLeft={1}>
|
||||
<ContextUsageDisplay
|
||||
promptTokenCount={uiState.sessionStats.lastPromptTokenCount}
|
||||
model={
|
||||
typeof uiState.currentModel === 'string'
|
||||
? uiState.currentModel
|
||||
: undefined
|
||||
}
|
||||
terminalWidth={uiState.terminalWidth}
|
||||
/>
|
||||
</Box>
|
||||
)}
|
||||
</Box>
|
||||
</Box>
|
||||
)}
|
||||
|
||||
@@ -72,7 +72,9 @@ const createMockConfig = (overrides = {}) => ({
|
||||
const renderStatusDisplay = async (
|
||||
props: { hideContextSummary: boolean } = { hideContextSummary: false },
|
||||
uiState: UIState = createMockUIState(),
|
||||
settings = createMockSettings(),
|
||||
settings = createMockSettings({
|
||||
ui: { hideContextSummary: true },
|
||||
}),
|
||||
config = createMockConfig(),
|
||||
) => {
|
||||
const result = await render(
|
||||
@@ -97,16 +99,21 @@ describe('StatusDisplay', () => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
it('renders nothing by default if context summary is hidden via props', async () => {
|
||||
const { lastFrame, unmount } = await renderStatusDisplay({
|
||||
hideContextSummary: true,
|
||||
});
|
||||
it('renders nothing by default', async () => {
|
||||
const { lastFrame, unmount } = await renderStatusDisplay();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
it('renders ContextSummaryDisplay by default', async () => {
|
||||
const { lastFrame, unmount } = await renderStatusDisplay();
|
||||
it('renders ContextSummaryDisplay when hideContextSummary is false', async () => {
|
||||
const settings = createMockSettings({
|
||||
ui: { hideContextSummary: false },
|
||||
});
|
||||
const { lastFrame, unmount } = await renderStatusDisplay(
|
||||
{ hideContextSummary: false },
|
||||
undefined,
|
||||
settings,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
unmount();
|
||||
});
|
||||
@@ -135,6 +142,7 @@ describe('StatusDisplay', () => {
|
||||
activeHooks: [{ name: 'hook', eventName: 'event' }],
|
||||
});
|
||||
const settings = createMockSettings({
|
||||
ui: { hideContextSummary: true },
|
||||
hooksConfig: { notifications: false },
|
||||
});
|
||||
const { lastFrame, unmount } = await renderStatusDisplay(
|
||||
@@ -142,7 +150,7 @@ describe('StatusDisplay', () => {
|
||||
uiState,
|
||||
settings,
|
||||
);
|
||||
expect(lastFrame()).toMatchSnapshot();
|
||||
expect(lastFrame({ allowEmpty: true })).toBe('');
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -163,9 +171,13 @@ describe('StatusDisplay', () => {
|
||||
const uiState = createMockUIState({
|
||||
backgroundShellCount: 3,
|
||||
});
|
||||
const settings = createMockSettings({
|
||||
ui: { hideContextSummary: false },
|
||||
});
|
||||
const { lastFrame, unmount } = await renderStatusDisplay(
|
||||
{ hideContextSummary: false },
|
||||
uiState,
|
||||
settings,
|
||||
);
|
||||
expect(lastFrame()).toContain('Shells: 3');
|
||||
unmount();
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
|
||||
|
||||
exports[`StatusDisplay > does NOT render HookStatusDisplay if notifications are disabled in settings 1`] = `
|
||||
"Mock Context Summary Display (Skills: 2, Shells: 0)
|
||||
"
|
||||
`;
|
||||
|
||||
exports[`StatusDisplay > renders ContextSummaryDisplay by default 1`] = `
|
||||
exports[`StatusDisplay > renders ContextSummaryDisplay when hideContextSummary is false 1`] = `
|
||||
"Mock Context Summary Display (Skills: 2, Shells: 0)
|
||||
"
|
||||
`;
|
||||
|
||||
@@ -52,9 +52,9 @@ describe('<CompressionMessage />', () => {
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('✦');
|
||||
expect(output).not.toContain('✦');
|
||||
expect(output).toContain(
|
||||
'Chat history compressed from 100 to 50 tokens.',
|
||||
'Context compressed from 100 tokens to 50 tokens. Change threshold in /settings.',
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
@@ -76,9 +76,9 @@ describe('<CompressionMessage />', () => {
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('✦');
|
||||
expect(output).not.toContain('✦');
|
||||
expect(output).toContain(
|
||||
`compressed from ${original} to ${newTokens} tokens`,
|
||||
`Context compressed from ${original.toLocaleString()} tokens to ${newTokens.toLocaleString()} tokens. Change threshold in /settings.`,
|
||||
);
|
||||
expect(output).not.toContain('Skipping compression');
|
||||
expect(output).not.toContain('did not reduce size');
|
||||
@@ -101,7 +101,7 @@ describe('<CompressionMessage />', () => {
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('✦');
|
||||
expect(output).not.toContain('✦');
|
||||
expect(output).toContain(
|
||||
'Compression was not beneficial for this history size.',
|
||||
);
|
||||
@@ -133,17 +133,19 @@ describe('<CompressionMessage />', () => {
|
||||
{
|
||||
original: 200,
|
||||
newTokens: 80,
|
||||
expected: 'compressed from 200 to 80 tokens',
|
||||
expected:
|
||||
'Context compressed from 200 tokens to 80 tokens. Change threshold in /settings.',
|
||||
},
|
||||
{
|
||||
original: 500,
|
||||
newTokens: 150,
|
||||
expected: 'compressed from 500 to 150 tokens',
|
||||
expected:
|
||||
'Context compressed from 500 tokens to 150 tokens. Change threshold in /settings.',
|
||||
},
|
||||
{
|
||||
original: 1500,
|
||||
newTokens: 400,
|
||||
expected: 'compressed from 1500 to 400 tokens',
|
||||
expected: `Context compressed from ${(1500).toLocaleString()} tokens to 400 tokens. Change threshold in /settings.`,
|
||||
},
|
||||
])(
|
||||
'displays correct compression statistics (from $original to $newTokens)',
|
||||
@@ -229,9 +231,9 @@ describe('<CompressionMessage />', () => {
|
||||
);
|
||||
const output = lastFrame();
|
||||
|
||||
expect(output).toContain('✦');
|
||||
expect(output).not.toContain('✦');
|
||||
expect(output).toContain(
|
||||
'Chat history compression failed: the model returned an empty summary.',
|
||||
'Chat history compression failed: empty summary.',
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { CompressionProps } from '../../types.js';
|
||||
import { CliSpinner } from '../CliSpinner.js';
|
||||
import { theme } from '../../semantic-colors.js';
|
||||
import { SCREEN_READER_MODEL_PREFIX } from '../../textConstants.js';
|
||||
import { CompressionStatus } from '@google/gemini-cli-core';
|
||||
import { CompressionStatus, tokenLimit } from '@google/gemini-cli-core';
|
||||
|
||||
export interface CompressionDisplayProps {
|
||||
compression: CompressionProps;
|
||||
@@ -22,8 +22,13 @@ export interface CompressionDisplayProps {
|
||||
export function CompressionMessage({
|
||||
compression,
|
||||
}: CompressionDisplayProps): React.JSX.Element {
|
||||
const { isPending, originalTokenCount, newTokenCount, compressionStatus } =
|
||||
compression;
|
||||
const {
|
||||
isPending,
|
||||
originalTokenCount,
|
||||
newTokenCount,
|
||||
compressionStatus,
|
||||
model,
|
||||
} = compression;
|
||||
|
||||
const originalTokens = originalTokenCount ?? 0;
|
||||
const newTokens = newTokenCount ?? 0;
|
||||
@@ -33,9 +38,15 @@ export function CompressionMessage({
|
||||
return 'Compressing chat history';
|
||||
}
|
||||
|
||||
const limit = model ? tokenLimit(model) : 0;
|
||||
const formatPercent = (tokens: number) =>
|
||||
limit > 0
|
||||
? `${Math.round((tokens / limit) * 100)}% (${tokens.toLocaleString()} tokens)`
|
||||
: `${tokens.toLocaleString()} tokens`;
|
||||
|
||||
switch (compressionStatus) {
|
||||
case CompressionStatus.COMPRESSED:
|
||||
return `Chat history compressed from ${originalTokens} to ${newTokens} tokens.`;
|
||||
return `Context compressed from ${formatPercent(originalTokens)} to ${formatPercent(newTokens)}. Change threshold in /settings.`;
|
||||
case CompressionStatus.COMPRESSION_FAILED_INFLATED_TOKEN_COUNT:
|
||||
// For smaller histories (< 50k tokens), compression overhead likely exceeds benefits
|
||||
if (originalTokens < 50000) {
|
||||
@@ -43,11 +54,11 @@ export function CompressionMessage({
|
||||
}
|
||||
// For larger histories where compression should work but didn't,
|
||||
// this suggests an issue with the compression process itself
|
||||
return 'Chat history compression did not reduce size. This may indicate issues with the compression prompt.';
|
||||
return 'Chat history compression did not reduce size.';
|
||||
case CompressionStatus.COMPRESSION_FAILED_TOKEN_COUNT_ERROR:
|
||||
return 'Could not compress chat history due to a token counting error.';
|
||||
case CompressionStatus.COMPRESSION_FAILED_EMPTY_SUMMARY:
|
||||
return 'Chat history compression failed: the model returned an empty summary.';
|
||||
return 'Chat history compression failed: empty summary.';
|
||||
case CompressionStatus.NOOP:
|
||||
return 'Nothing to compress.';
|
||||
default:
|
||||
@@ -58,20 +69,13 @@ export function CompressionMessage({
|
||||
const text = getCompressionText();
|
||||
|
||||
return (
|
||||
<Box flexDirection="row">
|
||||
<Box marginRight={1}>
|
||||
{isPending ? (
|
||||
<CliSpinner type="dots" />
|
||||
) : (
|
||||
<Text color={theme.text.accent}>✦</Text>
|
||||
)}
|
||||
</Box>
|
||||
<Box flexDirection="row" paddingLeft={1} marginBottom={1}>
|
||||
<Box marginRight={1}>{isPending && <CliSpinner type="dots" />}</Box>
|
||||
<Box>
|
||||
<Text
|
||||
color={
|
||||
compression.isPending ? theme.text.accent : theme.status.success
|
||||
}
|
||||
color={theme.text.secondary}
|
||||
aria-label={SCREEN_READER_MODEL_PREFIX}
|
||||
italic
|
||||
>
|
||||
{text}
|
||||
</Text>
|
||||
|
||||
@@ -64,7 +64,6 @@ import { MessageType, StreamingState } from '../types.js';
|
||||
|
||||
import type { LoadedSettings } from '../../config/settings.js';
|
||||
import { findLastSafeSplitPoint } from '../utils/markdownUtilities.js';
|
||||
import { theme } from '../semantic-colors.js';
|
||||
|
||||
// --- MOCKS ---
|
||||
const mockSendMessageStream = vi
|
||||
@@ -332,6 +331,7 @@ describe('useGeminiStream', () => {
|
||||
})),
|
||||
getIdeMode: vi.fn(() => false),
|
||||
getEnableHooks: vi.fn(() => false),
|
||||
getShowContextWindowWarning: vi.fn(() => false),
|
||||
} as unknown as Config;
|
||||
|
||||
beforeEach(() => {
|
||||
@@ -2474,22 +2474,30 @@ describe('useGeminiStream', () => {
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: 'without suggestion when remaining tokens are > 75% of limit',
|
||||
name: 'NOT add a message when showContextWindowWarning is false',
|
||||
requestTokens: 20,
|
||||
remainingTokens: 80,
|
||||
expectedMessage:
|
||||
'Sending this message (20 tokens) might exceed the context window limit (80 tokens left).',
|
||||
shouldShow: false,
|
||||
},
|
||||
{
|
||||
name: 'with suggestion when remaining tokens are < 75% of limit',
|
||||
name: 'add a message when showContextWindowWarning is true',
|
||||
requestTokens: 30,
|
||||
remainingTokens: 70,
|
||||
shouldShow: true,
|
||||
expectedMessage:
|
||||
'Sending this message (30 tokens) might exceed the context window limit (70 tokens left). Please try reducing the size of your message or use the `/compress` command to compress the chat history.',
|
||||
'Context window is 30% full. Message size (30 tokens) might exceed the limit.\nPlease try reducing the size of your message or use the /compress command to compress the chat history.',
|
||||
},
|
||||
])(
|
||||
'should add message $name',
|
||||
async ({ requestTokens, remainingTokens, expectedMessage }) => {
|
||||
'should $name',
|
||||
async ({
|
||||
requestTokens,
|
||||
remainingTokens,
|
||||
shouldShow,
|
||||
expectedMessage,
|
||||
}) => {
|
||||
vi.mocked(mockConfig.getShowContextWindowWarning).mockReturnValue(
|
||||
shouldShow,
|
||||
);
|
||||
mockSendMessageStream.mockReturnValue(
|
||||
(async function* () {
|
||||
yield {
|
||||
@@ -2509,10 +2517,18 @@ describe('useGeminiStream', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith({
|
||||
type: 'info',
|
||||
text: expectedMessage,
|
||||
});
|
||||
if (shouldShow) {
|
||||
expect(mockAddItem).toHaveBeenCalledWith({
|
||||
type: 'info',
|
||||
text: expectedMessage,
|
||||
});
|
||||
} else {
|
||||
expect(mockAddItem).not.toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: 'info',
|
||||
}),
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
@@ -2593,10 +2609,14 @@ describe('useGeminiStream', () => {
|
||||
await waitFor(() => {
|
||||
expect(mockAddItem).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
type: MessageType.INFO,
|
||||
text: 'Context compressed from 10% to 5%.',
|
||||
secondaryText: 'Change threshold in /settings.',
|
||||
color: theme.status.warning,
|
||||
type: MessageType.COMPRESSION,
|
||||
compression: {
|
||||
isPending: false,
|
||||
originalTokenCount: 1000,
|
||||
newTokenCount: 500,
|
||||
compressionStatus: 'compressed',
|
||||
model: 'gemini-2.5-pro',
|
||||
},
|
||||
}),
|
||||
expect.any(Number),
|
||||
);
|
||||
|
||||
@@ -61,7 +61,6 @@ import type {
|
||||
HistoryItemThinking,
|
||||
HistoryItemWithoutId,
|
||||
HistoryItemToolGroup,
|
||||
HistoryItemInfo,
|
||||
IndividualToolCallDisplay,
|
||||
SlashCommandProcessorResult,
|
||||
HistoryItemModel,
|
||||
@@ -1144,22 +1143,18 @@ export const useGeminiStream = (
|
||||
setPendingHistoryItem(null);
|
||||
}
|
||||
|
||||
const limit = tokenLimit(config.getModel());
|
||||
const originalPercentage = Math.round(
|
||||
((eventValue?.originalTokenCount ?? 0) / limit) * 100,
|
||||
);
|
||||
const newPercentage = Math.round(
|
||||
((eventValue?.newTokenCount ?? 0) / limit) * 100,
|
||||
);
|
||||
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: `Context compressed from ${originalPercentage}% to ${newPercentage}%.`,
|
||||
secondaryText: `Change threshold in /settings.`,
|
||||
color: theme.status.warning,
|
||||
marginBottom: 1,
|
||||
} as HistoryItemInfo,
|
||||
type: 'compression',
|
||||
compression: {
|
||||
isPending: false,
|
||||
originalTokenCount: eventValue?.originalTokenCount ?? null,
|
||||
newTokenCount: eventValue?.newTokenCount ?? null,
|
||||
compressionStatus: eventValue?.compressionStatus ?? null,
|
||||
model: config.getModel(),
|
||||
},
|
||||
timestamp: new Date(userMessageTimestamp),
|
||||
} as HistoryItemWithoutId,
|
||||
userMessageTimestamp,
|
||||
);
|
||||
},
|
||||
@@ -1181,18 +1176,17 @@ export const useGeminiStream = (
|
||||
(estimatedRequestTokenCount: number, remainingTokenCount: number) => {
|
||||
onCancelSubmit(true);
|
||||
|
||||
const limit = tokenLimit(config.getModel());
|
||||
|
||||
const isMoreThan25PercentUsed =
|
||||
limit > 0 && remainingTokenCount < limit * 0.75;
|
||||
|
||||
let text = `Sending this message (${estimatedRequestTokenCount} tokens) might exceed the context window limit (${remainingTokenCount.toLocaleString()} tokens left).`;
|
||||
|
||||
if (isMoreThan25PercentUsed) {
|
||||
text +=
|
||||
' Please try reducing the size of your message or use the `/compress` command to compress the chat history.';
|
||||
if (!config.getShowContextWindowWarning()) {
|
||||
return;
|
||||
}
|
||||
|
||||
const limit = tokenLimit(config.getModel());
|
||||
const usedPercentage = Math.round(
|
||||
((limit - remainingTokenCount) / limit) * 100,
|
||||
);
|
||||
|
||||
const text = `Context window is ${usedPercentage}% full. Message size (${estimatedRequestTokenCount.toLocaleString()} tokens) might exceed the limit.\nPlease try reducing the size of your message or use the /compress command to compress the chat history.`;
|
||||
|
||||
addItem({
|
||||
type: 'info',
|
||||
text,
|
||||
|
||||
@@ -141,6 +141,7 @@ export interface CompressionProps {
|
||||
originalTokenCount: number | null;
|
||||
newTokenCount: number | null;
|
||||
compressionStatus: CompressionStatus | null;
|
||||
model?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user