Skip to content
1 change: 0 additions & 1 deletion packages/cli/src/nonInteractiveCli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1540,7 +1540,6 @@ describe('runNonInteractive', () => {
] as any, // eslint-disable-line @typescript-eslint/no-explicit-any
startTime: new Date().toISOString(),
lastUpdated: new Date().toISOString(),
firstUserMessage: 'Previous message',
projectHash: 'test-hash',
},
filePath: '/path/to/session.json',
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/nonInteractiveCliCommands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ export const handleSlashCommand = async (
settings,
git: undefined,
logger,
chatRecordingService: undefined,
},
ui: createNonInteractiveUI(),
session: {
Expand Down
2 changes: 2 additions & 0 deletions packages/cli/src/services/BuiltinCommandLoader.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ import { policiesCommand } from '../ui/commands/policiesCommand.js';
import { profileCommand } from '../ui/commands/profileCommand.js';
import { quitCommand } from '../ui/commands/quitCommand.js';
import { restoreCommand } from '../ui/commands/restoreCommand.js';
import { renameCommand } from '../ui/commands/renameCommand.js';
import { resumeCommand } from '../ui/commands/resumeCommand.js';
import { statsCommand } from '../ui/commands/statsCommand.js';
import { themeCommand } from '../ui/commands/themeCommand.js';
Expand Down Expand Up @@ -136,6 +137,7 @@ export class BuiltinCommandLoader implements ICommandLoader {
policiesCommand,
...(isDevelopment ? [profileCommand] : []),
quitCommand,
renameCommand,
restoreCommand(this.config),
resumeCommand,
statsCommand,
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/test-utils/mockCommandContext.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ export const createMockCommandContext = (
loadCheckpoint: vi.fn().mockResolvedValue([]),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
} as any, // Cast because Logger is a class.
chatRecordingService: undefined,
},
ui: {
addItem: vi.fn(),
Expand Down
72 changes: 72 additions & 0 deletions packages/cli/src/ui/commands/renameCommand.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import { beforeEach, describe, expect, it, vi } from 'vitest';
import { renameCommand } from './renameCommand.js';
import type { ChatRecordingService } from '@google/gemini-cli-core';
import type { CommandContext } from './types.js';

describe('renameCommand', () => {
let mockContext: CommandContext;
let mockChatRecordingService: {
setDisplayName: ReturnType<typeof vi.fn>;
};

beforeEach(() => {
mockChatRecordingService = {
setDisplayName: vi.fn(),
};

mockContext = {
services: {
chatRecordingService:
mockChatRecordingService as unknown as ChatRecordingService,
},
} as unknown as CommandContext;
});

it('should require a name argument', async () => {
const result = await renameCommand.action!(mockContext, '');
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: 'Please provide a new name for the session.',
});
});

it('should handle empty name string', async () => {
const result = await renameCommand.action!(mockContext, ' ');
expect(result).toEqual({
type: 'message',
messageType: 'error',
content: 'Please provide a new name for the session.',
});
});

it('should return undefined if chatRecordingService is not available', async () => {
mockContext = {
services: {
chatRecordingService: undefined,
},
} as unknown as CommandContext;

const result = await renameCommand.action!(mockContext, 'New Name');
expect(result).toEqual(undefined);
});

it('should rename the current session', async () => {
const result = await renameCommand.action!(mockContext, 'New Name');

expect(mockChatRecordingService.setDisplayName).toHaveBeenCalledWith(
'New Name',
);
expect(result).toEqual({
type: 'message',
messageType: 'info',
content: 'Session renamed to "New Name"',
});
});
});
39 changes: 39 additions & 0 deletions packages/cli/src/ui/commands/renameCommand.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* @license
* Copyright 2025 Google LLC
* SPDX-License-Identifier: Apache-2.0
*/

import {
CommandKind,
type SlashCommand,
type SlashCommandActionReturn,
} from './types.js';

export const renameCommand: SlashCommand = {
name: 'rename',
description: 'Set the display name for the current session',
kind: CommandKind.BUILT_IN,
action: async (context, input): Promise<SlashCommandActionReturn | void> => {
const newName = input.trim();
if (!newName) {
return {
type: 'message',
messageType: 'error',
content: 'Please provide a new name for the session.',
};
}

if (!context.services.chatRecordingService) {
return;
}

context.services.chatRecordingService.setDisplayName(newName);

return {
type: 'message',
messageType: 'info',
content: `Session renamed to "${newName}"`,
};
},
};
2 changes: 2 additions & 0 deletions packages/cli/src/ui/commands/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import type {
ConfirmationRequest,
} from '../types.js';
import type {
ChatRecordingService,
Config,
GitService,
Logger,
Expand Down Expand Up @@ -43,6 +44,7 @@ export interface CommandContext {
settings: LoadedSettings;
git: GitService | undefined;
logger: Logger;
chatRecordingService: ChatRecordingService | undefined;
};
// UI state and history management
ui: {
Expand Down
31 changes: 28 additions & 3 deletions packages/cli/src/ui/components/SessionBrowser.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,6 @@ const createSession = (overrides: Partial<SessionInfo>): SessionInfo => ({
lastUpdated: new Date().toISOString(),
messageCount: 1,
displayName: 'Test Session',
firstUserMessage: 'Test Session',
isCurrentSession: false,
index: 0,
...overrides,
Expand Down Expand Up @@ -205,12 +204,39 @@ describe('SessionBrowser component', () => {
expect(lastFrame()).toMatchSnapshot();
});

it('displays fallback displayName', () => {
const session = createSession({
id: 'no-display-name',
file: 'no-display-name',
displayName: 'Fallback Message',
lastUpdated: '2025-01-01T10:05:00Z',
messageCount: 2,
index: 0,
});

const config = createMockConfig();
const onResumeSession = vi.fn();
const onDeleteSession = vi.fn().mockResolvedValue(undefined);
const onExit = vi.fn();

const { lastFrame } = render(
<TestSessionBrowser
config={config}
onResumeSession={onResumeSession}
onDeleteSession={onDeleteSession}
onExit={onExit}
testSessions={[session]}
/>,
);

expect(lastFrame()).toContain('Fallback Message');
});

it('enters search mode, filters sessions, and renders match snippets', async () => {
const searchSession = createSession({
id: 'search1',
file: 'search1',
displayName: 'Query is here and another query.',
firstUserMessage: 'Query is here and another query.',
fullContent: 'Query is here and another query.',
messages: [
{
Expand All @@ -226,7 +252,6 @@ describe('SessionBrowser component', () => {
id: 'other',
file: 'other',
displayName: 'Nothing interesting here.',
firstUserMessage: 'Nothing interesting here.',
fullContent: 'Nothing interesting here.',
messages: [
{
Expand Down
3 changes: 1 addition & 2 deletions packages/cli/src/ui/components/SessionBrowser.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -249,8 +249,7 @@ const filterSessions = (
return sessions.filter((session) => {
const titleMatch =
session.displayName.toLowerCase().includes(lowerQuery) ||
session.id.toLowerCase().includes(lowerQuery) ||
session.firstUserMessage.toLowerCase().includes(lowerQuery);
session.id.toLowerCase().includes(lowerQuery);

const contentMatch = session.fullContent
?.toLowerCase()
Expand Down
1 change: 1 addition & 0 deletions packages/cli/src/ui/hooks/slashCommandProcessor.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,7 @@ describe('useSlashCommandProcessor', () => {
const mockClient = {
setHistory: vi.fn(),
stripThoughtsFromHistory: vi.fn(),
getChatRecordingService: vi.fn(),
} as unknown as GeminiClient;
vi.spyOn(mockConfig, 'getGeminiClient').mockReturnValue(mockClient);

Expand Down
3 changes: 3 additions & 0 deletions packages/cli/src/ui/hooks/slashCommandProcessor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ export const useSlashCommandProcessor = (
settings,
git: gitService,
logger,
chatRecordingService: config
?.getGeminiClient()
?.getChatRecordingService(),
},
ui: {
addItem,
Expand Down
Loading