mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-04-22 11:04:42 -07:00
Fix bugs where Rewind and Resume showed Ugly and 100X too verbose content. (#17940)
This commit is contained in:
@@ -258,6 +258,9 @@ describe('runNonInteractive', () => {
|
|||||||
[{ text: 'Test input' }],
|
[{ text: 'Test input' }],
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
'prompt-id-1',
|
'prompt-id-1',
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
'Test input',
|
||||||
);
|
);
|
||||||
expect(getWrittenOutput()).toBe('Hello World\n');
|
expect(getWrittenOutput()).toBe('Hello World\n');
|
||||||
// Note: Telemetry shutdown is now handled in runExitCleanup() in cleanup.ts
|
// Note: Telemetry shutdown is now handled in runExitCleanup() in cleanup.ts
|
||||||
@@ -374,6 +377,9 @@ describe('runNonInteractive', () => {
|
|||||||
[{ text: 'Tool response' }],
|
[{ text: 'Tool response' }],
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
'prompt-id-2',
|
'prompt-id-2',
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
expect(getWrittenOutput()).toBe('Final answer\n');
|
expect(getWrittenOutput()).toBe('Final answer\n');
|
||||||
});
|
});
|
||||||
@@ -531,6 +537,9 @@ describe('runNonInteractive', () => {
|
|||||||
],
|
],
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
'prompt-id-3',
|
'prompt-id-3',
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
expect(getWrittenOutput()).toBe('Sorry, let me try again.\n');
|
expect(getWrittenOutput()).toBe('Sorry, let me try again.\n');
|
||||||
});
|
});
|
||||||
@@ -670,6 +679,9 @@ describe('runNonInteractive', () => {
|
|||||||
processedParts,
|
processedParts,
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
'prompt-id-7',
|
'prompt-id-7',
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
rawInput,
|
||||||
);
|
);
|
||||||
|
|
||||||
// 6. Assert the final output is correct
|
// 6. Assert the final output is correct
|
||||||
@@ -703,6 +715,9 @@ describe('runNonInteractive', () => {
|
|||||||
[{ text: 'Test input' }],
|
[{ text: 'Test input' }],
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
'prompt-id-1',
|
'prompt-id-1',
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
'Test input',
|
||||||
);
|
);
|
||||||
expect(processStdoutSpy).toHaveBeenCalledWith(
|
expect(processStdoutSpy).toHaveBeenCalledWith(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
@@ -833,6 +848,9 @@ describe('runNonInteractive', () => {
|
|||||||
[{ text: 'Empty response test' }],
|
[{ text: 'Empty response test' }],
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
'prompt-id-empty',
|
'prompt-id-empty',
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
'Empty response test',
|
||||||
);
|
);
|
||||||
|
|
||||||
// This should output JSON with empty response but include stats
|
// This should output JSON with empty response but include stats
|
||||||
@@ -967,6 +985,9 @@ describe('runNonInteractive', () => {
|
|||||||
[{ text: 'Prompt from command' }],
|
[{ text: 'Prompt from command' }],
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
'prompt-id-slash',
|
'prompt-id-slash',
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
'/testcommand',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(getWrittenOutput()).toBe('Response from command\n');
|
expect(getWrittenOutput()).toBe('Response from command\n');
|
||||||
@@ -1010,6 +1031,9 @@ describe('runNonInteractive', () => {
|
|||||||
[{ text: 'Slash command output' }],
|
[{ text: 'Slash command output' }],
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
'prompt-id-slash',
|
'prompt-id-slash',
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
'/help',
|
||||||
);
|
);
|
||||||
expect(getWrittenOutput()).toBe('Response to slash command\n');
|
expect(getWrittenOutput()).toBe('Response to slash command\n');
|
||||||
handleSlashCommandSpy.mockRestore();
|
handleSlashCommandSpy.mockRestore();
|
||||||
@@ -1184,6 +1208,9 @@ describe('runNonInteractive', () => {
|
|||||||
[{ text: '/unknowncommand' }],
|
[{ text: '/unknowncommand' }],
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
'prompt-id-unknown',
|
'prompt-id-unknown',
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
'/unknowncommand',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(getWrittenOutput()).toBe('Response to unknown\n');
|
expect(getWrittenOutput()).toBe('Response to unknown\n');
|
||||||
|
|||||||
@@ -301,6 +301,9 @@ export async function runNonInteractive({
|
|||||||
currentMessages[0]?.parts || [],
|
currentMessages[0]?.parts || [],
|
||||||
abortController.signal,
|
abortController.signal,
|
||||||
prompt_id,
|
prompt_id,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
turnCount === 1 ? input : undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
let responseText = '';
|
let responseText = '';
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ describe('RewindViewer', () => {
|
|||||||
{
|
{
|
||||||
description: 'removes reference markers',
|
description: 'removes reference markers',
|
||||||
prompt: `some command @file\n--- Content from referenced files ---\nContent from file:\nblah blah\n--- End of content ---`,
|
prompt: `some command @file\n--- Content from referenced files ---\nContent from file:\nblah blah\n--- End of content ---`,
|
||||||
|
expected: 'some command @file',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
description: 'strips expanded MCP resource content',
|
description: 'strips expanded MCP resource content',
|
||||||
@@ -263,10 +264,23 @@ describe('RewindViewer', () => {
|
|||||||
'\nContent from @server3:mcp://demo-resource:\n' +
|
'\nContent from @server3:mcp://demo-resource:\n' +
|
||||||
'This is the content of the demo resource.\n' +
|
'This is the content of the demo resource.\n' +
|
||||||
`--- End of content ---`,
|
`--- End of content ---`,
|
||||||
|
expected: 'read @server3:mcp://demo-resource hello',
|
||||||
},
|
},
|
||||||
])('$description', async ({ prompt }) => {
|
{
|
||||||
|
description: 'uses displayContent if present and does not strip',
|
||||||
|
prompt: `raw content with markers\n--- Content from referenced files ---\nblah\n--- End of content ---`,
|
||||||
|
displayContent: 'clean display content',
|
||||||
|
expected: 'clean display content',
|
||||||
|
},
|
||||||
|
])('$description', async ({ prompt, displayContent, expected }) => {
|
||||||
const conversation = createConversation([
|
const conversation = createConversation([
|
||||||
{ type: 'user', content: prompt, id: '1', timestamp: '1' },
|
{
|
||||||
|
type: 'user',
|
||||||
|
content: prompt,
|
||||||
|
displayContent,
|
||||||
|
id: '1',
|
||||||
|
timestamp: '1',
|
||||||
|
},
|
||||||
]);
|
]);
|
||||||
const onRewind = vi.fn();
|
const onRewind = vi.fn();
|
||||||
const { lastFrame, stdin } = renderWithProviders(
|
const { lastFrame, stdin } = renderWithProviders(
|
||||||
@@ -289,6 +303,15 @@ describe('RewindViewer', () => {
|
|||||||
await waitFor(() => {
|
await waitFor(() => {
|
||||||
expect(lastFrame()).toContain('Confirm Rewind');
|
expect(lastFrame()).toContain('Confirm Rewind');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Confirm
|
||||||
|
act(() => {
|
||||||
|
stdin.write('\r');
|
||||||
|
});
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(onRewind).toHaveBeenCalledWith('1', expected, expect.anything());
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,14 @@ interface RewindViewerProps {
|
|||||||
|
|
||||||
const MAX_LINES_PER_BOX = 2;
|
const MAX_LINES_PER_BOX = 2;
|
||||||
|
|
||||||
|
const getCleanedRewindText = (userPrompt: MessageRecord): string => {
|
||||||
|
const contentToUse = userPrompt.displayContent || userPrompt.content;
|
||||||
|
const originalUserText = contentToUse ? partToString(contentToUse) : '';
|
||||||
|
return userPrompt.displayContent
|
||||||
|
? originalUserText
|
||||||
|
: stripReferenceContent(originalUserText);
|
||||||
|
};
|
||||||
|
|
||||||
export const RewindViewer: React.FC<RewindViewerProps> = ({
|
export const RewindViewer: React.FC<RewindViewerProps> = ({
|
||||||
conversation,
|
conversation,
|
||||||
onExit,
|
onExit,
|
||||||
@@ -162,10 +170,7 @@ export const RewindViewer: React.FC<RewindViewerProps> = ({
|
|||||||
(m) => m.id === selectedMessageId,
|
(m) => m.id === selectedMessageId,
|
||||||
);
|
);
|
||||||
if (userPrompt) {
|
if (userPrompt) {
|
||||||
const originalUserText = userPrompt.content
|
const cleanedText = getCleanedRewindText(userPrompt);
|
||||||
? partToString(userPrompt.content)
|
|
||||||
: '';
|
|
||||||
const cleanedText = stripReferenceContent(originalUserText);
|
|
||||||
setIsRewinding(true);
|
setIsRewinding(true);
|
||||||
await onRewind(selectedMessageId, cleanedText, outcome);
|
await onRewind(selectedMessageId, cleanedText, outcome);
|
||||||
}
|
}
|
||||||
@@ -224,7 +229,9 @@ export const RewindViewer: React.FC<RewindViewerProps> = ({
|
|||||||
isSelected ? theme.status.success : theme.text.primary
|
isSelected ? theme.status.success : theme.text.primary
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{partToString(userPrompt.content)}
|
{partToString(
|
||||||
|
userPrompt.displayContent || userPrompt.content,
|
||||||
|
)}
|
||||||
</Text>
|
</Text>
|
||||||
<Text color={theme.text.secondary}>
|
<Text color={theme.text.secondary}>
|
||||||
Cancel rewind and stay here
|
Cancel rewind and stay here
|
||||||
@@ -235,10 +242,7 @@ export const RewindViewer: React.FC<RewindViewerProps> = ({
|
|||||||
|
|
||||||
const stats = getStats(userPrompt);
|
const stats = getStats(userPrompt);
|
||||||
const firstFileName = stats?.details?.at(0)?.fileName;
|
const firstFileName = stats?.details?.at(0)?.fileName;
|
||||||
const originalUserText = userPrompt.content
|
const cleanedText = getCleanedRewindText(userPrompt);
|
||||||
? partToString(userPrompt.content)
|
|
||||||
: '';
|
|
||||||
const cleanedText = stripReferenceContent(originalUserText);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box flexDirection="column" marginBottom={1}>
|
<Box flexDirection="column" marginBottom={1}>
|
||||||
|
|||||||
@@ -34,6 +34,23 @@ exports[`RewindViewer > Content Filtering > 'strips expanded MCP resource conten
|
|||||||
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||||
`;
|
`;
|
||||||
|
|
||||||
|
exports[`RewindViewer > Content Filtering > 'uses displayContent if present and do…' 1`] = `
|
||||||
|
"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
|
│ │
|
||||||
|
│ > Rewind │
|
||||||
|
│ │
|
||||||
|
│ clean display content │
|
||||||
|
│ No files have been changed │
|
||||||
|
│ │
|
||||||
|
│ ● Stay at current position │
|
||||||
|
│ Cancel rewind and stay here │
|
||||||
|
│ │
|
||||||
|
│ │
|
||||||
|
│ (Use Enter to select a message, Esc to close, Right/Left to expand/collapse) │
|
||||||
|
│ │
|
||||||
|
╰──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╯"
|
||||||
|
`;
|
||||||
|
|
||||||
exports[`RewindViewer > Interaction Selection > 'cancels on Escape' > confirmation-dialog 1`] = `
|
exports[`RewindViewer > Interaction Selection > 'cancels on Escape' > confirmation-dialog 1`] = `
|
||||||
"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
"╭──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────╮
|
||||||
│ │
|
│ │
|
||||||
|
|||||||
@@ -655,6 +655,9 @@ describe('useGeminiStream', () => {
|
|||||||
expectedMergedResponse,
|
expectedMergedResponse,
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
'prompt-id-2',
|
'prompt-id-2',
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
expectedMergedResponse,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1057,6 +1060,9 @@ describe('useGeminiStream', () => {
|
|||||||
toolCallResponseParts,
|
toolCallResponseParts,
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
'prompt-id-4',
|
'prompt-id-4',
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
toolCallResponseParts,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1498,6 +1504,9 @@ describe('useGeminiStream', () => {
|
|||||||
'This is the actual prompt from the command file.',
|
'This is the actual prompt from the command file.',
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
'/my-custom-command',
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(mockScheduleToolCalls).not.toHaveBeenCalled();
|
expect(mockScheduleToolCalls).not.toHaveBeenCalled();
|
||||||
@@ -1524,6 +1533,9 @@ describe('useGeminiStream', () => {
|
|||||||
'',
|
'',
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
'/emptycmd',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1542,6 +1554,9 @@ describe('useGeminiStream', () => {
|
|||||||
'// This is a line comment',
|
'// This is a line comment',
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
'// This is a line comment',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1560,6 +1575,9 @@ describe('useGeminiStream', () => {
|
|||||||
'/* This is a block comment */',
|
'/* This is a block comment */',
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
'/* This is a block comment */',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -2392,6 +2410,9 @@ describe('useGeminiStream', () => {
|
|||||||
processedQueryParts, // Argument 1: The parts array directly
|
processedQueryParts, // Argument 1: The parts array directly
|
||||||
expect.any(AbortSignal), // Argument 2: An AbortSignal
|
expect.any(AbortSignal), // Argument 2: An AbortSignal
|
||||||
expect.any(String), // Argument 3: The prompt_id string
|
expect.any(String), // Argument 3: The prompt_id string
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
rawQuery,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2931,6 +2952,9 @@ describe('useGeminiStream', () => {
|
|||||||
'test query',
|
'test query',
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
'test query',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -3078,6 +3102,9 @@ describe('useGeminiStream', () => {
|
|||||||
'second query',
|
'second query',
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
expect.any(String),
|
expect.any(String),
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
'second query',
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1255,6 +1255,9 @@ export const useGeminiStream = (
|
|||||||
queryToSend,
|
queryToSend,
|
||||||
abortSignal,
|
abortSignal,
|
||||||
prompt_id!,
|
prompt_id!,
|
||||||
|
undefined,
|
||||||
|
false,
|
||||||
|
query,
|
||||||
);
|
);
|
||||||
const processingStatus = await processGeminiStreamEvents(
|
const processingStatus = await processGeminiStreamEvents(
|
||||||
stream,
|
stream,
|
||||||
|
|||||||
@@ -178,6 +178,30 @@ describe('convertSessionToHistoryFormats', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should prioritize displayContent for UI history but use content for client history', () => {
|
||||||
|
const messages: MessageRecord[] = [
|
||||||
|
{
|
||||||
|
type: 'user',
|
||||||
|
content: [{ text: 'Expanded content' }],
|
||||||
|
displayContent: [{ text: 'User input' }],
|
||||||
|
} as MessageRecord,
|
||||||
|
];
|
||||||
|
|
||||||
|
const result = convertSessionToHistoryFormats(messages);
|
||||||
|
|
||||||
|
expect(result.uiHistory).toHaveLength(1);
|
||||||
|
expect(result.uiHistory[0]).toMatchObject({
|
||||||
|
type: 'user',
|
||||||
|
text: 'User input',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(result.clientHistory).toHaveLength(1);
|
||||||
|
expect(result.clientHistory[0]).toEqual({
|
||||||
|
role: 'user',
|
||||||
|
parts: [{ text: 'Expanded content' }],
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should filter out slash commands from client history but keep in UI', () => {
|
it('should filter out slash commands from client history but keep in UI', () => {
|
||||||
const messages: MessageRecord[] = [
|
const messages: MessageRecord[] = [
|
||||||
{ type: 'user', content: '/help' } as MessageRecord,
|
{ type: 'user', content: '/help' } as MessageRecord,
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ import type {
|
|||||||
} from '@google/gemini-cli-core';
|
} from '@google/gemini-cli-core';
|
||||||
import type { Part } from '@google/genai';
|
import type { Part } from '@google/genai';
|
||||||
import { partListUnionToString, coreEvents } from '@google/gemini-cli-core';
|
import { partListUnionToString, coreEvents } from '@google/gemini-cli-core';
|
||||||
|
import { checkExhaustive } from '../../utils/checks.js';
|
||||||
import type { SessionInfo } from '../../utils/sessionUtils.js';
|
import type { SessionInfo } from '../../utils/sessionUtils.js';
|
||||||
import { MessageType, ToolCallStatus } from '../types.js';
|
import { MessageType, ToolCallStatus } from '../types.js';
|
||||||
|
|
||||||
@@ -125,8 +126,13 @@ export function convertSessionToHistoryFormats(
|
|||||||
|
|
||||||
for (const msg of messages) {
|
for (const msg of messages) {
|
||||||
// Add the message only if it has content
|
// Add the message only if it has content
|
||||||
|
const displayContentString = msg.displayContent
|
||||||
|
? partListUnionToString(msg.displayContent)
|
||||||
|
: undefined;
|
||||||
const contentString = partListUnionToString(msg.content);
|
const contentString = partListUnionToString(msg.content);
|
||||||
if (msg.content && contentString.trim()) {
|
const uiText = displayContentString || contentString;
|
||||||
|
|
||||||
|
if (uiText.trim()) {
|
||||||
let messageType: MessageType;
|
let messageType: MessageType;
|
||||||
switch (msg.type) {
|
switch (msg.type) {
|
||||||
case 'user':
|
case 'user':
|
||||||
@@ -141,14 +147,18 @@ export function convertSessionToHistoryFormats(
|
|||||||
case 'warning':
|
case 'warning':
|
||||||
messageType = MessageType.WARNING;
|
messageType = MessageType.WARNING;
|
||||||
break;
|
break;
|
||||||
|
case 'gemini':
|
||||||
|
messageType = MessageType.GEMINI;
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
|
checkExhaustive(msg);
|
||||||
messageType = MessageType.GEMINI;
|
messageType = MessageType.GEMINI;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
uiHistory.push({
|
uiHistory.push({
|
||||||
type: messageType,
|
type: messageType,
|
||||||
text: contentString,
|
text: uiText,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +209,9 @@ export function convertSessionToHistoryFormats(
|
|||||||
// Add regular user message
|
// Add regular user message
|
||||||
clientHistory.push({
|
clientHistory.push({
|
||||||
role: 'user',
|
role: 'user',
|
||||||
parts: [{ text: contentString }],
|
parts: Array.isArray(msg.content)
|
||||||
|
? (msg.content as Part[])
|
||||||
|
: [{ text: contentString }],
|
||||||
});
|
});
|
||||||
} else if (msg.type === 'gemini') {
|
} else if (msg.type === 'gemini') {
|
||||||
// Handle Gemini messages with potential tool calls
|
// Handle Gemini messages with potential tool calls
|
||||||
|
|||||||
@@ -890,6 +890,7 @@ ${JSON.stringify(
|
|||||||
{ model: 'default-routed-model' },
|
{ model: 'default-routed-model' },
|
||||||
initialRequest,
|
initialRequest,
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1707,6 +1708,7 @@ ${JSON.stringify(
|
|||||||
{ model: 'routed-model' },
|
{ model: 'routed-model' },
|
||||||
[{ text: 'Hi' }],
|
[{ text: 'Hi' }],
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1724,6 +1726,7 @@ ${JSON.stringify(
|
|||||||
{ model: 'routed-model' },
|
{ model: 'routed-model' },
|
||||||
[{ text: 'Hi' }],
|
[{ text: 'Hi' }],
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Second turn
|
// Second turn
|
||||||
@@ -1741,6 +1744,7 @@ ${JSON.stringify(
|
|||||||
{ model: 'routed-model' },
|
{ model: 'routed-model' },
|
||||||
[{ text: 'Continue' }],
|
[{ text: 'Continue' }],
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1758,6 +1762,7 @@ ${JSON.stringify(
|
|||||||
{ model: 'routed-model' },
|
{ model: 'routed-model' },
|
||||||
[{ text: 'Hi' }],
|
[{ text: 'Hi' }],
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
// New prompt
|
// New prompt
|
||||||
@@ -1779,6 +1784,7 @@ ${JSON.stringify(
|
|||||||
{ model: 'new-routed-model' },
|
{ model: 'new-routed-model' },
|
||||||
[{ text: 'A new topic' }],
|
[{ text: 'A new topic' }],
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -1806,6 +1812,7 @@ ${JSON.stringify(
|
|||||||
{ model: 'original-model' },
|
{ model: 'original-model' },
|
||||||
[{ text: 'Hi' }],
|
[{ text: 'Hi' }],
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
mockRouterService.route.mockResolvedValue({
|
mockRouterService.route.mockResolvedValue({
|
||||||
@@ -1828,6 +1835,7 @@ ${JSON.stringify(
|
|||||||
{ model: 'fallback-model' },
|
{ model: 'fallback-model' },
|
||||||
[{ text: 'Continue' }],
|
[{ text: 'Continue' }],
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@@ -1912,6 +1920,7 @@ ${JSON.stringify(
|
|||||||
{ model: 'default-routed-model' },
|
{ model: 'default-routed-model' },
|
||||||
initialRequest,
|
initialRequest,
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Second call with "Please continue."
|
// Second call with "Please continue."
|
||||||
@@ -1920,6 +1929,7 @@ ${JSON.stringify(
|
|||||||
{ model: 'default-routed-model' },
|
{ model: 'default-routed-model' },
|
||||||
[{ text: 'System: Please continue.' }],
|
[{ text: 'System: Please continue.' }],
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -2332,6 +2342,7 @@ ${JSON.stringify(
|
|||||||
expect.objectContaining({ model: 'model-a' }),
|
expect.objectContaining({ model: 'model-a' }),
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -3183,6 +3194,7 @@ ${JSON.stringify(
|
|||||||
expect.anything(),
|
expect.anything(),
|
||||||
[{ text: 'Please explain' }],
|
[{ text: 'Please explain' }],
|
||||||
expect.anything(),
|
expect.anything(),
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -532,6 +532,7 @@ export class GeminiClient {
|
|||||||
prompt_id: string,
|
prompt_id: string,
|
||||||
boundedTurns: number,
|
boundedTurns: number,
|
||||||
isInvalidStreamRetry: boolean,
|
isInvalidStreamRetry: boolean,
|
||||||
|
displayContent?: PartListUnion,
|
||||||
): AsyncGenerator<ServerGeminiStreamEvent, Turn> {
|
): AsyncGenerator<ServerGeminiStreamEvent, Turn> {
|
||||||
// Re-initialize turn (it was empty before if in loop, or new instance)
|
// Re-initialize turn (it was empty before if in loop, or new instance)
|
||||||
let turn = new Turn(this.getChat(), prompt_id);
|
let turn = new Turn(this.getChat(), prompt_id);
|
||||||
@@ -647,7 +648,12 @@ export class GeminiClient {
|
|||||||
yield { type: GeminiEventType.ModelInfo, value: modelToUse };
|
yield { type: GeminiEventType.ModelInfo, value: modelToUse };
|
||||||
}
|
}
|
||||||
this.currentSequenceModel = modelToUse;
|
this.currentSequenceModel = modelToUse;
|
||||||
const resultStream = turn.run(modelConfigKey, request, linkedSignal);
|
const resultStream = turn.run(
|
||||||
|
modelConfigKey,
|
||||||
|
request,
|
||||||
|
linkedSignal,
|
||||||
|
displayContent,
|
||||||
|
);
|
||||||
let isError = false;
|
let isError = false;
|
||||||
let isInvalidStream = false;
|
let isInvalidStream = false;
|
||||||
|
|
||||||
@@ -708,6 +714,7 @@ export class GeminiClient {
|
|||||||
prompt_id,
|
prompt_id,
|
||||||
boundedTurns - 1,
|
boundedTurns - 1,
|
||||||
true,
|
true,
|
||||||
|
displayContent,
|
||||||
);
|
);
|
||||||
return turn;
|
return turn;
|
||||||
}
|
}
|
||||||
@@ -739,7 +746,8 @@ export class GeminiClient {
|
|||||||
signal,
|
signal,
|
||||||
prompt_id,
|
prompt_id,
|
||||||
boundedTurns - 1,
|
boundedTurns - 1,
|
||||||
// isInvalidStreamRetry is false
|
false, // isInvalidStreamRetry is false
|
||||||
|
displayContent,
|
||||||
);
|
);
|
||||||
return turn;
|
return turn;
|
||||||
}
|
}
|
||||||
@@ -754,6 +762,7 @@ export class GeminiClient {
|
|||||||
prompt_id: string,
|
prompt_id: string,
|
||||||
turns: number = MAX_TURNS,
|
turns: number = MAX_TURNS,
|
||||||
isInvalidStreamRetry: boolean = false,
|
isInvalidStreamRetry: boolean = false,
|
||||||
|
displayContent?: PartListUnion,
|
||||||
): AsyncGenerator<ServerGeminiStreamEvent, Turn> {
|
): AsyncGenerator<ServerGeminiStreamEvent, Turn> {
|
||||||
if (!isInvalidStreamRetry) {
|
if (!isInvalidStreamRetry) {
|
||||||
this.config.resetTurn();
|
this.config.resetTurn();
|
||||||
@@ -809,6 +818,7 @@ export class GeminiClient {
|
|||||||
prompt_id,
|
prompt_id,
|
||||||
boundedTurns,
|
boundedTurns,
|
||||||
isInvalidStreamRetry,
|
isInvalidStreamRetry,
|
||||||
|
displayContent,
|
||||||
);
|
);
|
||||||
|
|
||||||
// Fire AfterAgent hook if we have a turn and no pending tools
|
// Fire AfterAgent hook if we have a turn and no pending tools
|
||||||
@@ -860,6 +870,8 @@ export class GeminiClient {
|
|||||||
signal,
|
signal,
|
||||||
prompt_id,
|
prompt_id,
|
||||||
boundedTurns - 1,
|
boundedTurns - 1,
|
||||||
|
false,
|
||||||
|
displayContent,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -268,6 +268,7 @@ export class GeminiChat {
|
|||||||
* @param message - The list of messages to send.
|
* @param message - The list of messages to send.
|
||||||
* @param prompt_id - The ID of the prompt.
|
* @param prompt_id - The ID of the prompt.
|
||||||
* @param signal - An abort signal for this message.
|
* @param signal - An abort signal for this message.
|
||||||
|
* @param displayContent - An optional user-friendly version of the message to record.
|
||||||
* @return The model's response.
|
* @return The model's response.
|
||||||
*
|
*
|
||||||
* @example
|
* @example
|
||||||
@@ -286,6 +287,7 @@ export class GeminiChat {
|
|||||||
message: PartListUnion,
|
message: PartListUnion,
|
||||||
prompt_id: string,
|
prompt_id: string,
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
|
displayContent?: PartListUnion,
|
||||||
): Promise<AsyncGenerator<StreamEvent>> {
|
): Promise<AsyncGenerator<StreamEvent>> {
|
||||||
await this.sendPromise;
|
await this.sendPromise;
|
||||||
|
|
||||||
@@ -302,12 +304,25 @@ export class GeminiChat {
|
|||||||
// Record user input - capture complete message with all parts (text, files, images, etc.)
|
// Record user input - capture complete message with all parts (text, files, images, etc.)
|
||||||
// but skip recording function responses (tool call results) as they should be stored in tool call records
|
// but skip recording function responses (tool call results) as they should be stored in tool call records
|
||||||
if (!isFunctionResponse(userContent)) {
|
if (!isFunctionResponse(userContent)) {
|
||||||
const userMessage = Array.isArray(message) ? message : [message];
|
const userMessageParts = userContent.parts || [];
|
||||||
const userMessageContent = partListUnionToString(toParts(userMessage));
|
const userMessageContent = partListUnionToString(userMessageParts);
|
||||||
|
|
||||||
|
let finalDisplayContent: Part[] | undefined = undefined;
|
||||||
|
if (displayContent !== undefined) {
|
||||||
|
const displayParts = toParts(
|
||||||
|
Array.isArray(displayContent) ? displayContent : [displayContent],
|
||||||
|
);
|
||||||
|
const displayContentString = partListUnionToString(displayParts);
|
||||||
|
if (displayContentString !== userMessageContent) {
|
||||||
|
finalDisplayContent = displayParts;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
this.chatRecordingService.recordMessage({
|
this.chatRecordingService.recordMessage({
|
||||||
model,
|
model,
|
||||||
type: 'user',
|
type: 'user',
|
||||||
content: userMessageContent,
|
content: userMessageParts,
|
||||||
|
displayContent: finalDisplayContent,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -102,6 +102,7 @@ describe('Turn', () => {
|
|||||||
reqParts,
|
reqParts,
|
||||||
'prompt-id-1',
|
'prompt-id-1',
|
||||||
expect.any(AbortSignal),
|
expect.any(AbortSignal),
|
||||||
|
undefined,
|
||||||
);
|
);
|
||||||
|
|
||||||
expect(events).toEqual([
|
expect(events).toEqual([
|
||||||
|
|||||||
@@ -248,6 +248,7 @@ export class Turn {
|
|||||||
modelConfigKey: ModelConfigKey,
|
modelConfigKey: ModelConfigKey,
|
||||||
req: PartListUnion,
|
req: PartListUnion,
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
|
displayContent?: PartListUnion,
|
||||||
): AsyncGenerator<ServerGeminiStreamEvent> {
|
): AsyncGenerator<ServerGeminiStreamEvent> {
|
||||||
try {
|
try {
|
||||||
// Note: This assumes `sendMessageStream` yields events like
|
// Note: This assumes `sendMessageStream` yields events like
|
||||||
@@ -257,6 +258,7 @@ export class Turn {
|
|||||||
req,
|
req,
|
||||||
this.prompt_id,
|
this.prompt_id,
|
||||||
signal,
|
signal,
|
||||||
|
displayContent,
|
||||||
);
|
);
|
||||||
|
|
||||||
for await (const streamEvent of responseStream) {
|
for await (const streamEvent of responseStream) {
|
||||||
|
|||||||
@@ -130,6 +130,7 @@ describe('ChatRecordingService', () => {
|
|||||||
chatRecordingService.recordMessage({
|
chatRecordingService.recordMessage({
|
||||||
type: 'user',
|
type: 'user',
|
||||||
content: 'Hello',
|
content: 'Hello',
|
||||||
|
displayContent: 'User Hello',
|
||||||
model: 'gemini-pro',
|
model: 'gemini-pro',
|
||||||
});
|
});
|
||||||
expect(mkdirSyncSpy).toHaveBeenCalled();
|
expect(mkdirSyncSpy).toHaveBeenCalled();
|
||||||
@@ -139,6 +140,7 @@ describe('ChatRecordingService', () => {
|
|||||||
) as ConversationRecord;
|
) as ConversationRecord;
|
||||||
expect(conversation.messages).toHaveLength(1);
|
expect(conversation.messages).toHaveLength(1);
|
||||||
expect(conversation.messages[0].content).toBe('Hello');
|
expect(conversation.messages[0].content).toBe('Hello');
|
||||||
|
expect(conversation.messages[0].displayContent).toBe('User Hello');
|
||||||
expect(conversation.messages[0].type).toBe('user');
|
expect(conversation.messages[0].type).toBe('user');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ export interface BaseMessageRecord {
|
|||||||
id: string;
|
id: string;
|
||||||
timestamp: string;
|
timestamp: string;
|
||||||
content: PartListUnion;
|
content: PartListUnion;
|
||||||
|
displayContent?: PartListUnion;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -207,12 +208,14 @@ export class ChatRecordingService {
|
|||||||
private newMessage(
|
private newMessage(
|
||||||
type: ConversationRecordExtra['type'],
|
type: ConversationRecordExtra['type'],
|
||||||
content: PartListUnion,
|
content: PartListUnion,
|
||||||
|
displayContent?: PartListUnion,
|
||||||
): MessageRecord {
|
): MessageRecord {
|
||||||
return {
|
return {
|
||||||
id: randomUUID(),
|
id: randomUUID(),
|
||||||
timestamp: new Date().toISOString(),
|
timestamp: new Date().toISOString(),
|
||||||
type,
|
type,
|
||||||
content,
|
content,
|
||||||
|
displayContent,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,12 +226,17 @@ export class ChatRecordingService {
|
|||||||
model: string | undefined;
|
model: string | undefined;
|
||||||
type: ConversationRecordExtra['type'];
|
type: ConversationRecordExtra['type'];
|
||||||
content: PartListUnion;
|
content: PartListUnion;
|
||||||
|
displayContent?: PartListUnion;
|
||||||
}): void {
|
}): void {
|
||||||
if (!this.conversationFile) return;
|
if (!this.conversationFile) return;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
this.updateConversation((conversation) => {
|
this.updateConversation((conversation) => {
|
||||||
const msg = this.newMessage(message.type, message.content);
|
const msg = this.newMessage(
|
||||||
|
message.type,
|
||||||
|
message.content,
|
||||||
|
message.displayContent,
|
||||||
|
);
|
||||||
if (msg.type === 'gemini') {
|
if (msg.type === 'gemini') {
|
||||||
// If it's a new Gemini message then incorporate any queued thoughts.
|
// If it's a new Gemini message then incorporate any queued thoughts.
|
||||||
conversation.messages.push({
|
conversation.messages.push({
|
||||||
|
|||||||
Reference in New Issue
Block a user