diff --git a/extensions/copilot/src/extension/prompt/node/intentDetector.tsx b/extensions/copilot/src/extension/prompt/node/intentDetector.tsx
index cdb66eb6bf4..61a58119d02 100644
--- a/extensions/copilot/src/extension/prompt/node/intentDetector.tsx
+++ b/extensions/copilot/src/extension/prompt/node/intentDetector.tsx
@@ -17,6 +17,7 @@ import { ITabsAndEditorsService } from '../../../platform/tabs/common/tabsAndEdi
import { IExperimentationService } from '../../../platform/telemetry/common/nullExperimentationService';
import { ITelemetryService } from '../../../platform/telemetry/common/telemetry';
import { isNotebookCellOrNotebookChatInput } from '../../../util/common/notebooks';
+import { isFalsyOrEmpty } from '../../../util/vs/base/common/arrays';
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
import { Position, Range } from '../../../vscodeTypes';
import { getAgentForIntent, GITHUB_PLATFORM_AGENT, Intent } from '../../common/constants';
@@ -181,6 +182,11 @@ export class IntentDetector implements ChatParticipantDetectionProvider {
this.logService.trace('Building intent detector');
+ if (builtinParticipants.length === 0 && (isFalsyOrEmpty(thirdPartyParticipants))) {
+ this.logService.trace('No participants available for intent detection');
+ return undefined;
+ }
+
const endpoint = await this.endpointProvider.getChatEndpoint('copilot-fast');
const preferredIntent = await this.getPreferredIntent(location, documentContext, history, messageText);
diff --git a/extensions/copilot/src/extension/tools/test/node/replaceString/fixtures/math.js.txt b/extensions/copilot/src/extension/tools/test/node/replaceString/fixtures/math.js.txt
new file mode 100644
index 00000000000..2c5d6810026
--- /dev/null
+++ b/extensions/copilot/src/extension/tools/test/node/replaceString/fixtures/math.js.txt
@@ -0,0 +1,77 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+///
+
+console.log("foobar");
+
+// one
+// two
+// three
+
+logwo
+
+// * merge/re-use modified file entries
+// * no accept/disacrd on new session
+// * accept all hunks
+
+export function fib(nth) {
+ if (nth <= 0) return 0;
+ if (nth === 1) return 0;
+ if (nth === 2) return 1;
+ return fib(nth - 1) + fib(nth - 2);
+}
+
+///-///-////---
+
+const r = /hello/gim;
+
+export function sum(a, b) {
+ return a + b;
+}
+
+export function sub(a, b) {
+ return a - b;
+}
+
+export function sumArray(a) {
+ return a.reduce((sum, num) => sum + num, 0);
+}
+
+export function div(a, b) {
+ // console.log fff fff
+ return a / b;
+}
+
+
+
+export function mul(a, b) {
+ return a * b;
+}
+
+export function sumThreeFloats(a, b, c) {
+ return a + b + c;
+}
+
+/**
+ * Checks if a given number is a prime number.
+ *
+ * @param {number} number - The number to check for primality.
+ * @returns {boolean} - Returns true if the number is prime, otherwise false.
+ */
+export function isPrime(number) {
+ if (number <= 1) return false;
+ for (let i = 2; i <= Math.sqrt(number); i++) {
+ if (number % i === 0) {
+ return false;
+ }
+ }
+ return true;
+}
+
+
+export function isEven(n) {
+ return n % 2 === 0;
+}
diff --git a/extensions/copilot/src/extension/tools/test/node/replaceString/fixtures/math.js.txt.expected b/extensions/copilot/src/extension/tools/test/node/replaceString/fixtures/math.js.txt.expected
new file mode 100644
index 00000000000..e897e087b1d
--- /dev/null
+++ b/extensions/copilot/src/extension/tools/test/node/replaceString/fixtures/math.js.txt.expected
@@ -0,0 +1,75 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+///
+
+console.log("foobar");
+
+// one
+// two
+// three
+
+logwo
+
+// * merge/re-use modified file entries
+// * no accept/disacrd on new session
+// * accept all hunks
+
+export function fib(nth) {
+ if (nth <= 0) return 0;
+ if (nth === 1) return 0;
+ if (nth === 2) return 1;
+ return fib(nth - 1) + fib(nth - 2);
+}
+
+///-///-////---
+
+const r = /hello/gim;
+
+export function sum(a, b) {
+ return a + b;
+}
+
+export function sub(a, b) {
+ return a - b;
+}
+
+export function sumArray(a) {
+ return a.reduce((sum, num) => sum + num, 0);
+}
+
+export function div(A, b) {
+ // console.log fff fff
+ return A / b;
+}
+
+export function mul(a, b) {
+ return a * b;
+}
+
+export function sumThreeFloats(a, b, c) {
+ return a + b + c;
+}
+
+/**
+ * Checks if a given number is a prime number.
+ *
+ * @param {number} number - The number to check for primality.
+ * @returns {boolean} - Returns true if the number is prime, otherwise false.
+ */
+export function isPrime(number) {
+ if (number <= 1) return false;
+ for (let i = 2; i <= Math.sqrt(number); i++) {
+ if (number % i === 0) {
+ return false;
+ }
+ }
+ return true;
+}
+
+
+export function isEven(n) {
+ return n % 2 === 0;
+}
diff --git a/extensions/copilot/src/extension/tools/test/node/replaceString/replaceStringTool.spec.tsx b/extensions/copilot/src/extension/tools/test/node/replaceString/replaceStringTool.spec.tsx
new file mode 100644
index 00000000000..49dc18021e1
--- /dev/null
+++ b/extensions/copilot/src/extension/tools/test/node/replaceString/replaceStringTool.spec.tsx
@@ -0,0 +1,93 @@
+/*---------------------------------------------------------------------------------------------
+ * Copyright (c) Microsoft Corporation. All rights reserved.
+ * Licensed under the MIT License. See License.txt in the project root for license information.
+ *--------------------------------------------------------------------------------------------*/
+
+import { readFileSync } from 'fs';
+import { join } from 'path';
+import { beforeEach, expect, it, suite } from 'vitest';
+import { ITestingServicesAccessor } from '../../../../../platform/test/node/services';
+import { TestWorkspaceService } from '../../../../../platform/test/node/testWorkspaceService';
+import { IWorkspaceService } from '../../../../../platform/workspace/common/workspaceService';
+import { ChatResponseStreamImpl } from '../../../../../util/common/chatResponseStreamImpl';
+import { createTextDocumentData } from '../../../../../util/common/test/shims/textDocument';
+import { CancellationToken } from '../../../../../util/vs/base/common/cancellation';
+import { assertType } from '../../../../../util/vs/base/common/types';
+import { URI } from '../../../../../util/vs/base/common/uri';
+import { SyncDescriptor } from '../../../../../util/vs/platform/instantiation/common/descriptors';
+import { IInstantiationService } from '../../../../../util/vs/platform/instantiation/common/instantiation';
+import { ChatResponseTextEditPart } from '../../../../../vscodeTypes';
+import { ChatVariablesCollection } from '../../../../prompt/common/chatVariablesCollection';
+import { WorkingCopyOriginalDocument } from '../../../../prompts/node/inline/workingCopies';
+import { createExtensionUnitTestingServices } from '../../../../test/node/services';
+import { IReplaceStringToolParams, ReplaceStringTool } from '../../../node/replaceStringTool';
+
+
+suite('ReplaceString Tool', () => {
+
+ let accessor: ITestingServicesAccessor;
+
+ const path = join(__dirname, 'fixtures/math.js.txt');
+ const fileTsUri = URI.file(path);
+
+ beforeEach(function () {
+ const services = createExtensionUnitTestingServices();
+
+ const content = String(readFileSync(path));
+
+ const testDoc = createTextDocumentData(fileTsUri, content, 'ts').document;
+ services.define(IWorkspaceService, new SyncDescriptor(
+ TestWorkspaceService, [[fileTsUri], [testDoc]]
+ ));
+
+ accessor = services.createTestingAccessor();
+ });
+
+ it('whitespace change everywhere', async () => {
+
+ const input: IReplaceStringToolParams = JSON.parse(`{
+ "filePath": "${path.replaceAll('\\', '\\\\')}",
+ "oldString": "export function div(a, b) {\\n // console.log fff fff\\n return a / b;\\n}",
+ "newString": "export function div(A, b) {\\n // console.log fff fff\\n return A / b;\\n}"
+}`);
+
+ const tool = accessor.get(IInstantiationService).createInstance(ReplaceStringTool);
+
+ expect(tool).toBeDefined();
+
+ const document = accessor.get(IWorkspaceService).textDocuments.find(doc => doc.uri.toString() === fileTsUri.toString());
+ assertType(document);
+
+ const workingCopyDocument = new WorkingCopyOriginalDocument(document.getText());
+
+ expect(document.getText().includes(input.oldString)).toBe(false); // TAB vs SPACES
+
+ let seenEdits = 0;
+
+ const stream = new ChatResponseStreamImpl((part) => {
+
+ if (part instanceof ChatResponseTextEditPart) {
+ const offsetEdits = workingCopyDocument.transformer.toOffsetEdit(part.edits);
+
+ if (!workingCopyDocument.isNoop(offsetEdits)) {
+ seenEdits++;
+ workingCopyDocument.applyOffsetEdits(offsetEdits);
+ }
+ }
+
+ }, () => { }, () => { });
+
+ const input2 = await tool.resolveInput(input, {
+ history: [],
+ stream,
+ query: 'change a to A',
+ chatVariables: new ChatVariablesCollection([]),
+ });
+
+ await tool.invoke({ input: input2, toolInvocationToken: undefined }, CancellationToken.None);
+
+ expect(seenEdits).toBe(1);
+ expect(workingCopyDocument.text).toMatchFileSnapshot('fixtures/math.js.txt.expected');
+
+ });
+});