From 739699051c722c824e0747d94f985da87e9ebca0 Mon Sep 17 00:00:00 2001 From: AobaIwaki Date: Tue, 8 Jul 2025 17:11:56 +0900 Subject: [PATCH 1/5] Add pullRequestTemplate parameter to provideTitleAndDescription method --- src/api/api.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/api/api.d.ts b/src/api/api.d.ts index 67d5a856d4..739d228691 100644 --- a/src/api/api.d.ts +++ b/src/api/api.d.ts @@ -242,7 +242,7 @@ export interface IGit { } export interface TitleAndDescriptionProvider { - provideTitleAndDescription(context: { commitMessages: string[], patches: string[] | { patch: string, fileUri: string, previousFileUri?: string }[], issues?: { reference: string, content: string }[] }, token: CancellationToken): Promise<{ title: string, description?: string } | undefined>; + provideTitleAndDescription(context: { commitMessages: string[], patches: string[] | { patch: string, fileUri: string, previousFileUri?: string }[], issues?: { reference: string, content: string }[], pullRequestTemplate?: string }, token: CancellationToken): Promise<{ title: string, description?: string } | undefined>; } export interface ReviewerComments { From d668d188f8aad3b707b47832a8a893da434298df Mon Sep 17 00:00:00 2001 From: AobaIwaki Date: Tue, 8 Jul 2025 17:27:04 +0900 Subject: [PATCH 2/5] Add pull request template to title and description generation --- src/github/createPRViewProvider.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/github/createPRViewProvider.ts b/src/github/createPRViewProvider.ts index 317f19061e..0023b22701 100644 --- a/src/github/createPRViewProvider.ts +++ b/src/github/createPRViewProvider.ts @@ -1020,8 +1020,10 @@ export class CreatePullRequestViewProvider extends BaseCreatePullRequestViewProv const { commitMessages, patches } = await this.getCommitsAndPatches(); const issues = await this.findIssueContext(commitMessages); + const pullRequestTemplate = await this.getPullRequestTemplate(); + const provider = this._folderRepositoryManager.getTitleAndDescriptionProvider(searchTerm); - const result = await provider?.provider.provideTitleAndDescription({ commitMessages, patches, issues }, token); + const result = await provider?.provider.provideTitleAndDescription({ commitMessages, patches, issues, pullRequestTemplate }, token); if (provider) { this.lastGeneratedTitleAndDescription = { ...result, providerTitle: provider.title }; From 4605075690211358cc03294ef9c44ae75e6a1460 Mon Sep 17 00:00:00 2001 From: AobaIwaki Date: Tue, 8 Jul 2025 18:20:09 +0900 Subject: [PATCH 3/5] Add Copilot Title and Description Provider for PR generation --- src/extension.ts | 6 + src/lm/copilotTitleAndDescriptionProvider.ts | 263 +++++++++++++++++++ 2 files changed, 269 insertions(+) create mode 100644 src/lm/copilotTitleAndDescriptionProvider.ts diff --git a/src/extension.ts b/src/extension.ts index 3e539a039c..5e219c77af 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -29,6 +29,7 @@ import { registerBuiltinGitProvider, registerLiveShareGitProvider } from './gitP import { GitHubContactServiceProvider } from './gitProviders/GitHubContactServiceProvider'; import { GitLensIntegration } from './integrations/gitlens/gitlensImpl'; import { IssueFeatureRegistrar } from './issues/issueFeatureRegistrar'; +import { CopilotTitleAndDescriptionProvider } from './lm/copilotTitleAndDescriptionProvider'; import { ChatParticipant, ChatParticipantState } from './lm/participants'; import { registerTools } from './lm/tools/tools'; import { migrate } from './migrations'; @@ -397,6 +398,11 @@ async function deferredActivate(context: vscode.ExtensionContext, showPRControll const apiImpl = new GitApiImpl(reposManager); context.subscriptions.push(apiImpl); + // Register Copilot Title and Description Provider + Logger.debug('Registering Copilot Title and Description Provider.', 'Activation'); + const copilotProvider = new CopilotTitleAndDescriptionProvider(credentialStore, telemetry); + apiImpl.registerTitleAndDescriptionProvider('Copilot', copilotProvider); + deferredActivateRegisterBuiltInGitProvider(context, apiImpl, credentialStore); Logger.debug('Registering live share git provider.', 'Activation'); diff --git a/src/lm/copilotTitleAndDescriptionProvider.ts b/src/lm/copilotTitleAndDescriptionProvider.ts new file mode 100644 index 0000000000..0b3c05ed68 --- /dev/null +++ b/src/lm/copilotTitleAndDescriptionProvider.ts @@ -0,0 +1,263 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TitleAndDescriptionProvider } from '../api/api'; +import Logger from '../common/logger'; +import { ITelemetry } from '../common/telemetry'; +import { CredentialStore } from '../github/credentials'; + +/** + * Provides PR title and description generation using VS Code's built-in Copilot language models. + * This provider leverages pull request templates when available to generate contextually appropriate + * titles and descriptions that follow the repository's conventions. + */ +export class CopilotTitleAndDescriptionProvider implements TitleAndDescriptionProvider { + private static readonly ID = 'CopilotTitleAndDescriptionProvider'; + + constructor( + private readonly credentialStore: CredentialStore, + private readonly telemetry: ITelemetry + ) { } + + async provideTitleAndDescription( + context: { + commitMessages: string[]; + patches: string[] | { patch: string; fileUri: string; previousFileUri?: string }[]; + issues?: { reference: string; content: string }[]; + pullRequestTemplate?: string; + }, + token: vscode.CancellationToken + ): Promise<{ title: string; description?: string } | undefined> { + try { + Logger.debug('Starting Copilot PR title and description generation', CopilotTitleAndDescriptionProvider.ID); + + // Select the appropriate language model (use user's preference from all available models) + const models = await vscode.lm.selectChatModels(); + console.log(`Available models: ${models.map(m => `${m.vendor}:${m.family} (${m.name})`).join(', ')}`); + + if (!models || models.length === 0) { + Logger.warn('No language models available', CopilotTitleAndDescriptionProvider.ID); + return undefined; + } + + // Prefer higher capability models for better PR generation + // Priority: Claude > GPT-4 > GPT-3.5 + const model = this.selectBestModel(models); + console.log(`Using model: ${model.vendor}:${model.family} (${model.name})`); + Logger.debug(`Using model: ${model.vendor}:${model.family} (${model.name})`, CopilotTitleAndDescriptionProvider.ID); + + // Build the prompt + const prompt = this.buildPrompt(context); + const messages = [vscode.LanguageModelChatMessage.User(prompt)]; + + // Send request to language model + const response = await model.sendRequest(messages, { + justification: 'Generating pull request title and description based on commits and repository templates' + }, token); + + // Parse response + let responseText = ''; + for await (const part of response.stream) { + if (part instanceof vscode.LanguageModelTextPart) { + responseText += part.value; + } + } + + const result = this.parseResponse(responseText); + + if (result) { + Logger.debug(`Generated title: "${result.title}"`, CopilotTitleAndDescriptionProvider.ID); + Logger.debug(`Generated description length: ${result.description?.length || 0} characters`, CopilotTitleAndDescriptionProvider.ID); + + // Track telemetry + this.telemetry.sendTelemetryEvent('copilot.titleAndDescription.generated', { + hasTemplate: context.pullRequestTemplate ? 'true' : 'false', + commitCount: context.commitMessages.length.toString(), + patchCount: Array.isArray(context.patches) ? context.patches.length.toString() : 'unknown', + issueCount: (context.issues?.length || 0).toString(), + modelVendor: model.vendor, + modelFamily: model.family, + modelName: model.name + }); + } + + return result; + } catch (error) { + Logger.error(`Error generating PR title and description: ${error}`, CopilotTitleAndDescriptionProvider.ID); + + this.telemetry.sendTelemetryEvent('copilot.titleAndDescription.error', { + error: error instanceof Error ? error.message : 'unknown' + }); + + return undefined; + } + } + + /** + * Selects the best available model for PR generation. + * Prioritizes models based on their capabilities for text generation. + */ + private selectBestModel(models: readonly vscode.LanguageModelChat[]): vscode.LanguageModelChat { + // Define model preference order (higher priority = better for PR generation) + const modelPreferences = [ + // Basic models (lowest priority) + { vendor: 'copilot', family: 'gpt-3.5-turbo', priority: 1 }, + { vendor: 'copilot', family: 'gpt-3.5', priority: 1 }, + + // Standard GPT-4 models (medium priority) + { vendor: 'copilot', family: 'gpt-4', priority: 2 }, + { vendor: 'copilot', family: 'gpt-4o-mini', priority: 3 }, + { vendor: 'copilot', family: 'gpt-4-turbo', priority: 4 }, + { vendor: 'copilot', family: 'gpt-4o', priority: 5 }, + { vendor: 'copilot', family: 'gpt-4.1', priority: 6 }, + + // Claude models (high priority for text generation) + { vendor: 'copilot', family: 'claude-3-haiku', priority: 7 }, + { vendor: 'copilot', family: 'claude-3-sonnet', priority: 8 }, + { vendor: 'copilot', family: 'claude-3.5-sonnet', priority: 9 }, + { vendor: 'copilot', family: 'claude-3-opus', priority: 10 }, + { vendor: 'copilot', family: 'claude-3.7-sonnet', priority: 11 }, + { vendor: 'copilot', family: 'claude-3.7-sonnet-thought', priority: 12 }, + + // Gemini models (high priority) + { vendor: 'copilot', family: 'gemini-2.0-flash', priority: 13 }, + { vendor: 'copilot', family: 'gemini-2.5-pro', priority: 14 }, + + // Latest advanced models (highest priority) + { vendor: 'copilot', family: 'o3-mini', priority: 15 }, + { vendor: 'copilot', family: 'claude-sonnet-4', priority: 16 }, + { vendor: 'copilot', family: 'o4-mini', priority: 17 }, + ]; + + // Find the highest priority model available + let bestModel = models[0]; + let bestPriority = 0; + + for (const model of models) { + const preference = modelPreferences.find( + p => p.vendor === model.vendor && p.family === model.family + ); + const priority = preference?.priority || 0; + + if (priority > bestPriority) { + bestModel = model; + bestPriority = priority; + } + } + + Logger.debug( + `Selected model: ${bestModel.vendor}:${bestModel.family} (priority: ${bestPriority}) from ${models.length} available models`, + CopilotTitleAndDescriptionProvider.ID + ); + + return bestModel; + } + + private buildPrompt(context: { + commitMessages: string[]; + patches: string[] | { patch: string; fileUri: string; previousFileUri?: string }[]; + issues?: { reference: string; content: string }[]; + pullRequestTemplate?: string; + }): string { + let prompt = `You are an expert at writing clear, concise pull request titles and descriptions. Please generate a suitable title and description for this pull request based on the provided information. + +**Requirements:** +1. Title should be concise (under 50 characters when possible) and descriptive +2. Title should follow conventional commit format when appropriate (e.g., "feat:", "fix:", "docs:", etc.) +3. Description should be comprehensive but focused +4. If a pull request template is provided, follow its structure and fill in the sections appropriately +5. Reference any related issues mentioned in commits +6. Summarize the key changes without being overly technical + +`; + + // Add commit information + if (context.commitMessages && context.commitMessages.length > 0) { + prompt += `**Commit Messages:**\n`; + context.commitMessages.forEach((msg, index) => { + prompt += `${index + 1}. ${msg}\n`; + }); + prompt += '\n'; + } + + // Add patch information summary + if (context.patches && context.patches.length > 0) { + prompt += `**Changes Summary:**\n`; + if (Array.isArray(context.patches) && typeof context.patches[0] === 'string') { + prompt += `${context.patches.length} file(s) modified\n`; + } else { + const patchObjects = context.patches as { patch: string; fileUri: string; previousFileUri?: string }[]; + prompt += `Files modified: ${patchObjects.length}\n`; + const fileList = patchObjects.map(p => p.fileUri).slice(0, 10); // Limit to first 10 files + prompt += `Key files: ${fileList.join(', ')}${patchObjects.length > 10 ? '...' : ''}\n`; + } + prompt += '\n'; + } + + // Add related issues + if (context.issues && context.issues.length > 0) { + prompt += `**Related Issues:**\n`; + context.issues.forEach(issue => { + prompt += `- ${issue.reference}: ${issue.content}\n`; + }); + prompt += '\n'; + } + + // Add pull request template - this is the key part for template integration + if (context.pullRequestTemplate) { + prompt += `**Pull Request Template to Follow:**\n`; + prompt += '```\n' + context.pullRequestTemplate + '\n```\n\n'; + prompt += `Please structure the description according to this template. Fill in each section with relevant information based on the commits and changes. If a section is not applicable, you may omit it or note "N/A".\n\n`; + } + + prompt += `**Output Format:** +Please respond with the title and description in the following format: + +TITLE: [Your generated title here] + +DESCRIPTION: +[Your generated description here] + +Make sure the title is on a single line after "TITLE:" and the description follows after "DESCRIPTION:" on subsequent lines.`; + + return prompt; + } + + private parseResponse(responseText: string): { title: string; description?: string } | undefined { + try { + // Look for TITLE: and DESCRIPTION: markers + const titleMatch = responseText.match(/TITLE:\s*(.+?)(?=\n|$)/i); + const descriptionMatch = responseText.match(/DESCRIPTION:\s*([\s\S]*?)(?=\n\n|$)/i); + + if (!titleMatch) { + Logger.warn('Could not parse title from response', CopilotTitleAndDescriptionProvider.ID); + return undefined; + } + + const title = titleMatch[1].trim(); + const description = descriptionMatch ? descriptionMatch[1].trim() : undefined; + + // Validate title + if (!title || title.length === 0) { + Logger.warn('Generated title is empty', CopilotTitleAndDescriptionProvider.ID); + return undefined; + } + + // Clean up description + const cleanDescription = description && description.length > 0 + ? description.replace(/^[\s\n]+|[\s\n]+$/g, '') // Trim whitespace and newlines + : undefined; + + return { + title, + description: cleanDescription + }; + } catch (error) { + Logger.error(`Error parsing response: ${error}`, CopilotTitleAndDescriptionProvider.ID); + return undefined; + } + } +} From adb029f9f9716b3603e63c13705c226eea7294a5 Mon Sep 17 00:00:00 2001 From: AobaIwaki Date: Tue, 8 Jul 2025 19:36:42 +0900 Subject: [PATCH 4/5] Refactor PR generation prompt to enforce strict adherence to PR template structure and improve model priority order --- src/lm/copilotTitleAndDescriptionProvider.ts | 81 +++++++++++++------- 1 file changed, 52 insertions(+), 29 deletions(-) diff --git a/src/lm/copilotTitleAndDescriptionProvider.ts b/src/lm/copilotTitleAndDescriptionProvider.ts index 0b3c05ed68..ffc3a3ad55 100644 --- a/src/lm/copilotTitleAndDescriptionProvider.ts +++ b/src/lm/copilotTitleAndDescriptionProvider.ts @@ -128,8 +128,8 @@ export class CopilotTitleAndDescriptionProvider implements TitleAndDescriptionPr // Latest advanced models (highest priority) { vendor: 'copilot', family: 'o3-mini', priority: 15 }, - { vendor: 'copilot', family: 'claude-sonnet-4', priority: 16 }, - { vendor: 'copilot', family: 'o4-mini', priority: 17 }, + { vendor: 'copilot', family: 'o4-mini', priority: 16 }, + { vendor: 'copilot', family: 'claude-sonnet-4', priority: 17 }, ]; // Find the highest priority model available @@ -162,17 +162,37 @@ export class CopilotTitleAndDescriptionProvider implements TitleAndDescriptionPr issues?: { reference: string; content: string }[]; pullRequestTemplate?: string; }): string { - let prompt = `You are an expert at writing clear, concise pull request titles and descriptions. Please generate a suitable title and description for this pull request based on the provided information. + console.log('Building prompt for Copilot Title and Description generation'); -**Requirements:** -1. Title should be concise (under 50 characters when possible) and descriptive -2. Title should follow conventional commit format when appropriate (e.g., "feat:", "fix:", "docs:", etc.) -3. Description should be comprehensive but focused -4. If a pull request template is provided, follow its structure and fill in the sections appropriately -5. Reference any related issues mentioned in commits -6. Summarize the key changes without being overly technical + let prompt = ` + You are an expert at writing clear, concise, and structured pull request titles and descriptions. -`; + Your task is to generate a PR title and description that strictly follows the provided PR Template. + + --- + Guidelines: + - Use the PR Template as-is. Do not omit, rename, or reformat any section or checkbox. + - Fill in each section with appropriate content based on the context provided below. + - Use Conventional Commit format for the title (e.g., "feat:", "fix:", "docs:"). + - Keep the title under 50 characters if possible. + - Mention related issues using "Closes #123" or similar format. + - If no information is available for a section, write "N/A". + + --- + You will be given the following context: + - The PR Template (in Markdown format) + - A list of commit messages + - A summary of file changes + - A list of related issues (with title and reference) + + --- + Your output must include only: + 1. The PR title + 2. The PR description with the PR Template structure filled in + + Do not add any commentary or explanation. + + `; // Add commit information if (context.commitMessages && context.commitMessages.length > 0) { @@ -210,27 +230,35 @@ export class CopilotTitleAndDescriptionProvider implements TitleAndDescriptionPr if (context.pullRequestTemplate) { prompt += `**Pull Request Template to Follow:**\n`; prompt += '```\n' + context.pullRequestTemplate + '\n```\n\n'; - prompt += `Please structure the description according to this template. Fill in each section with relevant information based on the commits and changes. If a section is not applicable, you may omit it or note "N/A".\n\n`; + prompt += `Please copy the following PR template structure exactly and fill in each section with relevant content. If a section is not applicable, write "N/A" but do not remove the heading.\n\n`; } prompt += `**Output Format:** -Please respond with the title and description in the following format: + Please respond with the pull request title and description in the following format: -TITLE: [Your generated title here] + TITLE: [your generated title here] -DESCRIPTION: -[Your generated description here] + DESCRIPTION: + [your generated description here, using the PR template above] -Make sure the title is on a single line after "TITLE:" and the description follows after "DESCRIPTION:" on subsequent lines.`; + Notes: + - The title must be on a single line immediately after "TITLE:" + - The description must begin after "DESCRIPTION:" and strictly follow the provided PR template structure + - Do not include any text before or after this format + `; return prompt; } private parseResponse(responseText: string): { title: string; description?: string } | undefined { try { - // Look for TITLE: and DESCRIPTION: markers - const titleMatch = responseText.match(/TITLE:\s*(.+?)(?=\n|$)/i); - const descriptionMatch = responseText.match(/DESCRIPTION:\s*([\s\S]*?)(?=\n\n|$)/i); + console.log(`Received response: ${responseText}`); + + // Extract title after "TITLE:" until the end of the line + const titleMatch = responseText.match(/^TITLE:\s*(.+)$/m); + + // Extract description after "DESCRIPTION:\n" + const descriptionMatch = responseText.match(/^DESCRIPTION:\s*\n([\s\S]*)$/m); if (!titleMatch) { Logger.warn('Could not parse title from response', CopilotTitleAndDescriptionProvider.ID); @@ -238,23 +266,18 @@ Make sure the title is on a single line after "TITLE:" and the description follo } const title = titleMatch[1].trim(); - const description = descriptionMatch ? descriptionMatch[1].trim() : undefined; - - // Validate title if (!title || title.length === 0) { Logger.warn('Generated title is empty', CopilotTitleAndDescriptionProvider.ID); return undefined; } - // Clean up description + const description = descriptionMatch?.[1].trim(); const cleanDescription = description && description.length > 0 - ? description.replace(/^[\s\n]+|[\s\n]+$/g, '') // Trim whitespace and newlines + ? description.replace(/^[\s\n]+|[\s\n]+$/g, '') : undefined; - return { - title, - description: cleanDescription - }; + return { title, description: cleanDescription }; + } catch (error) { Logger.error(`Error parsing response: ${error}`, CopilotTitleAndDescriptionProvider.ID); return undefined; From e3cfba5b3e08e2a277d6299c9269b74b2953864b Mon Sep 17 00:00:00 2001 From: AobaIwaki Date: Tue, 8 Jul 2025 19:42:46 +0900 Subject: [PATCH 5/5] Remove unused CredentialStore dependency from CopilotTitleAndDescriptionProvider --- src/lm/copilotTitleAndDescriptionProvider.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lm/copilotTitleAndDescriptionProvider.ts b/src/lm/copilotTitleAndDescriptionProvider.ts index ffc3a3ad55..f7c3b34765 100644 --- a/src/lm/copilotTitleAndDescriptionProvider.ts +++ b/src/lm/copilotTitleAndDescriptionProvider.ts @@ -7,7 +7,6 @@ import * as vscode from 'vscode'; import { TitleAndDescriptionProvider } from '../api/api'; import Logger from '../common/logger'; import { ITelemetry } from '../common/telemetry'; -import { CredentialStore } from '../github/credentials'; /** * Provides PR title and description generation using VS Code's built-in Copilot language models. @@ -18,7 +17,6 @@ export class CopilotTitleAndDescriptionProvider implements TitleAndDescriptionPr private static readonly ID = 'CopilotTitleAndDescriptionProvider'; constructor( - private readonly credentialStore: CredentialStore, private readonly telemetry: ITelemetry ) { } @@ -34,6 +32,7 @@ export class CopilotTitleAndDescriptionProvider implements TitleAndDescriptionPr try { Logger.debug('Starting Copilot PR title and description generation', CopilotTitleAndDescriptionProvider.ID); + // FIXME: The model which the user selected should be used here. // Select the appropriate language model (use user's preference from all available models) const models = await vscode.lm.selectChatModels(); console.log(`Available models: ${models.map(m => `${m.vendor}:${m.family} (${m.name})`).join(', ')}`);