From bfcaea596599f9c06162dbfb06e8cdce55c7b567 Mon Sep 17 00:00:00 2001 From: bl-ue <54780737+bl-ue@users.noreply.github.com> Date: Mon, 17 Nov 2025 18:33:55 -0700 Subject: [PATCH 1/6] feat: make /clear start a new chat recording session --- packages/cli/src/ui/commands/clearCommand.test.ts | 5 +++++ packages/cli/src/ui/commands/clearCommand.ts | 13 +++++++++++++ 2 files changed, 18 insertions(+) diff --git a/packages/cli/src/ui/commands/clearCommand.test.ts b/packages/cli/src/ui/commands/clearCommand.test.ts index 5a261caa639..859c04a2314 100644 --- a/packages/cli/src/ui/commands/clearCommand.test.ts +++ b/packages/cli/src/ui/commands/clearCommand.test.ts @@ -30,6 +30,7 @@ describe('clearCommand', () => { beforeEach(() => { mockResetChat = vi.fn().mockResolvedValue(undefined); + const mockGetChatRecordingService = vi.fn(); vi.clearAllMocks(); mockContext = createMockCommandContext({ @@ -38,7 +39,11 @@ describe('clearCommand', () => { getGeminiClient: () => ({ resetChat: mockResetChat, + getChat: () => ({ + getChatRecordingService: mockGetChatRecordingService, + }), }) as unknown as GeminiClient, + setSessionId: vi.fn(), }, }, }); diff --git a/packages/cli/src/ui/commands/clearCommand.ts b/packages/cli/src/ui/commands/clearCommand.ts index c3f05859512..eca35a58fda 100644 --- a/packages/cli/src/ui/commands/clearCommand.ts +++ b/packages/cli/src/ui/commands/clearCommand.ts @@ -7,6 +7,7 @@ import { uiTelemetryService } from '@google/gemini-cli-core'; import type { SlashCommand } from './types.js'; import { CommandKind } from './types.js'; +import { randomUUID } from 'node:crypto'; export const clearCommand: SlashCommand = { name: 'clear', @@ -14,6 +15,11 @@ export const clearCommand: SlashCommand = { kind: CommandKind.BUILT_IN, action: async (context, _args) => { const geminiClient = context.services.config?.getGeminiClient(); + const config = context.services.config; + const chatRecordingService = context.services.config + ?.getGeminiClient() + ?.getChat() + .getChatRecordingService(); if (geminiClient) { context.ui.setDebugMessage('Clearing terminal and resetting chat.'); @@ -24,6 +30,13 @@ export const clearCommand: SlashCommand = { context.ui.setDebugMessage('Clearing terminal.'); } + // Start a new conversation recording with a new session ID + if (config && chatRecordingService) { + const newSessionId = randomUUID(); + config.setSessionId(newSessionId); + chatRecordingService.initialize(); + } + uiTelemetryService.setLastPromptTokenCount(0); context.ui.clear(); }, From e47e1d3fa951b55ce717018e1ec7edd18de717d5 Mon Sep 17 00:00:00 2001 From: bl-ue <54780737+bl-ue@users.noreply.github.com> Date: Tue, 18 Nov 2025 09:31:29 -0700 Subject: [PATCH 2/6] Add an isResuming parameter to useHistory --- packages/cli/src/ui/hooks/slashCommandProcessor.ts | 10 ++++++++-- packages/cli/src/ui/hooks/useEditorSettings.test.tsx | 7 +++---- packages/cli/src/ui/hooks/useEditorSettings.ts | 5 +++-- packages/cli/src/ui/hooks/useHistoryManager.ts | 12 ++++++++++-- packages/cli/src/ui/hooks/useSessionResume.test.ts | 4 ++++ packages/cli/src/ui/hooks/useSessionResume.ts | 2 +- packages/cli/src/ui/hooks/useThemeCommand.ts | 5 +++-- 7 files changed, 32 insertions(+), 13 deletions(-) diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index 3484436fa42..6c1e2dd72ac 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -307,6 +307,7 @@ export const useSlashCommandProcessor = ( async ( rawQuery: PartListUnion, oneTimeShellAllowlist?: Set, + addToHistory: boolean = true, overwriteConfirmed?: boolean, ): Promise => { if (!commands) { @@ -323,8 +324,13 @@ export const useSlashCommandProcessor = ( setIsProcessing(true); - const userMessageTimestamp = Date.now(); - addItem({ type: MessageType.USER, text: trimmed }, userMessageTimestamp); + if (addToHistory) { + const userMessageTimestamp = Date.now(); + addItem( + { type: MessageType.USER, text: trimmed }, + userMessageTimestamp, + ); + } let hasError = false; const { diff --git a/packages/cli/src/ui/hooks/useEditorSettings.test.tsx b/packages/cli/src/ui/hooks/useEditorSettings.test.tsx index da532c4d012..8ffcd378ef2 100644 --- a/packages/cli/src/ui/hooks/useEditorSettings.test.tsx +++ b/packages/cli/src/ui/hooks/useEditorSettings.test.tsx @@ -21,12 +21,13 @@ import type { LoadedSettings, } from '../../config/settings.js'; import { SettingScope } from '../../config/settings.js'; -import { MessageType, type HistoryItem } from '../types.js'; +import { MessageType } from '../types.js'; import { type EditorType, checkHasEditorType, allowEditorTypeInSandbox, } from '@google/gemini-cli-core'; +import type { UseHistoryManagerReturn } from './useHistoryManager.js'; import { SettingPaths } from '../../config/settingPaths.js'; @@ -45,9 +46,7 @@ const mockAllowEditorTypeInSandbox = vi.mocked(allowEditorTypeInSandbox); describe('useEditorSettings', () => { let mockLoadedSettings: LoadedSettings; let mockSetEditorError: MockedFunction<(error: string | null) => void>; - let mockAddItem: MockedFunction< - (item: Omit, timestamp: number) => void - >; + let mockAddItem: MockedFunction; let result: ReturnType; function TestComponent() { diff --git a/packages/cli/src/ui/hooks/useEditorSettings.ts b/packages/cli/src/ui/hooks/useEditorSettings.ts index aebe9fe6426..f9af11a8d83 100644 --- a/packages/cli/src/ui/hooks/useEditorSettings.ts +++ b/packages/cli/src/ui/hooks/useEditorSettings.ts @@ -9,12 +9,13 @@ import type { LoadableSettingScope, LoadedSettings, } from '../../config/settings.js'; -import { type HistoryItem, MessageType } from '../types.js'; +import { MessageType } from '../types.js'; import type { EditorType } from '@google/gemini-cli-core'; import { allowEditorTypeInSandbox, checkHasEditorType, } from '@google/gemini-cli-core'; +import type { UseHistoryManagerReturn } from './useHistoryManager.js'; import { SettingPaths } from '../../config/settingPaths.js'; @@ -31,7 +32,7 @@ interface UseEditorSettingsReturn { export const useEditorSettings = ( loadedSettings: LoadedSettings, setEditorError: (error: string | null) => void, - addItem: (item: Omit, timestamp: number) => void, + addItem: UseHistoryManagerReturn['addItem'], ): UseEditorSettingsReturn => { const [isEditorDialogOpen, setIsEditorDialogOpen] = useState(false); diff --git a/packages/cli/src/ui/hooks/useHistoryManager.ts b/packages/cli/src/ui/hooks/useHistoryManager.ts index c25fc84a29d..a5c272d5aa0 100644 --- a/packages/cli/src/ui/hooks/useHistoryManager.ts +++ b/packages/cli/src/ui/hooks/useHistoryManager.ts @@ -14,7 +14,11 @@ type HistoryItemUpdater = ( export interface UseHistoryManagerReturn { history: HistoryItem[]; - addItem: (itemData: Omit, baseTimestamp: number) => number; // Returns the generated ID + addItem: ( + itemData: Omit, + baseTimestamp: number, + isResuming?: boolean, + ) => number; // Returns the generated ID updateItem: ( id: number, updates: Partial> | HistoryItemUpdater, @@ -45,7 +49,11 @@ export function useHistory(): UseHistoryManagerReturn { // Adds a new item to the history state with a unique ID. const addItem = useCallback( - (itemData: Omit, baseTimestamp: number): number => { + ( + itemData: Omit, + baseTimestamp: number, + isResuming: boolean = false, + ): number => { const id = getNextMessageId(baseTimestamp); const newItem: HistoryItem = { ...itemData, id } as HistoryItem; diff --git a/packages/cli/src/ui/hooks/useSessionResume.test.ts b/packages/cli/src/ui/hooks/useSessionResume.test.ts index 93787b8d50f..e135006471e 100644 --- a/packages/cli/src/ui/hooks/useSessionResume.test.ts +++ b/packages/cli/src/ui/hooks/useSessionResume.test.ts @@ -101,11 +101,13 @@ describe('useSessionResume', () => { 1, { type: 'user', text: 'Hello' }, 0, + true, ); expect(mockHistoryManager.addItem).toHaveBeenNthCalledWith( 2, { type: 'gemini', text: 'Hi there!' }, 1, + true, ); expect(mockRefreshStatic).toHaveBeenCalled(); expect(mockGeminiClient.resumeChat).toHaveBeenCalledWith( @@ -328,11 +330,13 @@ describe('useSessionResume', () => { 1, { type: 'user', text: 'Hello from resumed session' }, 0, + true, ); expect(mockHistoryManager.addItem).toHaveBeenNthCalledWith( 2, { type: 'gemini', text: 'Welcome back!' }, 1, + true, ); expect(mockGeminiClient.resumeChat).toHaveBeenCalled(); }); diff --git a/packages/cli/src/ui/hooks/useSessionResume.ts b/packages/cli/src/ui/hooks/useSessionResume.ts index e738b5ce56c..9eea8726d39 100644 --- a/packages/cli/src/ui/hooks/useSessionResume.ts +++ b/packages/cli/src/ui/hooks/useSessionResume.ts @@ -59,7 +59,7 @@ export function useSessionResume({ setQuittingMessages(null); historyManagerRef.current.clearItems(); uiHistory.forEach((item, index) => { - historyManagerRef.current.addItem(item, index); + historyManagerRef.current.addItem(item, index, true); }); refreshStaticRef.current(); // Force Static component to re-render with the updated history. diff --git a/packages/cli/src/ui/hooks/useThemeCommand.ts b/packages/cli/src/ui/hooks/useThemeCommand.ts index 72133e9b11b..47252b9aff7 100644 --- a/packages/cli/src/ui/hooks/useThemeCommand.ts +++ b/packages/cli/src/ui/hooks/useThemeCommand.ts @@ -10,8 +10,9 @@ import type { LoadableSettingScope, LoadedSettings, } from '../../config/settings.js'; // Import LoadedSettings, AppSettings, MergedSetting -import { type HistoryItem, MessageType } from '../types.js'; +import { MessageType } from '../types.js'; import process from 'node:process'; +import type { UseHistoryManagerReturn } from './useHistoryManager.js'; interface UseThemeCommandReturn { isThemeDialogOpen: boolean; @@ -24,7 +25,7 @@ interface UseThemeCommandReturn { export const useThemeCommand = ( loadedSettings: LoadedSettings, setThemeError: (error: string | null) => void, - addItem: (item: Omit, timestamp: number) => void, + addItem: UseHistoryManagerReturn['addItem'], initialThemeError: string | null, ): UseThemeCommandReturn => { const [isThemeDialogOpen, setIsThemeDialogOpen] = From fb40505b916e53633bc47951df5a104f39f383cc Mon Sep 17 00:00:00 2001 From: bl-ue <54780737+bl-ue@users.noreply.github.com> Date: Tue, 18 Nov 2025 10:31:07 -0700 Subject: [PATCH 3/6] Make convertSessionToHistoryFormats understand system, error, and warning messages --- .../src/ui/hooks/useSessionBrowser.test.ts | 41 +++++++++++++++++++ .../cli/src/ui/hooks/useSessionBrowser.ts | 18 +++++--- .../core/src/services/chatRecordingService.ts | 2 +- 3 files changed, 55 insertions(+), 6 deletions(-) diff --git a/packages/cli/src/ui/hooks/useSessionBrowser.test.ts b/packages/cli/src/ui/hooks/useSessionBrowser.test.ts index 6cf878dcd12..3face81079e 100644 --- a/packages/cli/src/ui/hooks/useSessionBrowser.test.ts +++ b/packages/cli/src/ui/hooks/useSessionBrowser.test.ts @@ -55,6 +55,47 @@ describe('convertSessionToHistoryFormats', () => { }); }); + it('should convert system, warning, and error messages to appropriate types', () => { + const messages: MessageRecord[] = [ + { + id: 'msg-1', + timestamp: '2025-01-01T00:01:00Z', + content: 'System message', + type: 'info', + }, + { + id: 'msg-2', + timestamp: '2025-01-01T00:02:00Z', + content: 'Warning message', + type: 'warning', + }, + { + id: 'msg-3', + timestamp: '2025-01-01T00:03:00Z', + content: 'Error occurred', + type: 'error', + }, + ]; + + const result = convertSessionToHistoryFormats(messages); + + expect(result.uiHistory[0]).toEqual({ + type: MessageType.INFO, + text: 'System message', + }); + expect(result.uiHistory[1]).toEqual({ + type: MessageType.WARNING, + text: 'Warning message', + }); + expect(result.uiHistory[2]).toEqual({ + type: MessageType.ERROR, + text: 'Error occurred', + }); + + // System, warning, and error messages should not be included in client history + expect(result.clientHistory).toEqual([]); + }); + it('should filter out slash commands from client history', () => { const messages: MessageRecord[] = [ { diff --git a/packages/cli/src/ui/hooks/useSessionBrowser.ts b/packages/cli/src/ui/hooks/useSessionBrowser.ts index 9f159aac4dd..69a787b0306 100644 --- a/packages/cli/src/ui/hooks/useSessionBrowser.ts +++ b/packages/cli/src/ui/hooks/useSessionBrowser.ts @@ -29,6 +29,15 @@ export function convertSessionToHistoryFormats( case 'user': messageType = MessageType.USER; break; + case 'info': + messageType = MessageType.INFO; + break; + case 'error': + messageType = MessageType.ERROR; + break; + case 'warning': + messageType = MessageType.WARNING; + break; default: messageType = MessageType.GEMINI; break; @@ -70,9 +79,9 @@ export function convertSessionToHistoryFormats( for (const msg of messages) { // Skip system/error messages and user slash commands - // if (msg.type === 'system' || msg.type === 'error') { - // continue; - // } + if (msg.type === 'info' || msg.type === 'error' || msg.type === 'warning') { + continue; + } if (msg.type === 'user') { // Skip user slash commands @@ -91,8 +100,7 @@ export function convertSessionToHistoryFormats( }); } else if (msg.type === 'gemini') { // Handle Gemini messages with potential tool calls - const hasToolCalls = - 'toolCalls' in msg && msg.toolCalls && msg.toolCalls.length > 0; + const hasToolCalls = msg.toolCalls && msg.toolCalls.length > 0; if (hasToolCalls) { // Create model message with function calls diff --git a/packages/core/src/services/chatRecordingService.ts b/packages/core/src/services/chatRecordingService.ts index 2ac9f4d99ad..5bd9533bf13 100644 --- a/packages/core/src/services/chatRecordingService.ts +++ b/packages/core/src/services/chatRecordingService.ts @@ -62,7 +62,7 @@ export interface ToolCallRecord { */ export type ConversationRecordExtra = | { - type: 'user'; + type: 'user' | 'info' | 'error' | 'warning'; } | { type: 'gemini'; From de4d412ce9415ae8522653f3a54f6de43e140424 Mon Sep 17 00:00:00 2001 From: bl-ue <54780737+bl-ue@users.noreply.github.com> Date: Tue, 18 Nov 2025 10:48:46 -0700 Subject: [PATCH 4/6] Record interactive-only UI messages like errors and warnings --- packages/cli/src/ui/AppContainer.tsx | 4 +- .../cli/src/ui/hooks/useHistoryManager.ts | 47 ++++++++++++++++++- 2 files changed, 48 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index 1d29f72a80f..c86cdd70f1e 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -147,7 +147,9 @@ const SHELL_HEIGHT_PADDING = 10; export const AppContainer = (props: AppContainerProps) => { const { config, initializationResult, resumedSessionData } = props; - const historyManager = useHistory(); + const historyManager = useHistory({ + chatRecordingService: config.getGeminiClient()?.getChatRecordingService(), + }); useMemoryMonitor(historyManager); const settings = useSettings(); const isAlternateBuffer = useAlternateBuffer(); diff --git a/packages/cli/src/ui/hooks/useHistoryManager.ts b/packages/cli/src/ui/hooks/useHistoryManager.ts index a5c272d5aa0..66eff02824e 100644 --- a/packages/cli/src/ui/hooks/useHistoryManager.ts +++ b/packages/cli/src/ui/hooks/useHistoryManager.ts @@ -6,6 +6,7 @@ import { useState, useRef, useCallback, useMemo } from 'react'; import type { HistoryItem } from '../types.js'; +import type { ChatRecordingService } from '@google/gemini-cli-core/src/services/chatRecordingService.js'; // Type for the updater function passed to updateHistoryItem type HistoryItemUpdater = ( @@ -33,7 +34,11 @@ export interface UseHistoryManagerReturn { * Encapsulates the history array, message ID generation, adding items, * updating items, and clearing the history. */ -export function useHistory(): UseHistoryManagerReturn { +export function useHistory({ + chatRecordingService, +}: { + chatRecordingService?: ChatRecordingService | null; +} = {}): UseHistoryManagerReturn { const [history, setHistory] = useState([]); const messageIdCounterRef = useRef(0); @@ -71,9 +76,47 @@ export function useHistory(): UseHistoryManagerReturn { } return [...prevHistory, newItem]; }); + + // Record UI-specific messages, but don't do it if we're actually loading + // an existing session. + if (!isResuming && chatRecordingService) { + switch (itemData.type) { + case 'compression': + case 'info': + chatRecordingService?.recordMessage({ + model: undefined, + type: 'info', + content: itemData.text ?? '', + }); + break; + case 'warning': + chatRecordingService?.recordMessage({ + model: undefined, + type: 'warning', + content: itemData.text ?? '', + }); + break; + case 'error': + chatRecordingService?.recordMessage({ + model: undefined, + type: 'error', + content: itemData.text ?? '', + }); + break; + case 'user': + case 'gemini': + case 'gemini_content': + // Core conversation recording handled by GeminiChat. + break; + default: + // Ignore the rest. + break; + } + } + return id; // Return the generated ID (even if not added, to keep signature) }, - [getNextMessageId], + [getNextMessageId, chatRecordingService], ); /** From 2b6815d733d0ab36f19be2108c77c25a49af9908 Mon Sep 17 00:00:00 2001 From: bl-ue <54780737+bl-ue@users.noreply.github.com> Date: Tue, 18 Nov 2025 12:22:24 -0700 Subject: [PATCH 5/6] Don't record automatically-typed /quit --- packages/cli/src/ui/AppContainer.tsx | 4 ++-- packages/cli/src/ui/hooks/slashCommandProcessor.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/cli/src/ui/AppContainer.tsx b/packages/cli/src/ui/AppContainer.tsx index c86cdd70f1e..71af417795c 100644 --- a/packages/cli/src/ui/AppContainer.tsx +++ b/packages/cli/src/ui/AppContainer.tsx @@ -1018,7 +1018,7 @@ Logging in with Google... Please restart Gemini CLI to continue. recordExitFail(config); } if (ctrlCPressCount > 1) { - handleSlashCommand('/quit'); + handleSlashCommand('/quit', undefined, undefined, false); } else { ctrlCTimerRef.current = setTimeout(() => { setCtrlCPressCount(0); @@ -1036,7 +1036,7 @@ Logging in with Google... Please restart Gemini CLI to continue. recordExitFail(config); } if (ctrlDPressCount > 1) { - handleSlashCommand('/quit'); + handleSlashCommand('/quit', undefined, undefined, false); } else { ctrlDTimerRef.current = setTimeout(() => { setCtrlDPressCount(0); diff --git a/packages/cli/src/ui/hooks/slashCommandProcessor.ts b/packages/cli/src/ui/hooks/slashCommandProcessor.ts index 6c1e2dd72ac..7614a78248f 100644 --- a/packages/cli/src/ui/hooks/slashCommandProcessor.ts +++ b/packages/cli/src/ui/hooks/slashCommandProcessor.ts @@ -307,8 +307,8 @@ export const useSlashCommandProcessor = ( async ( rawQuery: PartListUnion, oneTimeShellAllowlist?: Set, - addToHistory: boolean = true, overwriteConfirmed?: boolean, + addToHistory: boolean = true, ): Promise => { if (!commands) { return false; From adba991c42d25bd62b82b6b29f2d7b5318fa4d0a Mon Sep 17 00:00:00 2001 From: bl-ue <54780737+bl-ue@users.noreply.github.com> Date: Tue, 18 Nov 2025 18:16:31 -0700 Subject: [PATCH 6/6] Fix the tests --- packages/cli/src/ui/AppContainer.test.tsx | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/packages/cli/src/ui/AppContainer.test.tsx b/packages/cli/src/ui/AppContainer.test.tsx index acbf175d356..e45334bf734 100644 --- a/packages/cli/src/ui/AppContainer.test.tsx +++ b/packages/cli/src/ui/AppContainer.test.tsx @@ -1422,7 +1422,12 @@ describe('AppContainer State Management', () => { pressKey({ name: 'c', ctrl: true }, 2); expect(mockCancelOngoingRequest).toHaveBeenCalledTimes(2); - expect(mockHandleSlashCommand).toHaveBeenCalledWith('/quit'); + expect(mockHandleSlashCommand).toHaveBeenCalledWith( + '/quit', + undefined, + undefined, + false, + ); unmount(); }); @@ -1462,7 +1467,12 @@ describe('AppContainer State Management', () => { pressKey({ name: 'd', ctrl: true }, 2); - expect(mockHandleSlashCommand).toHaveBeenCalledWith('/quit'); + expect(mockHandleSlashCommand).toHaveBeenCalledWith( + '/quit', + undefined, + undefined, + false, + ); unmount(); });