Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

10 changes: 8 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,8 @@
"onUri",
"onFileSystem:ccreq",
"onFileSystem:ccsettings",
"onCustomAgentsProvider"
"onCustomAgentsProvider",
"onInstructionsProvider"
],
"main": "./dist/extension",
"l10n": "./l10n",
Expand Down Expand Up @@ -2502,6 +2503,11 @@
"default": true,
"description": "%github.copilot.config.customAgents.showOrganizationAndEnterpriseAgents%"
},
"github.copilot.chat.customInstructions.useOrganizationInstructions": {
"type": "boolean",
"default": true,
"description": "%github.copilot.config.customInstructions.useOrganizationInstructions%"
},
"github.copilot.chat.agent.currentEditorContext.enabled": {
"type": "boolean",
"default": true,
Expand Down Expand Up @@ -5227,7 +5233,7 @@
"@humanwhocodes/gitignore-to-minimatch": "1.0.2",
"@microsoft/tiktokenizer": "^1.0.10",
"@sinclair/typebox": "^0.34.41",
"@vscode/copilot-api": "^0.2.5",
"@vscode/copilot-api": "^0.2.6",
"@vscode/extension-telemetry": "^1.2.0",
"@vscode/l10n": "^0.0.18",
"@vscode/prompt-tsx": "^0.4.0-alpha.5",
Expand Down
1 change: 1 addition & 0 deletions package.nls.json
Original file line number Diff line number Diff line change
Expand Up @@ -346,6 +346,7 @@
"github.copilot.config.agent.currentEditorContext.enabled": "When enabled, Copilot will include the name of the current active editor in the context for agent mode.",
"github.copilot.config.customInstructionsInSystemMessage": "When enabled, custom instructions and mode instructions will be appended to the system message instead of a user message.",
"github.copilot.config.customAgents.showOrganizationAndEnterpriseAgents": "Enable custom agents from GitHub Enterprise and Organizations. When disabled, custom agents from your organization or enterprise will not be available in Copilot.",
"github.copilot.config.customInstructions.useOrganizationInstructions": "Enable custom instructions from GitHub Organizations. When disabled, custom instructions from your organization will not be available in Copilot.",
"copilot.toolSet.editing.description": "Edit files in your workspace",
"copilot.toolSet.read.description": "Read files in your workspace",
"copilot.toolSet.search.description": "Search files in your workspace",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { ConfigKey, IConfigurationService } from '../../../platform/configuration/common/configurationService';
import { Disposable } from '../../../util/vs/base/common/lifecycle';
import { IInstantiationService } from '../../../util/vs/platform/instantiation/common/instantiation';
import { IExtensionContribution } from '../../common/contributions';
import { OrganizationInstructionsProvider } from './organizationInstructionsProvider';

export class OrganizationInstructionsContribution extends Disposable implements IExtensionContribution {
readonly id = 'OrganizationInstructions';

constructor(
@IInstantiationService instantiationService: IInstantiationService,
@IConfigurationService configurationService: IConfigurationService,
) {
super();

if ('registerInstructionsProvider' in vscode.chat) {
// Only register the provider if the setting is enabled
if (configurationService.getConfig(ConfigKey.UseOrganizationInstructions)) {
const provider = instantiationService.createInstance(OrganizationInstructionsProvider);
this._register(vscode.chat.registerInstructionsProvider(provider));
}
}
}
}
179 changes: 179 additions & 0 deletions src/extension/agents/vscode-node/organizationInstructionsProvider.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,179 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { IVSCodeExtensionContext } from '../../../platform/extContext/common/extensionContext';
import { IFileSystemService } from '../../../platform/filesystem/common/fileSystemService';
import { FileType } from '../../../platform/filesystem/common/fileTypes';
import { IGitService } from '../../../platform/git/common/gitService';
import { IOctoKitService } from '../../../platform/github/common/githubService';
import { ILogService } from '../../../platform/log/common/logService';
import { Disposable } from '../../../util/vs/base/common/lifecycle';
import { getRepoId } from '../../chatSessions/vscode/copilotCodingAgentUtils';

const InstructionFileExtension = '.instruction.md';

export class OrganizationInstructionsProvider extends Disposable implements vscode.InstructionsProvider {

private readonly _onDidChangeInstructions = this._register(new vscode.EventEmitter<void>());
readonly onDidChangeInstructions = this._onDidChangeInstructions.event;

private isFetching = false;

constructor(
@IOctoKitService private readonly octoKitService: IOctoKitService,
@ILogService private readonly logService: ILogService,
@IGitService private readonly gitService: IGitService,
@IVSCodeExtensionContext readonly extensionContext: IVSCodeExtensionContext,
@IFileSystemService private readonly fileSystem: IFileSystemService,
) {
super();
}

private getCacheDir(): vscode.Uri | undefined {
if (!this.extensionContext.storageUri) {
return;
}
return vscode.Uri.joinPath(this.extensionContext.storageUri, 'githubInstructionsCache');
}

private getCacheFilename(orgLogin: string): string {
return orgLogin + InstructionFileExtension;
}

async provideInstructions(
options: vscode.InstructionQueryOptions,
_token: vscode.CancellationToken
): Promise<vscode.CustomAgentResource[]> {
try {
// Get repository information from the active git repository
const repoId = await getRepoId(this.gitService);
if (!repoId) {
this.logService.trace('[OrganizationInstructionsProvider] No active repository found');
return [];
}

const orgLogin = repoId.org;

// Read from cache first
const cachedInstructions = await this.readFromCache(orgLogin);

// Trigger async fetch to update cache
this.fetchAndUpdateCache(orgLogin, options).catch(error => {
this.logService.error(`[OrganizationInstructionsProvider] Error in background fetch: ${error}`);
});

return cachedInstructions;
} catch (error) {
this.logService.error(`[OrganizationInstructionsProvider] Error in provideInstructions: ${error}`);
return [];
}
}

private async readFromCache(
orgLogin: string,
): Promise<vscode.CustomAgentResource[]> {
try {
const cacheDir = this.getCacheDir();
if (!cacheDir) {
this.logService.trace('[OrganizationInstructionsProvider] No workspace open, cannot use cache');
return [];
}

const cacheContents = await this.readCacheContents(orgLogin, cacheDir);
if (cacheContents === undefined) {
this.logService.trace(`[OrganizationInstructionsProvider] No cache found for org ${orgLogin}`);
return [];
}

const instructions: vscode.CustomAgentResource[] = [];
const fileName = this.getCacheFilename(orgLogin);
const fileUri = vscode.Uri.joinPath(cacheDir, fileName);
instructions.push({
name: orgLogin,
description: '',
uri: fileUri,
});

this.logService.trace(`[OrganizationInstructionsProvider] Loaded ${instructions.length} instructions from cache for org ${orgLogin}`);
return instructions;
} catch (error) {
this.logService.error(`[OrganizationInstructionsProvider] Error reading from cache: ${error}`);
return [];
}
}

private async fetchAndUpdateCache(
orgLogin: string,
options: vscode.InstructionQueryOptions
): Promise<void> {
// Prevent concurrent fetches
if (this.isFetching) {
this.logService.trace('[OrganizationInstructionsProvider] Fetch already in progress, skipping');
return;
}

this.isFetching = true;
try {
this.logService.trace(`[OrganizationInstructionsProvider] Fetching custom instructions for org ${orgLogin}`);

const instructions = await this.octoKitService.getOrgCustomInstructions(orgLogin);
const cacheDir = this.getCacheDir();
if (!cacheDir) {
this.logService.trace('[OrganizationInstructionsProvider] No workspace open, cannot use cache');
return;
}

if (!instructions) {
this.logService.trace(`[OrganizationInstructionsProvider] No custom instructions found for org ${orgLogin}`);
return;
}

// Ensure cache directory exists
try {
await this.fileSystem.stat(cacheDir);
} catch (error) {
// Directory doesn't exist, create it
await this.fileSystem.createDirectory(cacheDir);
}

const existingInstructions = await this.readCacheContents(orgLogin, cacheDir);
const hasChanges = instructions !== existingInstructions;

if (!hasChanges) {
this.logService.trace(`[OrganizationInstructionsProvider] No changes detected in cache for org ${orgLogin}`);
return;
}

const fileName = this.getCacheFilename(orgLogin);
const fileUri = vscode.Uri.joinPath(cacheDir, fileName);
await this.fileSystem.writeFile(fileUri, new TextEncoder().encode(instructions));

this.logService.trace(`[OrganizationInstructionsProvider] Updated cache with instructions for org ${orgLogin}`);

// Fire event to notify consumers that instructions have changed
this._onDidChangeInstructions.fire();
} finally {
this.isFetching = false;
}
}

private async readCacheContents(orgLogin: string, cacheDir: vscode.Uri): Promise<string | undefined> {
try {
const files = await this.fileSystem.readDirectory(cacheDir);
for (const [filename, fileType] of files) {
if (fileType === FileType.File && filename === this.getCacheFilename(orgLogin)) {
const fileUri = vscode.Uri.joinPath(cacheDir, filename);
const content = await this.fileSystem.readFile(fileUri);
const text = new TextDecoder().decode(content);
return text;
}
}
} catch {
// Directory might not exist yet or other errors
}
return undefined;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,7 @@ class MockOctoKitService implements IOctoKitService {
getPullRequestFiles = async () => [];
closePullRequest = async () => false;
getFileContent = async () => '';
getOrgCustomInstructions = async () => undefined;

async getCustomAgents(owner: string, repo: string, options?: CustomAgentListOptions): Promise<CustomAgentListItem[]> {
return this.customAgents;
Expand Down
Loading