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:
Adib234
2026-05-04 16:59:11 -04:00
committed by GitHub
parent 4d1ca92a19
commit 6a3175e973
2 changed files with 112 additions and 2 deletions
+80 -1
View File
@@ -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
+32 -1
View File
@@ -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(