mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-03-10 22:21:22 -07:00
feat(sessions): record interactive-only errors and warnings to chat recording JSON files (#13300)
This commit is contained in:
@@ -1392,7 +1392,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();
|
||||
});
|
||||
|
||||
@@ -1432,7 +1437,12 @@ describe('AppContainer State Management', () => {
|
||||
|
||||
pressKey({ name: 'd', ctrl: true }, 2);
|
||||
|
||||
expect(mockHandleSlashCommand).toHaveBeenCalledWith('/quit');
|
||||
expect(mockHandleSlashCommand).toHaveBeenCalledWith(
|
||||
'/quit',
|
||||
undefined,
|
||||
undefined,
|
||||
false,
|
||||
);
|
||||
unmount();
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
@@ -1026,7 +1028,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);
|
||||
@@ -1044,7 +1046,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);
|
||||
|
||||
@@ -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(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
},
|
||||
|
||||
@@ -311,6 +311,7 @@ export const useSlashCommandProcessor = (
|
||||
rawQuery: PartListUnion,
|
||||
oneTimeShellAllowlist?: Set<string>,
|
||||
overwriteConfirmed?: boolean,
|
||||
addToHistory: boolean = true,
|
||||
): Promise<SlashCommandProcessorResult | false> => {
|
||||
if (!commands) {
|
||||
return false;
|
||||
@@ -326,8 +327,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 {
|
||||
|
||||
@@ -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<HistoryItem, 'id'>, timestamp: number) => void
|
||||
>;
|
||||
let mockAddItem: MockedFunction<UseHistoryManagerReturn['addItem']>;
|
||||
let result: ReturnType<typeof useEditorSettings>;
|
||||
|
||||
function TestComponent() {
|
||||
|
||||
@@ -9,13 +9,14 @@ 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,
|
||||
getEditorDisplayName,
|
||||
} from '@google/gemini-cli-core';
|
||||
import type { UseHistoryManagerReturn } from './useHistoryManager.js';
|
||||
|
||||
import { SettingPaths } from '../../config/settingPaths.js';
|
||||
|
||||
@@ -32,7 +33,7 @@ interface UseEditorSettingsReturn {
|
||||
export const useEditorSettings = (
|
||||
loadedSettings: LoadedSettings,
|
||||
setEditorError: (error: string | null) => void,
|
||||
addItem: (item: Omit<HistoryItem, 'id'>, timestamp: number) => void,
|
||||
addItem: UseHistoryManagerReturn['addItem'],
|
||||
): UseEditorSettingsReturn => {
|
||||
const [isEditorDialogOpen, setIsEditorDialogOpen] = useState(false);
|
||||
|
||||
|
||||
@@ -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 = (
|
||||
@@ -14,7 +15,11 @@ type HistoryItemUpdater = (
|
||||
|
||||
export interface UseHistoryManagerReturn {
|
||||
history: HistoryItem[];
|
||||
addItem: (itemData: Omit<HistoryItem, 'id'>, baseTimestamp: number) => number; // Returns the generated ID
|
||||
addItem: (
|
||||
itemData: Omit<HistoryItem, 'id'>,
|
||||
baseTimestamp: number,
|
||||
isResuming?: boolean,
|
||||
) => number; // Returns the generated ID
|
||||
updateItem: (
|
||||
id: number,
|
||||
updates: Partial<Omit<HistoryItem, 'id'>> | HistoryItemUpdater,
|
||||
@@ -29,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<HistoryItem[]>([]);
|
||||
const messageIdCounterRef = useRef(0);
|
||||
|
||||
@@ -45,7 +54,11 @@ export function useHistory(): UseHistoryManagerReturn {
|
||||
|
||||
// Adds a new item to the history state with a unique ID.
|
||||
const addItem = useCallback(
|
||||
(itemData: Omit<HistoryItem, 'id'>, baseTimestamp: number): number => {
|
||||
(
|
||||
itemData: Omit<HistoryItem, 'id'>,
|
||||
baseTimestamp: number,
|
||||
isResuming: boolean = false,
|
||||
): number => {
|
||||
const id = getNextMessageId(baseTimestamp);
|
||||
const newItem: HistoryItem = { ...itemData, id } as HistoryItem;
|
||||
|
||||
@@ -63,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],
|
||||
);
|
||||
|
||||
/**
|
||||
|
||||
@@ -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[] = [
|
||||
{
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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<HistoryItem, 'id'>, timestamp: number) => void,
|
||||
addItem: UseHistoryManagerReturn['addItem'],
|
||||
initialThemeError: string | null,
|
||||
): UseThemeCommandReturn => {
|
||||
const [isThemeDialogOpen, setIsThemeDialogOpen] =
|
||||
|
||||
@@ -62,7 +62,7 @@ export interface ToolCallRecord {
|
||||
*/
|
||||
export type ConversationRecordExtra =
|
||||
| {
|
||||
type: 'user';
|
||||
type: 'user' | 'info' | 'error' | 'warning';
|
||||
}
|
||||
| {
|
||||
type: 'gemini';
|
||||
|
||||
Reference in New Issue
Block a user