From f7b7f470d79ca8fea738cacb6359607ec3294e47 Mon Sep 17 00:00:00 2001 From: Bryan Chen Date: Fri, 3 Oct 2025 16:08:30 -0700 Subject: [PATCH 1/6] feat: add timeout handling for file and text search operations --- src/extension/tools/node/findFilesTool.tsx | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/extension/tools/node/findFilesTool.tsx b/src/extension/tools/node/findFilesTool.tsx index e01d53363..a1a44bd1a 100644 --- a/src/extension/tools/node/findFilesTool.tsx +++ b/src/extension/tools/node/findFilesTool.tsx @@ -40,14 +40,25 @@ export class FindFilesTool implements ICopilotTool { // The input _should_ be a pattern matching inside a workspace, folder, but sometimes we get absolute paths, so try to resolve them const pattern = inputGlobToPattern(options.input.query, this.workspaceService); - const results = await this.searchService.findFiles(pattern, undefined, token); + // try find files with a timeout of 10s + // TODO: consider making the timeout configurable + const timeoutInMs = 10_000; + const results = await Promise.race([ + this.searchService.findFiles(pattern, undefined, token), + new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout in searching files')), timeoutInMs)) + ]); + checkCancellation(token); const maxResults = options.input.maxResults ?? 20; const resultsToShow = results.slice(0, maxResults); - const result = new ExtendedLanguageModelToolResult([ - new LanguageModelPromptTsxPart( - await renderPromptElementJSON(this.instantiationService, FindFilesResult, { fileResults: resultsToShow, totalResults: results.length }, options.tokenizationOptions, token))]); + // Render the prompt element with a timeout + const prompt = await Promise.race([ + renderPromptElementJSON(this.instantiationService, FindFilesResult, { fileResults: resultsToShow, totalResults: results.length }, options.tokenizationOptions, token), + new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout in rendering prompt element')), timeoutInMs)) + ]); + + const result = new ExtendedLanguageModelToolResult([new LanguageModelPromptTsxPart(prompt)]); const query = `\`${options.input.query}\``; result.toolResultMessage = resultsToShow.length === 0 ? new MarkdownString(l10n.t`Searched for files matching ${query}, no matches`) : From b176a1256863f725f3e0922c01ab3bd04194d1f7 Mon Sep 17 00:00:00 2001 From: Bryan Chen Date: Fri, 3 Oct 2025 16:17:15 -0700 Subject: [PATCH 2/6] feat: add timeout handling for search and rendering operations in FindTextInFilesTool --- .../tools/node/findTextInFilesTool.tsx | 24 +++++++++++++++---- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/src/extension/tools/node/findTextInFilesTool.tsx b/src/extension/tools/node/findTextInFilesTool.tsx index 964ee9b22..b27c9e60f 100644 --- a/src/extension/tools/node/findTextInFilesTool.tsx +++ b/src/extension/tools/node/findTextInFilesTool.tsx @@ -51,14 +51,28 @@ export class FindTextInFilesTool implements ICopilotTool((_, reject) => setTimeout(() => reject(new Error("Timeout in searching text in files")), timeoutInMs))]); + + checkCancellation(token); if (!results.length && queryIsValidRegex) { - results = await this.searchAndCollectResults(options.input.query, !isRegExp, patterns, maxResults, token); + results = await Promise.race([ + this.searchAndCollectResults(options.input.query, !isRegExp, patterns, maxResults, token), + new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout in searching text in files")), timeoutInMs))]); } - const result = new ExtendedLanguageModelToolResult([ - new LanguageModelPromptTsxPart( - await renderPromptElementJSON(this.instantiationService, FindTextInFilesResult, { textResults: results, maxResults, askedForTooManyResults: Boolean(askedForTooManyResults) }, options.tokenizationOptions, token))]); + checkCancellation(token); + const prompt = await Promise.race([ + renderPromptElementJSON(this.instantiationService, FindTextInFilesResult, { textResults: results, maxResults, askedForTooManyResults: Boolean(askedForTooManyResults) }, options.tokenizationOptions, token), + new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout in rendering prompt element')), timeoutInMs)) + ]); + + const result = new ExtendedLanguageModelToolResult([new LanguageModelPromptTsxPart(prompt)]); const textMatches = results.flatMap(r => { if ('ranges' in r) { return asArray(r.ranges).map(rangeInfo => new Location(r.uri, rangeInfo.sourceRange)); From e1d6814e9d67baad33e556fcd100d7dad2717e97 Mon Sep 17 00:00:00 2001 From: Bryan Chen Date: Mon, 6 Oct 2025 11:10:06 -0700 Subject: [PATCH 3/6] use raceTimeout and raceCancellation --- src/extension/tools/node/findFilesTool.tsx | 26 ++++++++---- .../tools/node/findTextInFilesTool.tsx | 40 +++++++++++++------ 2 files changed, 45 insertions(+), 21 deletions(-) diff --git a/src/extension/tools/node/findFilesTool.tsx b/src/extension/tools/node/findFilesTool.tsx index a1a44bd1a..7e8938bd8 100644 --- a/src/extension/tools/node/findFilesTool.tsx +++ b/src/extension/tools/node/findFilesTool.tsx @@ -11,6 +11,7 @@ import { URI } from '../../../util/vs/base/common/uri'; import * as l10n from '@vscode/l10n'; import { ISearchService } from '../../../platform/search/common/searchService'; import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService'; +import { raceCancellation, raceTimeout } from '../../../util/vs/base/common/async'; import { CancellationToken } from '../../../util/vs/base/common/cancellation'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { ExtendedLanguageModelToolResult, LanguageModelPromptTsxPart, MarkdownString } from '../../../vscodeTypes'; @@ -43,20 +44,29 @@ export class FindFilesTool implements ICopilotTool { // try find files with a timeout of 10s // TODO: consider making the timeout configurable const timeoutInMs = 10_000; - const results = await Promise.race([ - this.searchService.findFiles(pattern, undefined, token), - new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout in searching files')), timeoutInMs)) - ]); + const results = await raceTimeout( + raceCancellation( + Promise.resolve(this.searchService.findFiles(pattern, undefined, token)), + token + ), + timeoutInMs + ); + + if (results === undefined) { + throw new Error('Timeout in searching files'); + } checkCancellation(token); const maxResults = options.input.maxResults ?? 20; const resultsToShow = results.slice(0, maxResults); // Render the prompt element with a timeout - const prompt = await Promise.race([ - renderPromptElementJSON(this.instantiationService, FindFilesResult, { fileResults: resultsToShow, totalResults: results.length }, options.tokenizationOptions, token), - new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout in rendering prompt element')), timeoutInMs)) - ]); + const prompt = await raceTimeout( + raceCancellation( + renderPromptElementJSON(this.instantiationService, FindFilesResult, { fileResults: resultsToShow, totalResults: results.length }, options.tokenizationOptions, token), + token), + timeoutInMs + ); const result = new ExtendedLanguageModelToolResult([new LanguageModelPromptTsxPart(prompt)]); const query = `\`${options.input.query}\``; diff --git a/src/extension/tools/node/findTextInFilesTool.tsx b/src/extension/tools/node/findTextInFilesTool.tsx index b27c9e60f..ae62b73c3 100644 --- a/src/extension/tools/node/findTextInFilesTool.tsx +++ b/src/extension/tools/node/findTextInFilesTool.tsx @@ -11,6 +11,7 @@ import { IPromptPathRepresentationService } from '../../../platform/prompts/comm import { ISearchService } from '../../../platform/search/common/searchService'; import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService'; import { asArray } from '../../../util/vs/base/common/arrays'; +import { raceCancellation, raceTimeout } from '../../../util/vs/base/common/async'; import { CancellationToken } from '../../../util/vs/base/common/cancellation'; import { count } from '../../../util/vs/base/common/strings'; import { URI } from '../../../util/vs/base/common/uri'; @@ -55,22 +56,35 @@ export class FindTextInFilesTool implements ICopilotTool((_, reject) => setTimeout(() => reject(new Error("Timeout in searching text in files")), timeoutInMs))]); + let results = await raceTimeout( + raceCancellation( + this.searchAndCollectResults(options.input.query, isRegExp, patterns, maxResults, token), + token + ), + timeoutInMs + ); + + if (results && !results.length && queryIsValidRegex) { + results = await raceTimeout( + raceCancellation( + this.searchAndCollectResults(options.input.query, !isRegExp, patterns, maxResults, token), + token + ), + timeoutInMs + ); + } - checkCancellation(token); - if (!results.length && queryIsValidRegex) { - results = await Promise.race([ - this.searchAndCollectResults(options.input.query, !isRegExp, patterns, maxResults, token), - new Promise((_, reject) => setTimeout(() => reject(new Error("Timeout in searching text in files")), timeoutInMs))]); + if (results === undefined) { + throw new Error('Timeout in searching files'); } - checkCancellation(token); - const prompt = await Promise.race([ - renderPromptElementJSON(this.instantiationService, FindTextInFilesResult, { textResults: results, maxResults, askedForTooManyResults: Boolean(askedForTooManyResults) }, options.tokenizationOptions, token), - new Promise((_, reject) => setTimeout(() => reject(new Error('Timeout in rendering prompt element')), timeoutInMs)) - ]); + const prompt = await raceTimeout( + raceCancellation( + renderPromptElementJSON(this.instantiationService, FindTextInFilesResult, { textResults: results, maxResults, askedForTooManyResults: Boolean(askedForTooManyResults) }, options.tokenizationOptions, token), + token + ), + timeoutInMs + ); const result = new ExtendedLanguageModelToolResult([new LanguageModelPromptTsxPart(prompt)]); const textMatches = results.flatMap(r => { From a4b3dde70e8d8d390aab6f21f7c281d1fe22273e Mon Sep 17 00:00:00 2001 From: Bryan Chen Date: Mon, 6 Oct 2025 11:36:37 -0700 Subject: [PATCH 4/6] Updated per comments --- src/extension/tools/node/findFilesTool.tsx | 4 ++++ src/extension/tools/node/findTextInFilesTool.tsx | 7 ++++++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/extension/tools/node/findFilesTool.tsx b/src/extension/tools/node/findFilesTool.tsx index 7e8938bd8..95cf16cc4 100644 --- a/src/extension/tools/node/findFilesTool.tsx +++ b/src/extension/tools/node/findFilesTool.tsx @@ -68,6 +68,10 @@ export class FindFilesTool implements ICopilotTool { timeoutInMs ); + if (prompt === undefined) { + throw new Error('Timeout in rendering prompt'); + } + const result = new ExtendedLanguageModelToolResult([new LanguageModelPromptTsxPart(prompt)]); const query = `\`${options.input.query}\``; result.toolResultMessage = resultsToShow.length === 0 ? diff --git a/src/extension/tools/node/findTextInFilesTool.tsx b/src/extension/tools/node/findTextInFilesTool.tsx index ae62b73c3..64d908f50 100644 --- a/src/extension/tools/node/findTextInFilesTool.tsx +++ b/src/extension/tools/node/findTextInFilesTool.tsx @@ -75,7 +75,7 @@ export class FindTextInFilesTool implements ICopilotTool { if ('ranges' in r) { @@ -127,6 +131,7 @@ export class FindTextInFilesTool implements ICopilotTool setTimeout(r, 5000)); // yield to allow cancellation to be processed checkCancellation(token); results.push(item); } From 86bdb0e376a1eeeb5cd2bf7fc2aad8f9c498a225 Mon Sep 17 00:00:00 2001 From: Bryan Chen Date: Mon, 6 Oct 2025 14:02:27 -0700 Subject: [PATCH 5/6] extract a raceTimeoutAndCancellationError() --- src/extension/tools/node/findFilesTool.tsx | 35 ++++++------ .../tools/node/findTextInFilesTool.tsx | 55 +++++++++---------- 2 files changed, 42 insertions(+), 48 deletions(-) diff --git a/src/extension/tools/node/findFilesTool.tsx b/src/extension/tools/node/findFilesTool.tsx index 95cf16cc4..9656120dd 100644 --- a/src/extension/tools/node/findFilesTool.tsx +++ b/src/extension/tools/node/findFilesTool.tsx @@ -11,7 +11,7 @@ import { URI } from '../../../util/vs/base/common/uri'; import * as l10n from '@vscode/l10n'; import { ISearchService } from '../../../platform/search/common/searchService'; import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService'; -import { raceCancellation, raceTimeout } from '../../../util/vs/base/common/async'; +import { raceCancellationError, raceTimeout } from '../../../util/vs/base/common/async'; import { CancellationToken } from '../../../util/vs/base/common/cancellation'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { ExtendedLanguageModelToolResult, LanguageModelPromptTsxPart, MarkdownString } from '../../../vscodeTypes'; @@ -41,31 +41,30 @@ export class FindFilesTool implements ICopilotTool { // The input _should_ be a pattern matching inside a workspace, folder, but sometimes we get absolute paths, so try to resolve them const pattern = inputGlobToPattern(options.input.query, this.workspaceService); - // try find files with a timeout of 10s - // TODO: consider making the timeout configurable - const timeoutInMs = 10_000; - const results = await raceTimeout( - raceCancellation( - Promise.resolve(this.searchService.findFiles(pattern, undefined, token)), - token - ), - timeoutInMs - ); + // try find files with a timeout of 20s + const timeoutInMs = 20_000; + async function raceTimeoutAndCancellationError(promise: Promise, timeoutMessage: string): Promise { + const result = await raceTimeout(raceCancellationError(promise, token), timeoutInMs); + if (result === undefined) { + throw new Error(timeoutMessage); + } - if (results === undefined) { - throw new Error('Timeout in searching files'); + return result; } + const results = await raceTimeoutAndCancellationError( + Promise.resolve(this.searchService.findFiles(pattern, undefined, token)), + 'Timeout in searching files' + ); + checkCancellation(token); const maxResults = options.input.maxResults ?? 20; const resultsToShow = results.slice(0, maxResults); // Render the prompt element with a timeout - const prompt = await raceTimeout( - raceCancellation( - renderPromptElementJSON(this.instantiationService, FindFilesResult, { fileResults: resultsToShow, totalResults: results.length }, options.tokenizationOptions, token), - token), - timeoutInMs + const prompt = await raceTimeoutAndCancellationError( + renderPromptElementJSON(this.instantiationService, FindFilesResult, { fileResults: resultsToShow, totalResults: results.length }, options.tokenizationOptions, token), + 'Timeout in rendering prompt' ); if (prompt === undefined) { diff --git a/src/extension/tools/node/findTextInFilesTool.tsx b/src/extension/tools/node/findTextInFilesTool.tsx index 64d908f50..53875476a 100644 --- a/src/extension/tools/node/findTextInFilesTool.tsx +++ b/src/extension/tools/node/findTextInFilesTool.tsx @@ -11,7 +11,7 @@ import { IPromptPathRepresentationService } from '../../../platform/prompts/comm import { ISearchService } from '../../../platform/search/common/searchService'; import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService'; import { asArray } from '../../../util/vs/base/common/arrays'; -import { raceCancellation, raceTimeout } from '../../../util/vs/base/common/async'; +import { raceCancellationError, raceTimeout } from '../../../util/vs/base/common/async'; import { CancellationToken } from '../../../util/vs/base/common/cancellation'; import { count } from '../../../util/vs/base/common/strings'; import { URI } from '../../../util/vs/base/common/uri'; @@ -53,24 +53,27 @@ export class FindTextInFilesTool implements ICopilotTool(promise: Promise, timeoutMessage: string): Promise { + const result = await raceTimeout(raceCancellationError(promise, token), timeoutInMs); + if (result === undefined) { + throw new Error(timeoutMessage); + } + + return result; + } + + let results = await raceTimeoutAndCancellationError( + this.searchAndCollectResults(options.input.query, isRegExp, patterns, maxResults, token), + 'Timeout in searching text in files' ); - if (results && !results.length && queryIsValidRegex) { - results = await raceTimeout( - raceCancellation( - this.searchAndCollectResults(options.input.query, !isRegExp, patterns, maxResults, token), - token - ), - timeoutInMs + // If we still have no results, we need to try the opposite regex mode + if (!results.length && queryIsValidRegex) { + results = await raceTimeoutAndCancellationError( + this.searchAndCollectResults(options.input.query, !isRegExp, patterns, maxResults, token), + 'Timeout in searching text in files' ); } @@ -78,18 +81,11 @@ export class FindTextInFilesTool implements ICopilotTool { if ('ranges' in r) { @@ -131,7 +127,6 @@ export class FindTextInFilesTool implements ICopilotTool setTimeout(r, 5000)); // yield to allow cancellation to be processed checkCancellation(token); results.push(item); } @@ -243,8 +238,8 @@ interface IFindMatchProps extends BasePromptElementProps { * 1. Removes excessive extra character data from the match, e.g. avoiding * giant minified lines * 2. Wraps the match in a tag - * 3. Prioritizes lines in the middle of the match where the range lies - */ + * 3. Prioritizes lines in the middle of the match where the range lies + */ export class FindMatch extends PromptElement { constructor( props: PromptElementProps, From 7f0cd3521cff006b5a0d24b9463ed0cc609aeb42 Mon Sep 17 00:00:00 2001 From: Bryan Chen Date: Mon, 6 Oct 2025 17:17:56 -0700 Subject: [PATCH 6/6] enhance timeout handling and cancellation for file and text search operations --- src/extension/tools/node/findFilesTool.tsx | 20 ++++++----- .../tools/node/findTextInFilesTool.tsx | 33 +++++++++++-------- 2 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/extension/tools/node/findFilesTool.tsx b/src/extension/tools/node/findFilesTool.tsx index 9656120dd..471253632 100644 --- a/src/extension/tools/node/findFilesTool.tsx +++ b/src/extension/tools/node/findFilesTool.tsx @@ -12,7 +12,7 @@ import * as l10n from '@vscode/l10n'; import { ISearchService } from '../../../platform/search/common/searchService'; import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService'; import { raceCancellationError, raceTimeout } from '../../../util/vs/base/common/async'; -import { CancellationToken } from '../../../util/vs/base/common/cancellation'; +import { CancellationToken, CancellationTokenSource } from '../../../util/vs/base/common/cancellation'; import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation'; import { ExtendedLanguageModelToolResult, LanguageModelPromptTsxPart, MarkdownString } from '../../../vscodeTypes'; import { IBuildPromptContext } from '../../prompt/common/intents'; @@ -41,11 +41,18 @@ export class FindFilesTool implements ICopilotTool { // The input _should_ be a pattern matching inside a workspace, folder, but sometimes we get absolute paths, so try to resolve them const pattern = inputGlobToPattern(options.input.query, this.workspaceService); - // try find files with a timeout of 20s + // try find text with a timeout of 20s const timeoutInMs = 20_000; + // create a new cancellation token to be used in search + // so in the case of timeout, we can cancel the search + // also in the case of the parent token being cancelled, it will cancel this one too + const searchCancellation = new CancellationTokenSource(token); + async function raceTimeoutAndCancellationError(promise: Promise, timeoutMessage: string): Promise { const result = await raceTimeout(raceCancellationError(promise, token), timeoutInMs); if (result === undefined) { + // we have timed out, so cancel the search + searchCancellation.cancel(); throw new Error(timeoutMessage); } @@ -53,8 +60,8 @@ export class FindFilesTool implements ICopilotTool { } const results = await raceTimeoutAndCancellationError( - Promise.resolve(this.searchService.findFiles(pattern, undefined, token)), - 'Timeout in searching files' + Promise.resolve(this.searchService.findFiles(pattern, undefined, searchCancellation.token)), + 'Timeout in searching files, try a more specific search pattern' ); checkCancellation(token); @@ -62,10 +69,7 @@ export class FindFilesTool implements ICopilotTool { const maxResults = options.input.maxResults ?? 20; const resultsToShow = results.slice(0, maxResults); // Render the prompt element with a timeout - const prompt = await raceTimeoutAndCancellationError( - renderPromptElementJSON(this.instantiationService, FindFilesResult, { fileResults: resultsToShow, totalResults: results.length }, options.tokenizationOptions, token), - 'Timeout in rendering prompt' - ); + const prompt = await renderPromptElementJSON(this.instantiationService, FindFilesResult, { fileResults: resultsToShow, totalResults: results.length }, options.tokenizationOptions, token); if (prompt === undefined) { throw new Error('Timeout in rendering prompt'); diff --git a/src/extension/tools/node/findTextInFilesTool.tsx b/src/extension/tools/node/findTextInFilesTool.tsx index 53875476a..13482e78e 100644 --- a/src/extension/tools/node/findTextInFilesTool.tsx +++ b/src/extension/tools/node/findTextInFilesTool.tsx @@ -12,7 +12,7 @@ import { ISearchService } from '../../../platform/search/common/searchService'; import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService'; import { asArray } from '../../../util/vs/base/common/arrays'; import { raceCancellationError, raceTimeout } from '../../../util/vs/base/common/async'; -import { CancellationToken } from '../../../util/vs/base/common/cancellation'; +import { CancellationToken, CancellationTokenSource } from '../../../util/vs/base/common/cancellation'; import { count } from '../../../util/vs/base/common/strings'; import { URI } from '../../../util/vs/base/common/uri'; import { Position as EditorPosition } from '../../../util/vs/editor/common/core/position'; @@ -55,9 +55,17 @@ export class FindTextInFilesTool implements ICopilotTool(promise: Promise, timeoutMessage: string): Promise { const result = await raceTimeout(raceCancellationError(promise, token), timeoutInMs); if (result === undefined) { + // we have timed out, so cancel the search + searchCancellation.cancel(); throw new Error(timeoutMessage); } @@ -65,26 +73,25 @@ export class FindTextInFilesTool implements ICopilotTool {