mirror of
https://github.com/microsoft/vscode.git
synced 2026-04-17 15:24:40 +01:00
find agent instructions in parent folders (#300717)
* find agent instructions in parent folders * update
This commit is contained in:
committed by
GitHub
parent
970c15e9bb
commit
f9b7df95eb
@@ -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",),
|
||||
|
||||
@@ -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}.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<IResolvedAgentFile[]> {
|
||||
public async listAgentInstructions(token: CancellationToken, logger: Logger | undefined): Promise<IResolvedAgentFile[]> {
|
||||
const resolvedAgentFiles: IResolvedAgentFile[] = [];
|
||||
const promises: Promise<IResolvedAgentFile[]>[] = [];
|
||||
|
||||
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<IResolvedAgentFile[]> {
|
||||
// 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<IResolvedAgentFile[]> {
|
||||
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<IResolvedAgentFile[]> {
|
||||
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 {
|
||||
|
||||
@@ -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<IResolvedAgentFile[]> {
|
||||
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<IResolvedAgentFile[]> {
|
||||
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<URI[]> {
|
||||
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<IResolvedAgentFile[]> {
|
||||
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<IResolvedAgentFile[]> {
|
||||
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 });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user