diff --git a/docs/get-started/configuration.md b/docs/get-started/configuration.md index 633c10b6c7..4864e9a649 100644 --- a/docs/get-started/configuration.md +++ b/docs/get-started/configuration.md @@ -225,6 +225,10 @@ their corresponding top-level category object in your `settings.json` file. - **Description:** Show citations for generated text in the chat. - **Default:** `false` +- **`ui.showModelInfoInChat`** (boolean): + - **Description:** Show the model name in the chat for each model turn. + - **Default:** `false` + - **`ui.useFullWidth`** (boolean): - **Description:** Use the entire width of the terminal for output. - **Default:** `true` diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index fe5670fd16..32a949114e 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -483,6 +483,15 @@ const SETTINGS_SCHEMA = { description: 'Show citations for generated text in the chat.', showInDialog: true, }, + showModelInfoInChat: { + type: 'boolean', + label: 'Show Model Info In Chat', + category: 'UI', + requiresRestart: false, + default: false, + description: 'Show the model name in the chat for each model turn.', + showInDialog: true, + }, useFullWidth: { type: 'boolean', label: 'Use Full Width', diff --git a/packages/cli/src/ui/components/HistoryItemDisplay.tsx b/packages/cli/src/ui/components/HistoryItemDisplay.tsx index 4cc912c3b8..6ef94a6659 100644 --- a/packages/cli/src/ui/components/HistoryItemDisplay.tsx +++ b/packages/cli/src/ui/components/HistoryItemDisplay.tsx @@ -30,6 +30,7 @@ import { getMCPServerStatus } from '@google/gemini-cli-core'; import { ToolsList } from './views/ToolsList.js'; import { McpStatus } from './views/McpStatus.js'; import { ChatList } from './views/ChatList.js'; +import { ModelMessage } from './messages/ModelMessage.js'; interface HistoryItemDisplayProps { item: HistoryItem; @@ -117,6 +118,9 @@ export const HistoryItemDisplay: React.FC = ({ )} {itemForDisplay.type === 'model_stats' && } {itemForDisplay.type === 'tool_stats' && } + {itemForDisplay.type === 'model' && ( + + )} {itemForDisplay.type === 'quit' && ( )} diff --git a/packages/cli/src/ui/components/messages/ModelMessage.tsx b/packages/cli/src/ui/components/messages/ModelMessage.tsx new file mode 100644 index 0000000000..14232a3705 --- /dev/null +++ b/packages/cli/src/ui/components/messages/ModelMessage.tsx @@ -0,0 +1,21 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import type React from 'react'; +import { Text, Box } from 'ink'; +import { theme } from '../../semantic-colors.js'; + +interface ModelMessageProps { + model: string; +} + +export const ModelMessage: React.FC = ({ model }) => ( + + + responding with {model} + + +); diff --git a/packages/cli/src/ui/hooks/useGeminiStream.ts b/packages/cli/src/ui/hooks/useGeminiStream.ts index 2230efc612..d4fd21942d 100644 --- a/packages/cli/src/ui/hooks/useGeminiStream.ts +++ b/packages/cli/src/ui/hooks/useGeminiStream.ts @@ -44,6 +44,7 @@ import type { HistoryItemWithoutId, HistoryItemToolGroup, SlashCommandProcessorResult, + HistoryItemModel, } from '../types.js'; import { StreamingState, MessageType, ToolCallStatus } from '../types.js'; import { isAtCommand, isSlashCommand } from '../utils/commandUtils.js'; @@ -714,6 +715,26 @@ export const useGeminiStream = ( [addItem, onCancelSubmit, config], ); + const handleChatModelEvent = useCallback( + (eventValue: string, userMessageTimestamp: number) => { + if (!settings?.merged?.ui?.showModelInfoInChat) { + return; + } + if (pendingHistoryItemRef.current) { + addItem(pendingHistoryItemRef.current, userMessageTimestamp); + setPendingHistoryItem(null); + } + addItem( + { + type: 'model', + model: eventValue, + } as HistoryItemModel, + userMessageTimestamp, + ); + }, + [addItem, pendingHistoryItemRef, setPendingHistoryItem, settings], + ); + const processGeminiStreamEvents = useCallback( async ( stream: AsyncIterable, @@ -768,6 +789,9 @@ export const useGeminiStream = ( case ServerGeminiEventType.Citation: handleCitationEvent(event.value, userMessageTimestamp); break; + case ServerGeminiEventType.ModelInfo: + handleChatModelEvent(event.value, userMessageTimestamp); + break; case ServerGeminiEventType.LoopDetected: // handle later because we want to move pending history to history // before we add loop detected message to history @@ -799,9 +823,9 @@ export const useGeminiStream = ( handleMaxSessionTurnsEvent, handleContextWindowWillOverflowEvent, handleCitationEvent, + handleChatModelEvent, ], ); - const submitQuery = useCallback( async ( query: PartListUnion, diff --git a/packages/cli/src/ui/types.ts b/packages/cli/src/ui/types.ts index 8eef597d46..fc8008ad6a 100644 --- a/packages/cli/src/ui/types.ts +++ b/packages/cli/src/ui/types.ts @@ -151,6 +151,11 @@ export type HistoryItemToolStats = HistoryItemBase & { type: 'tool_stats'; }; +export type HistoryItemModel = HistoryItemBase & { + type: 'model'; + model: string; +}; + export type HistoryItemQuit = HistoryItemBase & { type: 'quit'; duration: string; @@ -250,6 +255,7 @@ export type HistoryItemWithoutId = | HistoryItemStats | HistoryItemModelStats | HistoryItemToolStats + | HistoryItemModel | HistoryItemQuit | HistoryItemCompression | HistoryItemExtensionsList diff --git a/packages/core/src/core/client.test.ts b/packages/core/src/core/client.test.ts index 66ccb30a1f..746c20d134 100644 --- a/packages/core/src/core/client.test.ts +++ b/packages/core/src/core/client.test.ts @@ -254,6 +254,7 @@ describe('Gemini Client (client.ts)', () => { getSkipNextSpeakerCheck: vi.fn().mockReturnValue(false), getUseSmartEdit: vi.fn().mockReturnValue(false), getUseModelRouter: vi.fn().mockReturnValue(false), + getShowModelInfoInChat: vi.fn().mockReturnValue(false), getContinueOnFailedApiCall: vi.fn(), getProjectRoot: vi.fn().mockReturnValue('/test/project/root'), storage: { @@ -1488,6 +1489,7 @@ ${JSON.stringify( // Assert expect(events).toEqual([ + { type: GeminiEventType.ModelInfo, value: 'default-routed-model' }, { type: GeminiEventType.InvalidStream }, { type: GeminiEventType.Content, value: 'Continued content' }, ]); @@ -1539,7 +1541,10 @@ ${JSON.stringify( const events = await fromAsync(stream); // Assert - expect(events).toEqual([{ type: GeminiEventType.InvalidStream }]); + expect(events).toEqual([ + { type: GeminiEventType.ModelInfo, value: 'default-routed-model' }, + { type: GeminiEventType.InvalidStream }, + ]); // Verify that turn.run was called only once expect(mockTurnRunFn).toHaveBeenCalledTimes(1); @@ -1573,10 +1578,12 @@ ${JSON.stringify( const events = await fromAsync(stream); // Assert - // We expect 2 InvalidStream events (original + 1 retry) - expect(events.length).toBe(2); + // We expect 3 events (model_info + original + 1 retry) + expect(events.length).toBe(3); expect( - events.every((e) => e.type === GeminiEventType.InvalidStream), + events + .filter((e) => e.type !== GeminiEventType.ModelInfo) + .every((e) => e.type === GeminiEventType.InvalidStream), ).toBe(true); // Verify that turn.run was called twice diff --git a/packages/core/src/core/client.ts b/packages/core/src/core/client.ts index fefc95a9fe..aadc847756 100644 --- a/packages/core/src/core/client.ts +++ b/packages/core/src/core/client.ts @@ -521,6 +521,7 @@ export class GeminiClient { modelToUse = decision.model; // Lock the model for the rest of the sequence this.currentSequenceModel = modelToUse; + yield { type: GeminiEventType.ModelInfo, value: modelToUse }; } const resultStream = turn.run(modelToUse, request, linkedSignal); diff --git a/packages/core/src/core/turn.ts b/packages/core/src/core/turn.ts index 681a7e9be2..3a01554030 100644 --- a/packages/core/src/core/turn.ts +++ b/packages/core/src/core/turn.ts @@ -62,6 +62,7 @@ export enum GeminiEventType { Retry = 'retry', ContextWindowWillOverflow = 'context_window_will_overflow', InvalidStream = 'invalid_stream', + ModelInfo = 'model_info', } export type ServerGeminiRetryEvent = { @@ -80,6 +81,11 @@ export type ServerGeminiInvalidStreamEvent = { type: GeminiEventType.InvalidStream; }; +export type ServerGeminiModelInfoEvent = { + type: GeminiEventType.ModelInfo; + value: string; +}; + export interface StructuredError { message: string; status?: number; @@ -212,7 +218,8 @@ export type ServerGeminiStreamEvent = | ServerGeminiUserCancelledEvent | ServerGeminiRetryEvent | ServerGeminiContextWindowWillOverflowEvent - | ServerGeminiInvalidStreamEvent; + | ServerGeminiInvalidStreamEvent + | ServerGeminiModelInfoEvent; // A turn manages the agentic loop turn within the server context. export class Turn { diff --git a/schemas/settings.schema.json b/schemas/settings.schema.json index 81611447c5..bb846b9fc4 100644 --- a/schemas/settings.schema.json +++ b/schemas/settings.schema.json @@ -268,6 +268,13 @@ "default": false, "type": "boolean" }, + "showModelInfoInChat": { + "title": "Show Model Info In Chat", + "description": "Show the model name in the chat for each model turn.", + "markdownDescription": "Show the model name in the chat for each model turn.\n\n- Category: `UI`\n- Requires restart: `no`\n- Default: `false`", + "default": false, + "type": "boolean" + }, "useFullWidth": { "title": "Use Full Width", "description": "Use the entire width of the terminal for output.",