Clarify regex vs literal text in search result messages (#1757)

* Initial plan

* Improve presentation of regex-based searches in agent mode

- Updated FindTextInFilesTool to indicate "regex" vs "text" in search messages
- Updated Claude grep tool formatter to show "Searched for regex"
- Added test for literal text search message format
- Updated existing test to match new regex message format

Co-authored-by: roblourens <323878+roblourens@users.noreply.github.com>

* Make regex/text keywords localizable via l10n

- Moved 'regex' and 'text' keywords inside l10n.t() calls
- This allows localization system to translate or keep terms as appropriate per language
- Addresses localization concern about whether 'regex' should be localized

Co-authored-by: roblourens <323878+roblourens@users.noreply.github.com>

* Refactor nested ternary operators into helper method

- Extract message generation logic into getResultMessage helper
- Improves code readability and maintainability
- All tests passing

Co-authored-by: roblourens <323878+roblourens@users.noreply.github.com>

* Update ChatSessionContentProvider test snapshot

- Updated snapshot to reflect new grep message format
- Changed from "Searched text for" to "Searched for regex"
- All tests now passing (8/8)

Co-authored-by: roblourens <323878+roblourens@users.noreply.github.com>

---------

Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: roblourens <323878+roblourens@users.noreply.github.com>
This commit is contained in:
Copilot
2025-11-03 00:49:59 +00:00
committed by GitHub
parent 9cea8e13cc
commit c19aeea325
4 changed files with 33 additions and 9 deletions
@@ -75,7 +75,7 @@ function formatGlobInvocation(invocation: ChatToolInvocationPart, toolUse: Anthr
function formatGrepInvocation(invocation: ChatToolInvocationPart, toolUse: Anthropic.ToolUseBlock): void {
const pattern: string = (toolUse.input as any)?.pattern ?? '';
invocation.invocationMessage = new MarkdownString(l10n.t("Searched text for `{0}`", pattern));
invocation.invocationMessage = new MarkdownString(l10n.t("Searched for regex `{0}`", pattern));
}
function formatLSInvocation(invocation: ChatToolInvocationPart, toolUse: Anthropic.ToolUseBlock): void {
@@ -30,7 +30,7 @@ exports[`ChatSessionContentProvider > loads real fixture file with tool invocati
{
"parts": [
{
"invocationMessage": "Searched text for \`ClaudeAgentManager\`",
"invocationMessage": "Searched for regex \`ClaudeAgentManager\`",
"isError": undefined,
"toolCallId": "toolu_01FqdrDGdxXUWRRLziM7gS2R",
"toolName": "Grep",
@@ -95,16 +95,28 @@ export class FindTextInFilesTool implements ICopilotTool<IFindTextInFilesToolPar
return [];
}).slice(0, maxResults);
const query = this.formatQueryString(options.input);
result.toolResultMessage = textMatches.length === 0 ?
new MarkdownString(l10n.t`Searched text for ${query}, no results`) :
textMatches.length === 1 ?
new MarkdownString(l10n.t`Searched text for ${query}, 1 result`) :
new MarkdownString(l10n.t`Searched text for ${query}, ${textMatches.length} results`);
result.toolResultMessage = this.getResultMessage(isRegExp, query, textMatches.length);
result.toolResultDetails = textMatches;
return result;
}
private getResultMessage(isRegExp: boolean, query: string, count: number): MarkdownString {
if (count === 0) {
return isRegExp
? new MarkdownString(l10n.t`Searched for regex ${query}, no results`)
: new MarkdownString(l10n.t`Searched for text ${query}, no results`);
} else if (count === 1) {
return isRegExp
? new MarkdownString(l10n.t`Searched for regex ${query}, 1 result`)
: new MarkdownString(l10n.t`Searched for text ${query}, 1 result`);
} else {
return isRegExp
? new MarkdownString(l10n.t`Searched for regex ${query}, ${count} results`)
: new MarkdownString(l10n.t`Searched for text ${query}, ${count} results`);
}
}
private isValidRegex(pattern: string): boolean {
try {
new RegExp(pattern);
@@ -138,8 +150,12 @@ export class FindTextInFilesTool implements ICopilotTool<IFindTextInFilesToolPar
}
prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions<IFindTextInFilesToolParams>, token: vscode.CancellationToken): vscode.ProviderResult<vscode.PreparedToolInvocation> {
const isRegExp = options.input.isRegexp ?? true;
const query = this.formatQueryString(options.input);
return {
invocationMessage: new MarkdownString(l10n.t`Searching text for ${this.formatQueryString(options.input)}`),
invocationMessage: isRegExp ?
new MarkdownString(l10n.t`Searching for regex ${query}`) :
new MarkdownString(l10n.t`Searching for text ${query}`),
};
}
@@ -89,7 +89,15 @@ suite('FindTextInFiles', () => {
const tool = accessor.get(IInstantiationService).createInstance(FindTextInFilesTool);
const prepared = await tool.prepareInvocation({ input: { query: 'hello `world`' }, }, CancellationToken.None);
expect((prepared?.invocationMessage as any as MarkdownString).value).toMatchInlineSnapshot(`"Searching text for \`\` hello \`world\` \`\`"`);
expect((prepared?.invocationMessage as any as MarkdownString).value).toMatchInlineSnapshot(`"Searching for regex \`\` hello \`world\` \`\`"`);
});
test('prepares invocation message with text for literal search', async () => {
setup(new RelativePattern(URI.file(workspaceFolder), ''), false);
const tool = accessor.get(IInstantiationService).createInstance(FindTextInFilesTool);
const prepared = await tool.prepareInvocation({ input: { query: 'hello', isRegexp: false }, }, CancellationToken.None);
expect((prepared?.invocationMessage as any as MarkdownString).value).toMatchInlineSnapshot(`"Searching for text \`hello\`"`);
});
test('retries with plain text when regex yields no results', async () => {