Path: blob/main/extensions/copilot/src/extension/review/node/githubReviewAgent.ts
13399 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { RequestType } from '@vscode/copilot-api';6import * as l10n from '@vscode/l10n';7import * as readline from 'readline';8import { Readable } from 'stream';9import type { Selection, TextDocument, TextEditor } from 'vscode';10import { IAuthenticationService } from '../../../platform/authentication/common/authentication';11import { ConfigKey } from '../../../platform/configuration/common/configurationService';12import { ICustomInstructionsService } from '../../../platform/customInstructions/common/customInstructionsService';13import { TextDocumentSnapshot } from '../../../platform/editing/common/textDocumentSnapshot';14import { ICAPIClientService } from '../../../platform/endpoint/common/capiClient';15import { IDomainService } from '../../../platform/endpoint/common/domainService';16import { IEnvService } from '../../../platform/env/common/envService';17import { IGitExtensionService } from '../../../platform/git/common/gitExtensionService';18import { API, Repository } from '../../../platform/git/vscode/git';19import { IIgnoreService } from '../../../platform/ignore/common/ignoreService';20import { ILogService } from '../../../platform/log/common/logService';21import { IFetcherService, Response } from '../../../platform/networking/common/fetcherService';22import { Progress } from '../../../platform/notification/common/notificationService';23import { ReviewComment, ReviewRequest } from '../../../platform/review/common/reviewService';24import { IWorkspaceService } from '../../../platform/workspace/common/workspaceService';25import { CancellationToken } from '../../../util/vs/base/common/cancellation';26import * as path from '../../../util/vs/base/common/path';27import { generateUuid } from '../../../util/vs/base/common/uuid';28import { MarkdownString, Range, Uri } from '../../../vscodeTypes';29import { FeedbackResult } from '../../prompt/node/feedbackGenerator';303132const testing = false;3334/**35* Represents a file change to be reviewed.36*/37interface FileChange {38repository: Repository | undefined;39uri?: Uri;40relativePath: string;41before: string;42after: string;43selection?: Selection;44document: TextDocument;45}4647/**48* Normalizes a file path to use forward slashes on all platforms.49*/50export function normalizePath(relativePath: string): string {51return process.platform === 'win32' ? relativePath.replace(/\\/g, '/') : relativePath;52}5354/**55* Collects file change data for a selection-based review.56*/57function collectSelectionChanges(58git: API,59editor: TextEditor,60workspaceService: IWorkspaceService61): FileChange[] {62return [{63repository: git.getRepository(editor.document.uri) || undefined,64uri: editor.document.uri,65relativePath: workspaceService.asRelativePath(editor.document.uri),66before: '',67after: editor.document.getText(),68selection: editor.selection,69document: editor.document,70}];71}7273/**74* Collects file change data for diff-based reviews (index, workingTree, or all).75*/76async function collectDiffChanges(77git: API,78group: 'index' | 'workingTree' | 'all',79workspaceService: IWorkspaceService80): Promise<(FileChange | undefined)[]> {81const repositoryChanges = await Promise.all(git.repositories.map(async repository => {82const uris = new Set<Uri>();83if (group === 'all' || group === 'index') {84repository.state.indexChanges.forEach(c => uris.add(c.uri));85}86if (group === 'all' || group === 'workingTree') {87repository.state.workingTreeChanges.forEach(c => uris.add(c.uri));88repository.state.untrackedChanges.forEach(c => uris.add(c.uri));89}90const changes = await Promise.all(Array.from(uris).map(async uri => {91const document = await workspaceService.openTextDocument(uri).then(undefined, () => undefined);92if (!document) {93return undefined; // Deleted files can be skipped.94}95const before = await (group === 'index' || group === 'all' ? repository.show('HEAD', uri.fsPath).catch(() => '') : repository.show('', uri.fsPath).catch(() => ''));96const after = group === 'index' ? await (repository.show('', uri.fsPath).catch(() => '')) : document.getText();97const relativePath = path.relative(repository.rootUri.fsPath, uri.fsPath);98return {99repository,100uri,101relativePath: normalizePath(relativePath),102before,103after,104document,105};106}));107return changes;108}));109return repositoryChanges.flat();110}111112/**113* Collects file change data for patch-based reviews (e.g., PR reviews).114*/115async function collectPatchChanges(116git: API,117group: { repositoryRoot: string; commitMessages: string[]; patches: { patch: string; fileUri: string; previousFileUri?: string }[] },118workspaceService: IWorkspaceService119): Promise<(FileChange | undefined)[]> {120return Promise.all(group.patches.map(async patch => {121const uri = Uri.parse(patch.fileUri);122const document = await workspaceService.openTextDocument(uri).then(undefined, () => undefined);123if (!document) {124return undefined; // Deleted files can be skipped.125}126const after = document.getText();127const before = reversePatch(after, patch.patch);128const relativePath = path.relative(group.repositoryRoot, uri.fsPath);129return {130repository: git.getRepository(Uri.parse(group.repositoryRoot))!,131relativePath: normalizePath(relativePath),132before,133after,134document,135};136}));137}138139/**140* Collects file change data for single-file reviews.141*/142async function collectSingleFileChanges(143git: API,144group: { group: 'index' | 'workingTree'; file: Uri },145workspaceService: IWorkspaceService146): Promise<FileChange[]> {147const { group: g, file } = group;148const repository = git.getRepository(file);149const document = await workspaceService.openTextDocument(file).then(undefined, () => undefined);150if (!repository || !document) {151return [];152}153const before = await (g === 'index' ? repository.show('HEAD', file.fsPath).catch(() => '') : repository.show('', file.fsPath).catch(() => ''));154const after = g === 'index' ? await (repository.show('', file.fsPath).catch(() => '')) : document.getText();155const relativePath = path.relative(repository.rootUri.fsPath, file.fsPath);156return [{157repository,158relativePath: normalizePath(relativePath),159before,160after,161document,162}];163}164165/**166* Collects all file changes based on the review group type.167*/168async function collectChanges(169git: API,170group: 'selection' | 'index' | 'workingTree' | 'all' | { group: 'index' | 'workingTree'; file: Uri } | { repositoryRoot: string; commitMessages: string[]; patches: { patch: string; fileUri: string; previousFileUri?: string }[] },171editor: TextEditor | undefined,172workspaceService: IWorkspaceService173): Promise<FileChange[]> {174if (group === 'selection') {175return collectSelectionChanges(git, editor!, workspaceService);176}177if (typeof group === 'string') {178const changes = await collectDiffChanges(git, group, workspaceService);179return changes.filter((change): change is FileChange => !!change);180}181if ('repositoryRoot' in group) {182const changes = await collectPatchChanges(git, group, workspaceService);183return changes.filter((change): change is FileChange => !!change);184}185return collectSingleFileChanges(git, group, workspaceService);186}187188export async function githubReview(189logService: ILogService,190gitExtensionService: IGitExtensionService,191authService: IAuthenticationService,192capiClientService: ICAPIClientService,193domainService: IDomainService,194fetcherService: IFetcherService,195envService: IEnvService,196ignoreService: IIgnoreService,197workspaceService: IWorkspaceService,198customInstructionsService: ICustomInstructionsService,199group: 'selection' | 'index' | 'workingTree' | 'all' | { group: 'index' | 'workingTree'; file: Uri } | { repositoryRoot: string; commitMessages: string[]; patches: { patch: string; fileUri: string; previousFileUri?: string }[] },200editor: TextEditor | undefined,201progress: Progress<ReviewComment[]>,202cancellationToken: CancellationToken203): Promise<FeedbackResult> {204const git = gitExtensionService.getExtensionApi();205if (!git) {206return { type: 'success', comments: [] };207}208const changes = await collectChanges(git, group, editor, workspaceService);209210if (!changes.length) {211return { type: 'success', comments: [] };212}213214const ignored = await Promise.all(changes.map(i => ignoreService.isCopilotIgnored(i.document.uri)));215const filteredChanges = changes.filter((_, i) => !ignored[i]);216if (filteredChanges.length === 0) {217logService.info('All input documents are ignored. Skipping feedback generation.');218return {219type: 'error',220severity: 'info',221reason: l10n.t('All input documents are ignored by configuration. Check your .copilotignore file.')222};223}224logService.debug(`[github review agent] files: ${filteredChanges.map(change => change.relativePath).join(', ')}`);225226const { requestId, rl } = !testing ? await fetchComments(227logService,228authService,229capiClientService,230fetcherService,231envService,232customInstructionsService,233workspaceService,234group === 'selection' ? 'selection' : 'diff',235filteredChanges[0].repository,236filteredChanges.map(change => ({ path: change.relativePath, content: change.before, languageId: change.document.languageId })),237filteredChanges.map(change => ({ path: change.relativePath, content: change.after, languageId: change.document.languageId, selection: 'selection' in change ? change.selection : undefined })),238cancellationToken,239) : {240requestId: 'test-request-id',241rl: [242'data: ...',243'data: [DONE]',244]245};246if (!rl || cancellationToken.isCancellationRequested) {247return { type: 'cancelled' };248}249250logService.info(`[github review agent] request id: ${requestId}`);251252const request: ReviewRequest = {253source: 'githubReviewAgent',254promptCount: -1,255messageId: requestId || generateUuid(),256inputType: 'change',257inputRanges: [],258};259const references: ResponseReference[] = [];260const comments: ReviewComment[] = [];261for await (const line of rl) {262if (cancellationToken.isCancellationRequested) {263return { type: 'cancelled' };264}265logService.debug(`[github review agent] response line: ${line}`);266const refs = parseLine(line);267references.push(...refs);268for (const ghComment of refs.filter(ref => ref.type === 'github.generated-pull-request-comment')) {269const change = filteredChanges.find(change => change.relativePath === ghComment.data.path);270if (!change) {271continue;272}273const comment = createReviewComment(ghComment, request, change.document, comments.length);274comments.push(comment);275progress.report([comment]);276}277}278const excludedComments = references.filter((ref): ref is ExcludedComment => ref.type === 'github.excluded-pull-request-comment')279.map(ghComment => {280const change = filteredChanges.find(change => change.relativePath === ghComment.data.path);281return { ghComment, change };282}).filter((item): item is { ghComment: ExcludedComment; change: NonNullable<typeof item.change> } => !!item.change)283.map(({ ghComment, change }, i) => createReviewComment(ghComment, request, change.document, comments.length + i));284const unsupportedLanguages = !comments.length ? [...new Set(references.filter((ref): ref is ExcludedFile => ref.type === 'github.excluded-file' && ref.data.reason === 'file_type_not_supported')285.map(ref => ref.data.language))] : [];286return { type: 'success', comments, excludedComments, reason: unsupportedLanguages.length ? l10n.t('Some of the submitted languages are currently not supported: {0}', unsupportedLanguages.join(', ')) : undefined };287}288289/**290* Review files specified as URI pairs (current + base content).291* This is the entry point for the `github.copilot.chat.codeReview.run` command,292* bypassing git-based change collection.293*/294export async function githubReviewFileUris(295logService: ILogService,296authService: IAuthenticationService,297capiClientService: ICAPIClientService,298fetcherService: IFetcherService,299envService: IEnvService,300ignoreService: IIgnoreService,301workspaceService: IWorkspaceService,302customInstructionsService: ICustomInstructionsService,303fileInputs: readonly { readonly currentUri: Uri; readonly baseContent: string }[],304cancellationToken: CancellationToken,305): Promise<FeedbackResult> {306const changes: { readonly relativePath: string; readonly before: string; readonly after: string; readonly document: TextDocument; readonly uri: Uri }[] = [];307for (const input of fileInputs) {308const document = await workspaceService.openTextDocument(input.currentUri);309changes.push({310uri: input.currentUri,311relativePath: normalizePath(workspaceService.asRelativePath(input.currentUri)),312before: input.baseContent,313after: document.getText(),314document,315});316}317318if (!changes.length) {319return { type: 'success', comments: [] };320}321322const ignored = await Promise.all(changes.map(c => ignoreService.isCopilotIgnored(c.uri)));323const filteredChanges = changes.filter((_, i) => !ignored[i]);324if (filteredChanges.length === 0) {325logService.info('All input documents are ignored. Skipping feedback generation.');326return {327type: 'error',328severity: 'info',329reason: l10n.t('All input documents are ignored by configuration. Check your .copilotignore file.')330};331}332logService.debug(`[github review agent] files: ${filteredChanges.map(c => c.relativePath).join(', ')}`);333334const { requestId, rl } = await fetchComments(335logService, authService, capiClientService, fetcherService, envService,336customInstructionsService, workspaceService,337'diff',338undefined,339filteredChanges.map(c => ({ path: c.relativePath, content: c.before, languageId: c.document.languageId })),340filteredChanges.map(c => ({ path: c.relativePath, content: c.after, languageId: c.document.languageId })),341cancellationToken,342);343if (!rl || cancellationToken.isCancellationRequested) {344return { type: 'cancelled' };345}346347logService.info(`[github review agent] request id: ${requestId}`);348349const request: ReviewRequest = {350source: 'githubReviewAgent',351promptCount: -1,352messageId: requestId || generateUuid(),353inputType: 'change',354inputRanges: [],355};356const references: ResponseReference[] = [];357const comments: ReviewComment[] = [];358for await (const line of rl) {359if (cancellationToken.isCancellationRequested) {360return { type: 'cancelled' };361}362logService.debug(`[github review agent] response line: ${line}`);363const refs = parseLine(line);364references.push(...refs);365for (const ghComment of refs.filter(ref => ref.type === 'github.generated-pull-request-comment')) {366const change = filteredChanges.find(c => c.relativePath === ghComment.data.path);367if (!change) {368continue;369}370const comment = createReviewComment(ghComment, request, change.document, comments.length);371comments.push(comment);372}373}374const excludedComments = references.filter((ref): ref is ExcludedComment => ref.type === 'github.excluded-pull-request-comment')375.map(ghComment => {376const change = filteredChanges.find(c => c.relativePath === ghComment.data.path);377return { ghComment, change };378}).filter((item): item is { ghComment: ExcludedComment; change: NonNullable<typeof item.change> } => !!item.change)379.map(({ ghComment, change }, i) => createReviewComment(ghComment, request, change.document, comments.length + i));380const unsupportedLanguages = !comments.length ? [...new Set(references.filter((ref): ref is ExcludedFile => ref.type === 'github.excluded-file' && ref.data.reason === 'file_type_not_supported')381.map(ref => ref.data.language))] : [];382return { type: 'success', comments, excludedComments, reason: unsupportedLanguages.length ? l10n.t('Some of the submitted languages are currently not supported: {0}', unsupportedLanguages.join(', ')) : undefined };383}384385export function createReviewComment(ghComment: ResponseComment | ExcludedComment, request: ReviewRequest, document: TextDocument, index: number) {386const fromLine = document.lineAt(ghComment.data.line - 1);387const lastNonWhitespaceCharacterIndex = fromLine.text.trimEnd().length;388const range = new Range(fromLine.lineNumber, fromLine.firstNonWhitespaceCharacterIndex, fromLine.lineNumber, lastNonWhitespaceCharacterIndex);389const raw = ghComment.data.body;390// Remove suggestion because that interfers with our own suggestion rendering later.391const { content, suggestions } = removeSuggestion(raw);392const startLine = typeof ghComment.data.start_line === 'number' ? ghComment.data.start_line : ghComment.data.line;393const suggestionRange = new Range(startLine - 1, 0, ghComment.data.line, 0);394const comment: ReviewComment = {395request,396document: TextDocumentSnapshot.create(document),397uri: document.uri,398languageId: document.languageId,399range,400body: new MarkdownString(content),401kind: 'bug',402severity: 'medium',403originalIndex: index,404actionCount: 0,405skipSuggestion: true,406suggestion: {407markdown: '',408edits: suggestions.map(suggestion => {409const oldText = document.getText(suggestionRange);410return {411range: suggestionRange,412newText: suggestion,413oldText,414};415}),416},417};418return comment;419}420421const SUGGESTION_EXPRESSION = /```suggestion(\u0020*(\r\n|\n))((?<suggestion>[\s\S]*?)(\r\n|\n))?```/g;422export function removeSuggestion(body: string) {423const suggestions: string[] = [];424const content = body.replaceAll(SUGGESTION_EXPRESSION, (_match, _ws, _nl, suggestion) => {425if (suggestion) {426suggestions.push(suggestion);427}428return '';429});430return { content, suggestions };431}432433// Represents the "before" or "after" state of a file, sent to the agent434interface FileState {435// The path of the file436path: string;437// The file's contents. If the file does not exist in this state, this should be an empty string.438content: string;439// The language ID of the file440languageId: string;441// The selection within the file, if any442selection?: Selection;443}444445// A generated pull request comment returned by the agent.446//447// NOTE: The shape of these return values is under active development and is likely to change.448//449// Example:450//451// {452// "type": "github.generated-pull-request-comment",453// "data": {454// "path": "packages/issues/test/models/referrer_and_referenceable_model_test.rb",455// "line": 82,456// "body": "The word 'Out' should be 'Our'.\n```suggestion\n # Our batched insert only hits the cross references table twice\n```",457// "side": "RIGHT"458// },459// "id": "",460// "is_implicit": false,461// "metadata": {462// "display_name": "",463// "display_icon": "",464// "display_url": ""465// }466// }467468export type ResponseReference = ResponseComment | ExcludedComment | ExcludedFile | { type: 'unknown' };469470export interface ResponseComment {471type: 'github.generated-pull-request-comment';472data: {473// The path of the file474path: string;475// The right-hand line number the comment relates to476line: number;477// The body of the comment, including a ```suggestion block if there is a suggested change478body: string;479start_line?: number;480};481}482483export interface ExcludedComment {484type: 'github.excluded-pull-request-comment';485data: {486path: string;487line: number;488body: string;489start_line?: number;490exclusion_reason: 'denylisted_type' | 'unknown';491};492}493494export interface ExcludedFile {495type: 'github.excluded-file';496data: {497file_path: string;498language: string;499reason: 'file_type_not_supported' | 'unknown';500};501}502503/**504* Raw reference structure from the API response before type validation.505*/506interface RawReference {507type?: string;508data?: unknown;509}510511/**512* Raw parsed response structure from the streaming API.513*/514interface ParsedResponse {515copilot_references?: RawReference[];516}517518/**519* Type guard to check if a raw reference has a valid type field.520* Matches original behavior: filters to refs where ref.type is truthy.521*/522function hasType(ref: RawReference): ref is RawReference & { type: string } {523return !!ref.type;524}525526export function parseLine(line: string): ResponseReference[] {527528if (line === 'data: [DONE]') { return []; }529if (line === '') { return []; }530531const parsedLine: ParsedResponse = JSON.parse(line.replace('data: ', ''));532533if (Array.isArray(parsedLine.copilot_references) && parsedLine.copilot_references.length > 0) {534return parsedLine.copilot_references.filter(hasType) as ResponseReference[];535} else {536return [];537}538}539540async function fetchComments(logService: ILogService, authService: IAuthenticationService, capiClientService: ICAPIClientService, fetcherService: IFetcherService, envService: IEnvService, customInstructionsService: ICustomInstructionsService, workspaceService: IWorkspaceService, kind: 'selection' | 'diff', repository: Repository | undefined, baseFileContents: FileState[], headFileContents: FileState[], cancellationToken: CancellationToken) {541// Collect languageId to file patterns mapping542const languageIdToFilePatterns = new Map<string, Set<string>>();543for (const file of [...baseFileContents, ...headFileContents]) {544const ext = path.extname(file.path);545if (ext) {546if (!languageIdToFilePatterns.has(file.languageId)) {547languageIdToFilePatterns.set(file.languageId, new Set());548}549languageIdToFilePatterns.get(file.languageId)!.add(`*${ext}`);550}551}552553const customInstructions = await loadCustomInstructions(customInstructionsService, workspaceService, kind, languageIdToFilePatterns, 2);554555const requestBody = {556messages: [{557role: 'user',558...(kind === 'selection' ? {559review_type: 'snippet',560snippet_files: headFileContents.map(f => ({561path: f.path,562regions: [563{564start_line: f.selection!.start.line + 1,565end_line: f.selection!.end.line + (f.selection!.end.character > 0 ? 1 : 0), // If selection ends at start of line, don't include that line566}567]568})),569} : {}),570copilot_references: [571{572type: 'github.pull_request',573id: '1',574data: {575type: 'pull-request',576headFileContents: headFileContents.map(({ path, content }) => ({ path, content })),577baseFileContents: baseFileContents.map(({ path, content }) => ({ path, content })),578},579},580...customInstructions,581],582}]583};584585const abort = fetcherService.makeAbortController();586const disposable = cancellationToken.onCancellationRequested(() => abort.abort());587let response: Response;588try {589const copilotToken = await authService.getCopilotToken();590response = await capiClientService.makeRequest({591method: 'POST',592headers: {593Authorization: 'Bearer ' + copilotToken.token,594'X-Copilot-Code-Review-Mode': 'ide',595},596body: JSON.stringify(requestBody),597signal: abort.signal,598}, { type: RequestType.CodeReviewAgent });599} catch (err) {600if (fetcherService.isAbortError(err)) {601return {602requestId: undefined,603rl: undefined,604};605}606throw err;607} finally {608disposable.dispose();609}610611const requestId = response.headers.get('x-github-request-id') || undefined;612613if (!response.ok) {614if (response.status === 402) {615const err = new Error(`You have reached your Code Review quota limit.`);616(err as any).severity = 'info';617throw err;618}619throw new Error(`Agent returned an unexpected HTTP ${response.status} error (request id ${requestId || 'unknown'}).`);620}621622return {623requestId,624rl: readline.createInterface({ input: Readable.fromWeb(response.body.toReadableStream()) }),625};626}627628export function reversePatch(after: string, diff: string) {629const patch = parsePatch(diff.split(/\r?\n/));630const patchedLines = reverseParsedPatch(after.split(/\r?\n/), patch);631return patchedLines.join('\n');632}633634export interface LineChange {635beforeLineNumber: number;636content: string;637type: 'add' | 'remove';638}639640export function parsePatch(patchLines: string[]): LineChange[] {641const changes: LineChange[] = [];642let beforeLineNumber = -1;643644for (const line of patchLines) {645if (line.startsWith('@@')) {646const match = /@@ -(\d+),\d+ \+\d+,\d+ @@/.exec(line);647if (match) {648beforeLineNumber = parseInt(match[1], 10);649}650} else if (beforeLineNumber !== -1) {651if (line.startsWith('+')) {652changes.push({ beforeLineNumber, content: line.slice(1), type: 'add' });653} else if (line.startsWith('-')) {654changes.push({ beforeLineNumber, content: line.slice(1), type: 'remove' });655beforeLineNumber++;656} else {657beforeLineNumber++;658}659}660}661662return changes;663}664665export function reverseParsedPatch(fileLines: string[], patch: LineChange[]): string[] {666for (const change of patch) {667if (change.type === 'add') {668fileLines.splice(change.beforeLineNumber - 1, 1);669} else if (change.type === 'remove') {670fileLines.splice(change.beforeLineNumber - 1, 0, change.content);671}672}673674return fileLines;675}676677export interface CodingGuideline {678type: string;679id: string;680data: {681id: number;682type: string;683name: string;684description: string;685filePatterns: string[];686};687}688689export async function loadCustomInstructions(customInstructionsService: ICustomInstructionsService, workspaceService: IWorkspaceService, kind: 'selection' | 'diff', languageIdToFilePatterns: Map<string, Set<string>>, firstId: number): Promise<CodingGuideline[]> {690const customInstructionRefs = [];691let nextId = firstId;692693// Collect instruction files from agent instructions694const agentInstructionUris = await customInstructionsService.getAgentInstructions();695for (const uri of agentInstructionUris) {696const instructions = await customInstructionsService.fetchInstructionsFromFile(Uri.from(uri));697if (instructions) {698const relativePath = workspaceService.asRelativePath(Uri.from(uri));699for (const instruction of instructions.content) {700// Skip instructions with languageId if not in map701if (instruction.languageId && !languageIdToFilePatterns.has(instruction.languageId)) {702continue;703}704const filePatterns = instruction.languageId ? Array.from(languageIdToFilePatterns.get(instruction.languageId)!) : ['*'];705customInstructionRefs.push({706type: 'github.coding_guideline',707id: `${nextId}`,708data: {709id: nextId,710type: 'coding-guideline',711name: `Instruction from ${relativePath}`,712description: instruction.instruction,713filePatterns,714},715});716nextId++;717}718}719}720721// Collect instructions from settings722const settingsConfigs = [723{ config: ConfigKey.CodeGenerationInstructions, name: 'Code Generation Instruction' },724...(kind === 'selection' ? [{ config: ConfigKey.CodeFeedbackInstructions, name: 'Code Review Instruction' }] : []),725];726727for (const { config, name } of settingsConfigs) {728const instructionsGroups = await customInstructionsService.fetchInstructionsFromSetting(config);729for (const instructionsGroup of instructionsGroups) {730for (const instruction of instructionsGroup.content) {731// Skip instructions with languageId if not in map732if (instruction.languageId && !languageIdToFilePatterns.has(instruction.languageId)) {733continue;734}735const filePatterns = instruction.languageId ? Array.from(languageIdToFilePatterns.get(instruction.languageId)!) : ['*'];736customInstructionRefs.push({737type: 'github.coding_guideline',738id: `${nextId}`,739data: {740id: nextId,741type: 'coding-guideline',742name,743description: instruction.instruction,744filePatterns,745},746});747nextId++;748}749}750}751752return customInstructionRefs;753}754755756