mirror of
https://github.com/google-gemini/gemini-cli.git
synced 2026-05-13 13:22:35 -07:00
fix(core): properly format markdown in AskUser tool by unescaping newlines (#26349)
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
This commit is contained in:
@@ -5,7 +5,12 @@
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { AskUserTool, isCompletedAskUserTool } from './ask-user.js';
|
||||
import {
|
||||
AskUserTool,
|
||||
isCompletedAskUserTool,
|
||||
type AskUserParams,
|
||||
type AskUserInvocation,
|
||||
} from './ask-user.js';
|
||||
import { QuestionType, type Question } from '../confirmation-bus/types.js';
|
||||
import type { MessageBus } from '../confirmation-bus/message-bus.js';
|
||||
import { ToolConfirmationOutcome } from './tools.js';
|
||||
@@ -63,6 +68,80 @@ describe('AskUserTool', () => {
|
||||
expect(tool.displayName).toBe('Ask User');
|
||||
});
|
||||
|
||||
describe('createInvocation and normalization', () => {
|
||||
it('should unescape double-escaped newlines in question parameters', async () => {
|
||||
const params: AskUserParams = {
|
||||
questions: [
|
||||
{
|
||||
question: 'Line 1\\nLine 2',
|
||||
header: 'Header\\nTest',
|
||||
placeholder: 'Placeholder\\nTest',
|
||||
type: QuestionType.CHOICE,
|
||||
options: [
|
||||
{ label: 'Option\\n1', description: 'Desc\\n1' },
|
||||
{ label: 'Option\\n2', description: 'Desc\\n2' },
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const invocation = (
|
||||
tool as unknown as {
|
||||
createInvocation: (
|
||||
params: AskUserParams,
|
||||
messageBus: MessageBus,
|
||||
toolName: string,
|
||||
toolDisplayName: string,
|
||||
) => AskUserInvocation;
|
||||
}
|
||||
).createInvocation(params, mockMessageBus, 'ask_user', 'Ask User');
|
||||
const details = await invocation.shouldConfirmExecute(
|
||||
new AbortController().signal,
|
||||
);
|
||||
|
||||
if (!details || details.type !== 'ask_user') {
|
||||
throw new Error('Expected ask_user details');
|
||||
}
|
||||
|
||||
expect(details.questions[0].question).toBe('Line 1\nLine 2');
|
||||
expect(details.questions[0].header).toBe('Header\nTest');
|
||||
expect(details.questions[0].placeholder).toBe('Placeholder\nTest');
|
||||
expect(details.questions[0].options?.[0].label).toBe('Option\n1');
|
||||
expect(details.questions[0].options?.[0].description).toBe('Desc\n1');
|
||||
});
|
||||
|
||||
it('should handle carriage returns and literal newlines', async () => {
|
||||
const params: AskUserParams = {
|
||||
questions: [
|
||||
{
|
||||
question: 'Line 1\\r\\nLine 2\nLine 3',
|
||||
header: 'Header',
|
||||
type: QuestionType.TEXT,
|
||||
},
|
||||
],
|
||||
};
|
||||
const invocation = (
|
||||
tool as unknown as {
|
||||
createInvocation: (
|
||||
params: AskUserParams,
|
||||
messageBus: MessageBus,
|
||||
toolName: string,
|
||||
toolDisplayName: string,
|
||||
) => AskUserInvocation;
|
||||
}
|
||||
).createInvocation(params, mockMessageBus, 'ask_user', 'Ask User');
|
||||
const details = await invocation.shouldConfirmExecute(
|
||||
new AbortController().signal,
|
||||
);
|
||||
|
||||
if (!details || details.type !== 'ask_user') {
|
||||
throw new Error('Expected ask_user details');
|
||||
}
|
||||
|
||||
expect(details.questions[0].question).toBe('Line 1\nLine 2\nLine 3');
|
||||
});
|
||||
});
|
||||
|
||||
describe('validateToolParams', () => {
|
||||
it('should return error if questions is missing', () => {
|
||||
// @ts-expect-error - Intentionally invalid params
|
||||
|
||||
@@ -93,7 +93,38 @@ export class AskUserTool extends BaseDeclarativeTool<
|
||||
toolName: string,
|
||||
toolDisplayName: string,
|
||||
): AskUserInvocation {
|
||||
return new AskUserInvocation(params, messageBus, toolName, toolDisplayName);
|
||||
const unescape = (str: string): string =>
|
||||
str.replace(/\\r\\n/g, '\n').replace(/\\n/g, '\n');
|
||||
|
||||
const normalizedParams: AskUserParams = {
|
||||
questions: params.questions.map((q) => {
|
||||
const normalizedQ: Question = {
|
||||
...q,
|
||||
type: q.type,
|
||||
question: unescape(q.question),
|
||||
};
|
||||
if (q.header) normalizedQ.header = unescape(q.header);
|
||||
if (q.placeholder) normalizedQ.placeholder = unescape(q.placeholder);
|
||||
|
||||
if (q.options) {
|
||||
normalizedQ.options = q.options.map((opt) => ({
|
||||
...opt,
|
||||
label: unescape(opt.label),
|
||||
description: opt.description?.trim()
|
||||
? unescape(opt.description.trim())
|
||||
: '',
|
||||
}));
|
||||
}
|
||||
return normalizedQ;
|
||||
}),
|
||||
};
|
||||
|
||||
return new AskUserInvocation(
|
||||
normalizedParams,
|
||||
messageBus,
|
||||
toolName,
|
||||
toolDisplayName,
|
||||
);
|
||||
}
|
||||
|
||||
override async validateBuildAndExecute(
|
||||
|
||||
Reference in New Issue
Block a user