fix(cli): correct initial history length handling for chat commands (#15223)

This commit is contained in:
Sandy Tao
2025-12-17 14:04:02 -10:00
committed by GitHub
parent 5d13145995
commit 739c02bd6d
4 changed files with 16 additions and 20 deletions
@@ -163,7 +163,6 @@ describe('chatCommand', () => {
mockGetHistory.mockReturnValue([ mockGetHistory.mockReturnValue([
{ role: 'user', parts: [{ text: 'context for our chat' }] }, { role: 'user', parts: [{ text: 'context for our chat' }] },
{ role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
]); ]);
result = await saveCommand?.action?.(mockContext, tag); result = await saveCommand?.action?.(mockContext, tag);
expect(result).toEqual({ expect(result).toEqual({
@@ -208,9 +207,7 @@ describe('chatCommand', () => {
it('should save the conversation if overwrite is confirmed', async () => { it('should save the conversation if overwrite is confirmed', async () => {
const history: Content[] = [ const history: Content[] = [
{ role: 'user', parts: [{ text: 'context for our chat' }] }, { role: 'user', parts: [{ text: 'context for our chat' }] },
{ role: 'model', parts: [{ text: 'Got it. Thanks for the context!' }] },
{ role: 'user', parts: [{ text: 'hello' }] }, { role: 'user', parts: [{ text: 'hello' }] },
{ role: 'model', parts: [{ text: 'Hi there!' }] },
]; ];
mockGetHistory.mockReturnValue(history); mockGetHistory.mockReturnValue(history);
mockContext.overwriteConfirmed = true; mockContext.overwriteConfirmed = true;
@@ -263,6 +260,7 @@ describe('chatCommand', () => {
it('should resume a conversation with matching authType', async () => { it('should resume a conversation with matching authType', async () => {
const conversation: Content[] = [ const conversation: Content[] = [
{ role: 'user', parts: [{ text: 'system setup' }] },
{ role: 'user', parts: [{ text: 'hello gemini' }] }, { role: 'user', parts: [{ text: 'hello gemini' }] },
{ role: 'model', parts: [{ text: 'hello world' }] }, { role: 'model', parts: [{ text: 'hello world' }] },
]; ];
@@ -285,6 +283,7 @@ describe('chatCommand', () => {
it('should block resuming a conversation with mismatched authType', async () => { it('should block resuming a conversation with mismatched authType', async () => {
const conversation: Content[] = [ const conversation: Content[] = [
{ role: 'user', parts: [{ text: 'system setup' }] },
{ role: 'user', parts: [{ text: 'hello gemini' }] }, { role: 'user', parts: [{ text: 'hello gemini' }] },
{ role: 'model', parts: [{ text: 'hello world' }] }, { role: 'model', parts: [{ text: 'hello world' }] },
]; ];
@@ -304,6 +303,7 @@ describe('chatCommand', () => {
it('should resume a legacy conversation without authType', async () => { it('should resume a legacy conversation without authType', async () => {
const conversation: Content[] = [ const conversation: Content[] = [
{ role: 'user', parts: [{ text: 'system setup' }] },
{ role: 'user', parts: [{ text: 'hello gemini' }] }, { role: 'user', parts: [{ text: 'hello gemini' }] },
{ role: 'model', parts: [{ text: 'hello world' }] }, { role: 'model', parts: [{ text: 'hello world' }] },
]; ];
@@ -521,7 +521,6 @@ Hi there!`;
it('should inform if there is no conversation to share', async () => { it('should inform if there is no conversation to share', async () => {
mockGetHistory.mockReturnValue([ mockGetHistory.mockReturnValue([
{ role: 'user', parts: [{ text: 'context' }] }, { role: 'user', parts: [{ text: 'context' }] },
{ role: 'model', parts: [{ text: 'context response' }] },
]); ]);
const result = await shareCommand?.action?.(mockContext, 'my-chat.json'); const result = await shareCommand?.action?.(mockContext, 'my-chat.json');
expect(mockFs.writeFile).not.toHaveBeenCalled(); expect(mockFs.writeFile).not.toHaveBeenCalled();
+10 -16
View File
@@ -17,6 +17,7 @@ import { CommandKind } from './types.js';
import { import {
decodeTagName, decodeTagName,
type MessageActionReturn, type MessageActionReturn,
INITIAL_HISTORY_LENGTH,
} from '@google/gemini-cli-core'; } from '@google/gemini-cli-core';
import path from 'node:path'; import path from 'node:path';
import type { import type {
@@ -131,7 +132,7 @@ const saveCommand: SlashCommand = {
} }
const history = chat.getHistory(); const history = chat.getHistory();
if (history.length > 2) { if (history.length > INITIAL_HISTORY_LENGTH) {
const authType = config?.getContentGeneratorConfig()?.authType; const authType = config?.getContentGeneratorConfig()?.authType;
await logger.saveCheckpoint({ history, authType }, tag); await logger.saveCheckpoint({ history, authType }, tag);
return { return {
@@ -200,11 +201,8 @@ const resumeCommand: SlashCommand = {
}; };
const uiHistory: HistoryItemWithoutId[] = []; const uiHistory: HistoryItemWithoutId[] = [];
let hasSystemPrompt = false;
let i = 0;
for (const item of conversation) { for (const item of conversation.slice(INITIAL_HISTORY_LENGTH)) {
i += 1;
const text = const text =
item.parts item.parts
?.filter((m) => !!m.text) ?.filter((m) => !!m.text)
@@ -213,15 +211,11 @@ const resumeCommand: SlashCommand = {
if (!text) { if (!text) {
continue; continue;
} }
if (i === 1 && text.match(/context for our chat/)) {
hasSystemPrompt = true; uiHistory.push({
} type: (item.role && rolemap[item.role]) || MessageType.GEMINI,
if (i > 2 || !hasSystemPrompt) { text,
uiHistory.push({ } as HistoryItemWithoutId);
type: (item.role && rolemap[item.role]) || MessageType.GEMINI,
text,
} as HistoryItemWithoutId);
}
} }
return { return {
type: 'load_history', type: 'load_history',
@@ -343,10 +337,10 @@ const shareCommand: SlashCommand = {
const history = chat.getHistory(); const history = chat.getHistory();
// An empty conversation has two hidden messages that setup the context for // An empty conversation has a hidden message that sets up the context for
// the chat. Thus, to check whether a conversation has been started, we // the chat. Thus, to check whether a conversation has been started, we
// can't check for length 0. // can't check for length 0.
if (history.length <= 2) { if (history.length <= INITIAL_HISTORY_LENGTH) {
return { return {
type: 'message', type: 'message',
messageType: 'info', messageType: 'info',
+1
View File
@@ -73,6 +73,7 @@ export * from './utils/generateContentResponseUtilities.js';
export * from './utils/filesearch/fileSearch.js'; export * from './utils/filesearch/fileSearch.js';
export * from './utils/errorParsing.js'; export * from './utils/errorParsing.js';
export * from './utils/workspaceContext.js'; export * from './utils/workspaceContext.js';
export * from './utils/environmentContext.js';
export * from './utils/ignorePatterns.js'; export * from './utils/ignorePatterns.js';
export * from './utils/partUtils.js'; export * from './utils/partUtils.js';
export * from './utils/promptIdContext.js'; export * from './utils/promptIdContext.js';
@@ -8,6 +8,8 @@ import type { Part, Content } from '@google/genai';
import type { Config } from '../config/config.js'; import type { Config } from '../config/config.js';
import { getFolderStructure } from './getFolderStructure.js'; import { getFolderStructure } from './getFolderStructure.js';
export const INITIAL_HISTORY_LENGTH = 1;
/** /**
* Generates a string describing the current workspace directories and their structures. * Generates a string describing the current workspace directories and their structures.
* @param {Config} config - The runtime configuration and services. * @param {Config} config - The runtime configuration and services.