Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions packages/cli/src/ui/AppContainer.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

Expand Down Expand Up @@ -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();
});

Expand Down
8 changes: 5 additions & 3 deletions packages/cli/src/ui/AppContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -1021,7 +1023,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);
Expand All @@ -1039,7 +1041,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);
Expand Down
5 changes: 5 additions & 0 deletions packages/cli/src/ui/commands/clearCommand.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ describe('clearCommand', () => {

beforeEach(() => {
mockResetChat = vi.fn().mockResolvedValue(undefined);
const mockGetChatRecordingService = vi.fn();
vi.clearAllMocks();

mockContext = createMockCommandContext({
Expand All @@ -38,7 +39,11 @@ describe('clearCommand', () => {
getGeminiClient: () =>
({
resetChat: mockResetChat,
getChat: () => ({
getChatRecordingService: mockGetChatRecordingService,
}),
}) as unknown as GeminiClient,
setSessionId: vi.fn(),
},
},
});
Expand Down
13 changes: 13 additions & 0 deletions packages/cli/src/ui/commands/clearCommand.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,19 @@
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',
description: 'Clear the screen and conversation history',
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.');
Expand All @@ -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();
},
Expand Down
10 changes: 8 additions & 2 deletions packages/cli/src/ui/hooks/slashCommandProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,7 @@ export const useSlashCommandProcessor = (
rawQuery: PartListUnion,
oneTimeShellAllowlist?: Set<string>,
overwriteConfirmed?: boolean,
addToHistory: boolean = true,
): Promise<SlashCommandProcessorResult | false> => {
if (!commands) {
return false;
Expand All @@ -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 {
Expand Down
7 changes: 3 additions & 4 deletions packages/cli/src/ui/hooks/useEditorSettings.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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() {
Expand Down
5 changes: 3 additions & 2 deletions packages/cli/src/ui/hooks/useEditorSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';

Expand All @@ -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);

Expand Down
59 changes: 55 additions & 4 deletions packages/cli/src/ui/hooks/useHistoryManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (
Expand All @@ -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,
Expand All @@ -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);

Expand All @@ -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;

Expand All @@ -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],
);

/**
Expand Down
41 changes: 41 additions & 0 deletions packages/cli/src/ui/hooks/useSessionBrowser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[] = [
{
Expand Down
18 changes: 13 additions & 5 deletions packages/cli/src/ui/hooks/useSessionBrowser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/ui/hooks/useSessionResume.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down Expand Up @@ -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();
});
Expand Down
Loading