diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index 68fb63a6913..80c3eb36f76 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -995,6 +995,15 @@ configurationRegistry.registerConfiguration({ disallowConfigurationDefault: true, tags: ['prompts', 'reusable prompts', 'prompt snippets', 'instructions'] }, + [PromptsConfig.SEARCH_ROOT_REPO_CUSTOMIZATIONS]: { + type: 'boolean', + title: nls.localize('chat.searchRootRepositoryCustomizations.title', "Search Root Repository Customizations",), + markdownDescription: nls.localize('chat.searchRootRepositoryCustomizations.description', "Controls whether configuration files should be searched in parent folders of the workspace folder if the parent folder are repositories.",), + default: false, + restricted: true, + disallowConfigurationDefault: true, + tags: ['prompts', 'reusable prompts', 'prompt snippets', 'instructions'] + }, [PromptsConfig.SKILLS_LOCATION_KEY]: { type: 'object', title: nls.localize('chat.agentSkillsLocations.title', "Agent Skills Locations",), diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/config/config.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/config/config.ts index 4c7497b9f7d..b59bd97c2af 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/config/config.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/config/config.ts @@ -135,6 +135,11 @@ export namespace PromptsConfig { */ export const INCLUDE_REFERENCED_INSTRUCTIONS = 'chat.includeReferencedInstructions'; + /** + * Search for configuration files in parent folders of the workspace folder + */ + export const SEARCH_ROOT_REPO_CUSTOMIZATIONS = 'chat.searchRootRepositoryCustomizations'; + /** * Get value of the `reusable prompt locations` configuration setting. * @see {@link PROMPT_LOCATIONS_CONFIG_KEY}, {@link INSTRUCTIONS_LOCATIONS_CONFIG_KEY}, {@link MODE_LOCATIONS_CONFIG_KEY}, {@link SKILLS_LOCATION_KEY}. diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts index 5c4a0e26a91..9f03e7f01d2 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/config/promptFileLocations.ts @@ -60,6 +60,10 @@ export const CLAUDE_CONFIG_FOLDER = '.claude'; */ export const COPILOT_CUSTOM_INSTRUCTIONS_FILENAME = 'copilot-instructions.md'; +/** + * GitHub configuration folder name. + */ +export const GITHUB_CONFIG_FOLDER = '.github'; /** * Default reusable prompt files source folder. diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts index 73118655901..3dedd8c1382 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/service/promptsServiceImpl.ts @@ -10,7 +10,7 @@ import { parse as parseJSONC } from '../../../../../../base/common/json.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../../../base/common/lifecycle.js'; import { autorun, IReader } from '../../../../../../base/common/observable.js'; import { ResourceMap, ResourceSet } from '../../../../../../base/common/map.js'; -import { basename, dirname, isEqual, joinPath } from '../../../../../../base/common/resources.js'; +import { basename, dirname, isEqual } from '../../../../../../base/common/resources.js'; import { URI } from '../../../../../../base/common/uri.js'; import { OffsetRange } from '../../../../../../editor/common/core/ranges/offsetRange.js'; import { type ITextModel } from '../../../../../../editor/common/model.js'; @@ -29,9 +29,12 @@ import { ITelemetryService } from '../../../../../../platform/telemetry/common/t import { IUserDataProfileService } from '../../../../../services/userDataProfile/common/userDataProfile.js'; import { IVariableReference } from '../../chatModes.js'; import { PromptsConfig } from '../config/config.js'; -import { AGENT_MD_FILENAME, CLAUDE_CONFIG_FOLDER, CLAUDE_LOCAL_MD_FILENAME, CLAUDE_MD_FILENAME, getCleanPromptName, IResolvedPromptFile, IResolvedPromptSourceFolder, PromptFileSource } from '../config/promptFileLocations.js'; +import { AGENT_MD_FILENAME, CLAUDE_CONFIG_FOLDER, CLAUDE_LOCAL_MD_FILENAME, CLAUDE_MD_FILENAME, COPILOT_CUSTOM_INSTRUCTIONS_FILENAME, getCleanPromptName, GITHUB_CONFIG_FOLDER, IResolvedPromptFile, IResolvedPromptSourceFolder, PromptFileSource } from '../config/promptFileLocations.js'; import { PROMPT_LANGUAGE_ID, PromptsType, Target, getPromptsTypeForLanguageId } from '../promptTypes.js'; -import { PromptFilesLocator } from '../utils/promptFilesLocator.js'; +import { + IWorkspaceInstructionFile, + PromptFilesLocator +} from '../utils/promptFilesLocator.js'; import { PromptFileParser, ParsedPromptFile, PromptHeaderAttributes } from '../promptFileParser.js'; import { IAgentInstructions, type IAgentSource, IChatPromptSlashCommand, IConfiguredHooksInfo, ICustomAgent, IExtensionPromptPath, ILocalPromptPath, IPluginPromptPath, IPromptPath, IPromptsService, IAgentSkill, IUserPromptPath, PromptsStorage, ExtensionAgentSourceType, CUSTOM_AGENT_PROVIDER_ACTIVATION_EVENT, INSTRUCTIONS_PROVIDER_ACTIVATION_EVENT, IPromptFileContext, IPromptFileResource, PROMPT_FILE_PROVIDER_ACTIVATION_EVENT, SKILL_PROVIDER_ACTIVATION_EVENT, IPromptDiscoveryInfo, IPromptFileDiscoveryResult, IPromptSourceFolderResult, ICustomAgentVisibility, IResolvedAgentFile, AgentFileType, Logger, IPromptDiscoveryLogEntry } from './promptsService.js'; import { Delayer } from '../../../../../../base/common/async.js'; @@ -899,49 +902,42 @@ export class PromptsService extends Disposable implements IPromptsService { return []; } - public async listAgentMDs(token: CancellationToken, logger: Logger | undefined): Promise { + public async listAgentInstructions(token: CancellationToken, logger: Logger | undefined): Promise { + const resolvedAgentFiles: IResolvedAgentFile[] = []; + const promises: Promise[] = []; + + const includeParents = this.configurationService.getValue(PromptsConfig.SEARCH_ROOT_REPO_CUSTOMIZATIONS) === true; + const rootFolders = await this.fileLocator.getWorkspaceFolderRoots(includeParents); + + const rootFiles: IWorkspaceInstructionFile[] = []; const useAgentMD = this.configurationService.getValue(PromptsConfig.USE_AGENT_MD); if (!useAgentMD) { logger?.logInfo('Agent MD files are disabled via configuration.'); - return []; + } else { + rootFiles.push({ fileName: AGENT_MD_FILENAME, type: AgentFileType.agentsMd }); } - return await this.fileLocator.findFilesInWorkspaceRoots(AGENT_MD_FILENAME, undefined, AgentFileType.agentsMd, token); - } - - public async listClaudeMDs(token: CancellationToken, logger: Logger | undefined): Promise { - // see https://code.claude.com/docs/en/memory const useClaudeMD = this.configurationService.getValue(PromptsConfig.USE_CLAUDE_MD); if (!useClaudeMD) { logger?.logInfo('Claude MD files are disabled via configuration.'); - return []; - } - const results: IResolvedAgentFile[] = []; - const userHome = await this.pathService.userHome(); - const userClaudeFolder = joinPath(userHome, CLAUDE_CONFIG_FOLDER); - await Promise.all([ - this.fileLocator.findFilesInWorkspaceRoots(CLAUDE_MD_FILENAME, undefined, AgentFileType.claudeMd, token, results), // in workspace roots - this.fileLocator.findFilesInWorkspaceRoots(CLAUDE_LOCAL_MD_FILENAME, undefined, AgentFileType.claudeMd, token, results), // CLAUDE.local in workspace roots - this.fileLocator.findFilesInWorkspaceRoots(CLAUDE_MD_FILENAME, CLAUDE_CONFIG_FOLDER, AgentFileType.claudeMd, token, results), // in workspace/.claude folders - this.fileLocator.findFilesInRoots([userClaudeFolder], CLAUDE_MD_FILENAME, AgentFileType.claudeMd, token, results) // in ~/.claude folder - ]); - return results.sort((a, b) => a.uri.toString().localeCompare(b.uri.toString())); - } + } else { + const claudeMdFile = { fileName: CLAUDE_MD_FILENAME, type: AgentFileType.claudeMd }; + rootFiles.push(claudeMdFile); // CLAUDE.md in workspace root + rootFiles.push({ fileName: CLAUDE_LOCAL_MD_FILENAME, type: AgentFileType.claudeMd }); // CLAUDE.local.md in workspace root - public async listCopilotInstructionsMDs(token: CancellationToken, logger: Logger | undefined): Promise { + promises.push(this.fileLocator.findFilesInRoots(rootFolders, CLAUDE_CONFIG_FOLDER, [claudeMdFile], token, resolvedAgentFiles)); // CLAUDE.md in .claude folder under workspace root + promises.push(this.fileLocator.findFilesInRoots([await this.pathService.userHome()], CLAUDE_CONFIG_FOLDER, [claudeMdFile], token, resolvedAgentFiles)); // CLAUDE.md in in ~/.claude folder + } const useCopilotInstructionsFiles = this.configurationService.getValue(PromptsConfig.USE_COPILOT_INSTRUCTION_FILES); if (!useCopilotInstructionsFiles) { logger?.logInfo('Copilot instructions files are disabled via configuration.'); - return []; + } else { + const githubConfigFiles = [{ fileName: COPILOT_CUSTOM_INSTRUCTIONS_FILENAME, type: AgentFileType.copilotInstructionsMd }]; + promises.push(this.fileLocator.findFilesInRoots(rootFolders, GITHUB_CONFIG_FOLDER, githubConfigFiles, token, resolvedAgentFiles)); } - return await this.fileLocator.findCopilotInstructionsMDsInWorkspace(token); - } - public async listAgentInstructions(token: CancellationToken, logger: Logger | undefined): Promise { - const [agentMDs, claudeMDs, copilotInstructionsMDs] = await Promise.all([ - this.listAgentMDs(token, logger), - this.listClaudeMDs(token, logger), - this.listCopilotInstructionsMDs(token, logger) - ]); + promises.push(this.fileLocator.findFilesInRoots(rootFolders, undefined, rootFiles, token, resolvedAgentFiles)); + + await Promise.all(promises); if (token.isCancellationRequested) { return []; } @@ -958,9 +954,7 @@ export class PromptsService extends Disposable implements IPromptsService { } return true; }; - agentMDs.forEach(add); - claudeMDs.forEach(add); - copilotInstructionsMDs.forEach(add); + resolvedAgentFiles.forEach(add); for (const symlink of symlinks) { if (seenFileURI.has(symlink.realPath)) { logger?.logInfo(`Skipping symlinked agent instructions file ${symlink.uri} as target already included: ${symlink.realPath}`); @@ -969,7 +963,7 @@ export class PromptsService extends Disposable implements IPromptsService { seenFileURI.add(symlink.realPath); } } - return result; + return result.sort((a, b) => a.uri.toString().localeCompare(b.uri.toString())); } public getAgentFileURIFromModeFile(oldURI: URI): URI | undefined { diff --git a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts index c0675b37659..deaaabbcc3b 100644 --- a/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts +++ b/src/vs/workbench/contrib/chat/common/promptSyntax/utils/promptFilesLocator.ts @@ -12,7 +12,7 @@ import { getPromptFileLocationsConfigKey, isTildePath, PromptsConfig } from '../ import { basename, dirname, isEqual, isEqualOrParent, joinPath } from '../../../../../../base/common/resources.js'; import { IWorkspaceContextService, IWorkspaceFolder } from '../../../../../../platform/workspace/common/workspace.js'; import { IConfigurationService } from '../../../../../../platform/configuration/common/configuration.js'; -import { COPILOT_CUSTOM_INSTRUCTIONS_FILENAME, AGENTS_SOURCE_FOLDER, getPromptFileExtension, getPromptFileType, LEGACY_MODE_FILE_EXTENSION, getCleanPromptName, AGENT_FILE_EXTENSION, getPromptFileDefaultLocations, SKILL_FILENAME, IPromptSourceFolder, IResolvedPromptFile, IResolvedPromptSourceFolder, PromptFileSource } from '../config/promptFileLocations.js'; +import { AGENTS_SOURCE_FOLDER, getPromptFileExtension, getPromptFileType, LEGACY_MODE_FILE_EXTENSION, getCleanPromptName, AGENT_FILE_EXTENSION, getPromptFileDefaultLocations, SKILL_FILENAME, IPromptSourceFolder, IResolvedPromptFile, IResolvedPromptSourceFolder, PromptFileSource } from '../config/promptFileLocations.js'; import { PromptsType } from '../promptTypes.js'; import { IWorkbenchEnvironmentService } from '../../../../../services/environment/common/environmentService.js'; import { Schemas } from '../../../../../../base/common/network.js'; @@ -25,12 +25,18 @@ import { Emitter, Event } from '../../../../../../base/common/event.js'; import { DisposableStore } from '../../../../../../base/common/lifecycle.js'; import { ILogService } from '../../../../../../platform/log/common/log.js'; import { IPathService } from '../../../../../services/path/common/pathService.js'; +import { equalsIgnoreCase } from '../../../../../../base/common/strings.js'; /** * Maximum recursion depth when traversing subdirectories for instruction files. */ const MAX_INSTRUCTIONS_RECURSION_DEPTH = 5; +export interface IWorkspaceInstructionFile { + readonly fileName: string; + readonly type: AgentFileType; +} + /** * Utility class to locate prompt files. */ @@ -584,24 +590,6 @@ export class PromptFilesLocator { return []; } - public async findCopilotInstructionsMDsInWorkspace(token: CancellationToken): Promise { - const result: IResolvedAgentFile[] = []; - const folders = this.getWorkspaceFolders(); - for (const folder of folders) { - const file = joinPath(folder.uri, `.github/` + COPILOT_CUSTOM_INSTRUCTIONS_FILENAME); - try { - const stat = await this.fileService.stat(file); - if (stat.isFile) { - const realPath = stat.isSymbolicLink ? await this.fileService.realpath(file) : undefined; - result.push({ uri: file, realPath, type: AgentFileType.copilotInstructionsMd }); - } - } catch (error) { - this.logService.trace(`[PromptFilesLocator] Skipping copilot-instructions.md at ${file.toString()}: ${error}`); - } - } - return result; - } - /** * Gets list of `AGENTS.md` files anywhere in the workspace. */ @@ -683,29 +671,40 @@ export class PromptFilesLocator { return result; } - /** - * Gets list of files at the root workspace folder(s). - */ - public async findFilesInWorkspaceRoots(fileName: string, folder: string | undefined, type: AgentFileType, token: CancellationToken, result: IResolvedAgentFile[] = []): Promise { - const folders = this.getWorkspaceFolders(); - if (folder) { - return this.findFilesInRoots(folders.map(f => joinPath(f.uri, folder)), fileName, type, token, result); + public async getWorkspaceFolderRoots(includeParents: boolean): Promise { + const workspaceFolders = this.getWorkspaceFolders(); + if (includeParents) { + const folders: URI[] = []; + const userHome = await this.pathService.userHome(); + for (const workspaceFolder of workspaceFolders) { + folders.push(workspaceFolder.uri); + let parent = dirname(workspaceFolder.uri); + while (parent.path !== '/' && !isEqual(userHome, parent) && !folders.some(f => isEqual(f, parent))) { + folders.push(parent); + parent = dirname(parent); + } + } + return folders; } - return this.findFilesInRoots(folders.map(f => f.uri), fileName, type, token, result); + return workspaceFolders.map(f => f.uri); } - public async findFilesInRoots(roots: URI[], fileName: string, type: AgentFileType, token: CancellationToken, result: IResolvedAgentFile[] = []): Promise { - const fileNameLower = fileName.toLowerCase(); - const resolvedRoots = await this.fileService.resolveAll(roots.map(uri => ({ resource: uri }))); + public async findFilesInRoots(roots: URI[], folder: string | undefined, paths: IWorkspaceInstructionFile[], token: CancellationToken, result: IResolvedAgentFile[] = []): Promise { + const toResolve = roots.map(root => ({ resource: folder !== undefined ? joinPath(root, folder) : root })); + const resolvedRoots = await this.fileService.resolveAll(toResolve); if (token.isCancellationRequested) { return result; } for (const root of resolvedRoots) { if (root.success && root.stat?.children) { - const file = root.stat.children.find(c => c.isFile && c.name.toLowerCase() === fileNameLower); - if (file) { - const realPath = file.isSymbolicLink ? await this.fileService.realpath(file.resource) : undefined; - result.push({ uri: file.resource, realPath, type }); + for (const child of root.stat.children) { + if (child.isFile) { + const matchingPath = paths.find(p => equalsIgnoreCase(p.fileName, child.name)); + if (matchingPath) { + const realPath = child.isSymbolicLink ? await this.fileService.realpath(child.resource) : undefined; + result.push({ uri: child.resource, realPath, type: matchingPath.type }); + } + } } } } diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts index 899d04339a4..88ace7b82e6 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/computeAutomaticInstructions.test.ts @@ -76,6 +76,7 @@ suite('ComputeAutomaticInstructions', () => { testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_SKILLS, true); testConfigService.setUserConfiguration(PromptsConfig.INCLUDE_APPLYING_INSTRUCTIONS, true); testConfigService.setUserConfiguration(PromptsConfig.INCLUDE_REFERENCED_INSTRUCTIONS, true); + testConfigService.setUserConfiguration(PromptsConfig.SEARCH_ROOT_REPO_CUSTOMIZATIONS, false); testConfigService.setUserConfiguration(PromptsConfig.INSTRUCTIONS_LOCATION_KEY, { [INSTRUCTIONS_DEFAULT_SOURCE_FOLDER]: true, [CLAUDE_RULES_SOURCE_FOLDER]: true }); testConfigService.setUserConfiguration(PromptsConfig.PROMPT_LOCATIONS_KEY, { [PROMPT_DEFAULT_SOURCE_FOLDER]: true }); testConfigService.setUserConfiguration(PromptsConfig.MODE_LOCATION_KEY, { [LEGACY_MODE_DEFAULT_SOURCE_FOLDER]: true }); @@ -1628,6 +1629,114 @@ suite('ComputeAutomaticInstructions', () => { assert.ok(!paths.includes(`${rootFolder}/.claude/CLAUDE.md`), 'Should not include .claude/CLAUDE.md when disabled'); }); + test('should collect parent folder CLAUDE configurations when includeWorkspaceFolderParents is enabled', async () => { + const parentFolderName = 'collect-claude-parent-test'; + const parentFolder = `/${parentFolderName}`; + const rootFolder = `${parentFolder}/repo`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${parentFolder}/CLAUDE.md`, + contents: ['Parent Claude guidelines'], + }, + { + path: `${parentFolder}/.claude/CLAUDE.md`, + contents: ['Parent .claude Claude guidelines'], + }, + { + path: `${rootFolder}/src/file.ts`, + contents: ['console.log("test");'], + }, + ]); + + testConfigService.setUserConfiguration(PromptsConfig.USE_CLAUDE_MD, true); + testConfigService.setUserConfiguration(PromptsConfig.SEARCH_ROOT_REPO_CUSTOMIZATIONS, false); + + const disabledParentContextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatModeKind.Agent, undefined, undefined, undefined); + const disabledParentVariables = new ChatRequestVariableSet(); + disabledParentVariables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file.ts'))); + + await disabledParentContextComputer.collect(disabledParentVariables, CancellationToken.None); + + let paths = disabledParentVariables.asArray() + .filter(v => isPromptFileVariableEntry(v)) + .map(i => isPromptFileVariableEntry(i) ? i.value.path : undefined); + assert.ok(!paths.includes(`${parentFolder}/CLAUDE.md`), 'Should not include parent CLAUDE.md when parent search is disabled'); + assert.ok(!paths.includes(`${parentFolder}/.claude/CLAUDE.md`), 'Should not include parent .claude/CLAUDE.md when parent search is disabled'); + + // Parent folder settings should allow finding both root and .claude CLAUDE files above the workspace folder. + testConfigService.setUserConfiguration(PromptsConfig.SEARCH_ROOT_REPO_CUSTOMIZATIONS, true); + + const enabledParentContextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatModeKind.Agent, undefined, undefined, undefined); + const enabledParentVariables = new ChatRequestVariableSet(); + enabledParentVariables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file.ts'))); + + await enabledParentContextComputer.collect(enabledParentVariables, CancellationToken.None); + + paths = enabledParentVariables.asArray() + .filter(v => isPromptFileVariableEntry(v)) + .map(i => isPromptFileVariableEntry(i) ? i.value.path : undefined); + assert.ok(paths.includes(`${parentFolder}/CLAUDE.md`), 'Should include parent CLAUDE.md when parent search is enabled'); + assert.ok(paths.includes(`${parentFolder}/.claude/CLAUDE.md`), 'Should include parent .claude/CLAUDE.md when parent search is enabled'); + }); + + test('should collect parent folder copilot-instructions.md and AGENTS.md when includeWorkspaceFolderParents is enabled', async () => { + const parentFolderName = 'collect-agent-parent-test'; + const parentFolder = `/${parentFolderName}`; + const rootFolder = `${parentFolder}/repo`; + const rootFolderUri = URI.file(rootFolder); + + workspaceContextService.setWorkspace(testWorkspace(rootFolderUri)); + + await mockFiles(fileService, [ + { + path: `${parentFolder}/.github/copilot-instructions.md`, + contents: ['Parent copilot instructions'], + }, + { + path: `${parentFolder}/AGENTS.md`, + contents: ['Parent agent guidelines'], + }, + { + path: `${rootFolder}/src/file.ts`, + contents: ['console.log("test");'], + }, + ]); + + testConfigService.setUserConfiguration(PromptsConfig.USE_COPILOT_INSTRUCTION_FILES, true); + testConfigService.setUserConfiguration(PromptsConfig.USE_AGENT_MD, true); + testConfigService.setUserConfiguration(PromptsConfig.SEARCH_ROOT_REPO_CUSTOMIZATIONS, false); + + const disabledParentContextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatModeKind.Agent, undefined, undefined, undefined); + const disabledParentVariables = new ChatRequestVariableSet(); + disabledParentVariables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file.ts'))); + + await disabledParentContextComputer.collect(disabledParentVariables, CancellationToken.None); + + let paths = disabledParentVariables.asArray() + .filter(v => isPromptFileVariableEntry(v)) + .map(i => isPromptFileVariableEntry(i) ? i.value.path : undefined); + assert.ok(!paths.includes(`${parentFolder}/.github/copilot-instructions.md`), 'Should not include parent copilot-instructions.md when parent search is disabled'); + assert.ok(!paths.includes(`${parentFolder}/AGENTS.md`), 'Should not include parent AGENTS.md when parent search is disabled'); + + testConfigService.setUserConfiguration(PromptsConfig.SEARCH_ROOT_REPO_CUSTOMIZATIONS, true); + + const enabledParentContextComputer = instaService.createInstance(ComputeAutomaticInstructions, ChatModeKind.Agent, undefined, undefined, undefined); + const enabledParentVariables = new ChatRequestVariableSet(); + enabledParentVariables.add(toFileVariableEntry(URI.joinPath(rootFolderUri, 'src/file.ts'))); + + await enabledParentContextComputer.collect(enabledParentVariables, CancellationToken.None); + + paths = enabledParentVariables.asArray() + .filter(v => isPromptFileVariableEntry(v)) + .map(i => isPromptFileVariableEntry(i) ? i.value.path : undefined); + assert.ok(paths.includes(`${parentFolder}/.github/copilot-instructions.md`), 'Should include parent copilot-instructions.md when parent search is enabled'); + assert.ok(paths.includes(`${parentFolder}/AGENTS.md`), 'Should include parent AGENTS.md when parent search is enabled'); + }); + test('should collect ~/.claude/CLAUDE.md when enabled', async () => { const rootFolderName = 'collect-claude-home-test'; const rootFolder = `/${rootFolderName}`; diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts index 20cecffea32..5adbbb760ed 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/service/promptsService.test.ts @@ -80,6 +80,7 @@ suite('PromptsService', () => { testConfigService.setUserConfiguration(PromptsConfig.USE_NESTED_AGENT_MD, false); testConfigService.setUserConfiguration(PromptsConfig.INCLUDE_REFERENCED_INSTRUCTIONS, true); testConfigService.setUserConfiguration(PromptsConfig.INCLUDE_APPLYING_INSTRUCTIONS, true); + testConfigService.setUserConfiguration(PromptsConfig.SEARCH_ROOT_REPO_CUSTOMIZATIONS, false); testConfigService.setUserConfiguration(PromptsConfig.INSTRUCTIONS_LOCATION_KEY, { [INSTRUCTIONS_DEFAULT_SOURCE_FOLDER]: true }); testConfigService.setUserConfiguration(PromptsConfig.PROMPT_LOCATIONS_KEY, { [PROMPT_DEFAULT_SOURCE_FOLDER]: true }); testConfigService.setUserConfiguration(PromptsConfig.MODE_LOCATION_KEY, { [LEGACY_MODE_DEFAULT_SOURCE_FOLDER]: true }); diff --git a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts index 2dde1dfb0a2..4d030f7c342 100644 --- a/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts +++ b/src/vs/workbench/contrib/chat/test/common/promptSyntax/utils/promptFilesLocator.test.ts @@ -101,6 +101,7 @@ suite('PromptFilesLocator', () => { 'explorer.excludeGitIgnore': false, 'files.exclude': {}, 'search.exclude': {}, + [PromptsConfig.SEARCH_ROOT_REPO_CUSTOMIZATIONS]: false, [PromptsConfig.PROMPT_LOCATIONS_KEY]: configValue, [PromptsConfig.INSTRUCTIONS_LOCATION_KEY]: configValue, [PromptsConfig.MODE_LOCATION_KEY]: configValue,