From 6fa866f39d84179d004edab249b9b1c57260cb89 Mon Sep 17 00:00:00 2001 From: Kyle Cutler Date: Mon, 20 Oct 2025 15:51:59 +0200 Subject: [PATCH 1/4] Support generating PR descriptions based on a template --- ...PullRequestTitleAndDescriptionGenerator.ts | 25 +++++++++++-------- .../github/pullRequestDescriptionPrompt.tsx | 13 +++++++++- 2 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/extension/prompt/node/githubPullRequestTitleAndDescriptionGenerator.ts b/src/extension/prompt/node/githubPullRequestTitleAndDescriptionGenerator.ts index 539b60f16..14c3ad5b8 100644 --- a/src/extension/prompt/node/githubPullRequestTitleAndDescriptionGenerator.ts +++ b/src/extension/prompt/node/githubPullRequestTitleAndDescriptionGenerator.ts @@ -73,16 +73,17 @@ export class GitHubPullRequestTitleAndDescriptionGenerator implements TitleAndDe return patches; } - async 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> { + async provideTitleAndDescription(context: { commitMessages: string[]; patches: string[] | { patch: string; fileUri: string; previousFileUri?: string }[]; issues?: { reference: string; content: string }[]; template?: string }, token: CancellationToken): Promise<{ title: string; description?: string } | undefined> { const commitMessages: string[] = context.commitMessages; const allPatches: { patch: string; fileUri?: string; previousFileUri?: string }[] = isStringArray(context.patches) ? context.patches.map(patch => ({ patch })) : context.patches; const patches = await this.excludePatches(allPatches); const issues: { reference: string; content: string }[] | undefined = context.issues; + const template: string | undefined = context.template; const endpoint = await this.endpointProvider.getChatEndpoint('gpt-4o-mini'); const charLimit = Math.floor((endpoint.modelMaxPromptTokens * 4) / 3); - const prompt = await this.createPRTitleAndDescriptionPrompt(commitMessages, patches, issues, charLimit); + const prompt = await this.createPRTitleAndDescriptionPrompt(commitMessages, patches, issues, template, charLimit); const fetchResult = await endpoint .makeChatRequest( 'githubPullRequestTitleAndDescriptionGenerator', @@ -105,10 +106,10 @@ export class GitHubPullRequestTitleAndDescriptionGenerator implements TitleAndDe return undefined; } - return GitHubPullRequestTitleAndDescriptionGenerator.parseFetchResult(fetchResult.value); + return GitHubPullRequestTitleAndDescriptionGenerator.parseFetchResult(fetchResult.value, !!template); } - public static parseFetchResult(value: string, retry: boolean = true): { title: string; description?: string } | undefined { + public static parseFetchResult(value: string, hasTemplate: boolean = false, retry: boolean = true): { title: string; description?: string } | undefined { value = value.trim(); let workingValue = value; let delimiter = '+++'; @@ -130,8 +131,12 @@ export class GitHubPullRequestTitleAndDescriptionGenerator implements TitleAndDe // If there's only one line, split on newlines as the model has left out some +++ delimiters splitOnLines = splitOnPlus[0].split('\n'); } else if (splitOnPlus.length > 1) { - const descriptionLines = splitOnPlus.slice(1).map(line => line.split('\n')).flat().filter(s => s.trim().length > 0); - splitOnLines = [splitOnPlus[0], ...descriptionLines]; + if (hasTemplate) { + splitOnLines = splitOnPlus; + } else { + const descriptionLines = splitOnPlus.slice(1).map(line => line.split('\n')).flat().filter(s => s.trim().length > 0); + splitOnLines = [splitOnPlus[0], ...descriptionLines]; + } } else { return undefined; } @@ -141,7 +146,7 @@ export class GitHubPullRequestTitleAndDescriptionGenerator implements TitleAndDe if (splitOnLines.length === 1) { title = splitOnLines[0].trim(); if (retry && value.includes('\n') && (value.split(delimiter).length === 3)) { - return this.parseFetchResult(value + delimiter, false); + return this.parseFetchResult(value + delimiter, hasTemplate, false); } } else if (splitOnLines.length > 1) { title = splitOnLines[0].trim(); @@ -159,14 +164,14 @@ export class GitHubPullRequestTitleAndDescriptionGenerator implements TitleAndDe if (title) { title = title.replace(/Title\:\s/, '').trim(); title = title.replace(/^\"(?.+)\"$/, (_match, title) => title); - if (description) { + if (description && !hasTemplate) { description = description.replace(/Description\:\s/, '').trim(); } return { title, description }; } } - private async createPRTitleAndDescriptionPrompt(commitMessages: string[], patches: string[], issues: { reference: string; content: string }[] | undefined, charLimit: number): Promise<RenderPromptResult> { + private async createPRTitleAndDescriptionPrompt(commitMessages: string[], patches: string[], issues: { reference: string; content: string }[] | undefined, template: string | undefined, charLimit: number): Promise<RenderPromptResult> { // Reserve 20% of the character limit for the safety rules and instructions const availableChars = charLimit - Math.floor(charLimit * 0.2); @@ -184,7 +189,7 @@ export class GitHubPullRequestTitleAndDescriptionGenerator implements TitleAndDe } const endpoint = await this.endpointProvider.getChatEndpoint('gpt-4o-mini'); - const promptRenderer = PromptRenderer.create(this.instantiationService, endpoint, GitHubPullRequestPrompt, { commitMessages, issues, patches }); + const promptRenderer = PromptRenderer.create(this.instantiationService, endpoint, GitHubPullRequestPrompt, { commitMessages, issues, patches, template }); return promptRenderer.render(undefined, undefined); } } diff --git a/src/extension/prompts/node/github/pullRequestDescriptionPrompt.tsx b/src/extension/prompts/node/github/pullRequestDescriptionPrompt.tsx index 278faeda7..c0f882d6e 100644 --- a/src/extension/prompts/node/github/pullRequestDescriptionPrompt.tsx +++ b/src/extension/prompts/node/github/pullRequestDescriptionPrompt.tsx @@ -11,6 +11,7 @@ interface GitHubPullRequestPromptProps extends BasePromptElementProps { commitMessages: string[]; patches: string[]; issues: { reference: string; content: string }[] | undefined; + template: string | undefined; } interface GitHubPullRequestIdentityProps extends BasePromptElementProps { @@ -76,6 +77,7 @@ class GitHubPullRequestSystemRules extends PromptElement<GitHubPullRequestIdenti To compose the description, read through each commit and patch and tersly describe the intent of the changes, not the changes themselves. Do not list commits, files or patches. Do not make up an issue reference if the pull request isn't fixing an issue.<br /> If the pull request is fixing an issue, consider how the commits relate to the issue and include that in the description.<br /> Avoid saying "this PR" or similar. Avoid passive voice.<br /> + If a template is specified, the description must match the template, filling in any required fields.<br /> The title and description of a pull request should be markdown and start with +++ and end with +++.<br /> <GitHubPullRequestSystemExamples issues={this.props.issues} /> </> @@ -86,6 +88,7 @@ class GitHubPullRequestSystemRules extends PromptElement<GitHubPullRequestIdenti interface GitHubPullRequestUserMessageProps extends BasePromptElementProps { commitMessages: string[]; patches: string[]; + template: string | undefined; } class GitHubPullRequestUserMessage extends PromptElement<GitHubPullRequestUserMessageProps> { @@ -98,6 +101,14 @@ class GitHubPullRequestUserMessage extends PromptElement<GitHubPullRequestUserMe {formattedCommitMessages}<br /> Below is a list of git patches that contain the file changes for all the files that will be included in the pull request:<br /> {formattedPatches}<br /> + {this.props.template ? ( + <> + The pull request description should match the following template:<br /> + ```<br /> + {this.props.template}<br /> + ```<br /> + </> + ) : null} Based on the git patches and on the git commit messages above, the title and description of the pull request should be:<br /> </> ); @@ -113,7 +124,7 @@ export class GitHubPullRequestPrompt extends PromptElement<GitHubPullRequestProm <SafetyRules /> </SystemMessage> <UserMessage> - <GitHubPullRequestUserMessage commitMessages={this.props.commitMessages} patches={this.props.patches} /> + <GitHubPullRequestUserMessage commitMessages={this.props.commitMessages} patches={this.props.patches} template={this.props.template} /> <Tag priority={750} name='custom-instructions'> <CustomInstructions chatVariables={undefined} From bb50072f70844e4679768973a315f50fa7409eb0 Mon Sep 17 00:00:00 2001 From: Kyle Cutler <kycutler@microsoft.com> Date: Wed, 22 Oct 2025 10:39:41 +0200 Subject: [PATCH 2/4] Try fix tests --- .../prompts/node/github/pullRequestDescriptionPrompt.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/extension/prompts/node/github/pullRequestDescriptionPrompt.tsx b/src/extension/prompts/node/github/pullRequestDescriptionPrompt.tsx index c0f882d6e..73d13626e 100644 --- a/src/extension/prompts/node/github/pullRequestDescriptionPrompt.tsx +++ b/src/extension/prompts/node/github/pullRequestDescriptionPrompt.tsx @@ -108,7 +108,7 @@ class GitHubPullRequestUserMessage extends PromptElement<GitHubPullRequestUserMe {this.props.template}<br /> ```<br /> </> - ) : null} + ) : ''} Based on the git patches and on the git commit messages above, the title and description of the pull request should be:<br /> </> ); From 492e1ff52edc471867128460efb84f0c8850df80 Mon Sep 17 00:00:00 2001 From: Kyle Cutler <kycutler@microsoft.com> Date: Wed, 22 Oct 2025 10:43:00 +0200 Subject: [PATCH 3/4] Comment --- .../prompt/node/githubPullRequestTitleAndDescriptionGenerator.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/extension/prompt/node/githubPullRequestTitleAndDescriptionGenerator.ts b/src/extension/prompt/node/githubPullRequestTitleAndDescriptionGenerator.ts index 14c3ad5b8..a0ca6aa70 100644 --- a/src/extension/prompt/node/githubPullRequestTitleAndDescriptionGenerator.ts +++ b/src/extension/prompt/node/githubPullRequestTitleAndDescriptionGenerator.ts @@ -132,6 +132,7 @@ export class GitHubPullRequestTitleAndDescriptionGenerator implements TitleAndDe splitOnLines = splitOnPlus[0].split('\n'); } else if (splitOnPlus.length > 1) { if (hasTemplate) { + // When using a template, keep description whitespace as-is. splitOnLines = splitOnPlus; } else { const descriptionLines = splitOnPlus.slice(1).map(line => line.split('\n')).flat().filter(s => s.trim().length > 0); From ae0abec69539f1a1b26acf3e157aa5d217e9d839 Mon Sep 17 00:00:00 2001 From: Kyle Cutler <kycutler@microsoft.com> Date: Fri, 24 Oct 2025 18:02:45 +0200 Subject: [PATCH 4/4] Update cache --- .../prompts/node/github/pullRequestDescriptionPrompt.tsx | 4 ++-- test/outcome/pr-title-and-description-context.json | 2 +- .../cache/layers/dece6d72-483a-4eba-bfb8-762d7146f5e0.sqlite | 3 +++ 3 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 test/simulation/cache/layers/dece6d72-483a-4eba-bfb8-762d7146f5e0.sqlite diff --git a/src/extension/prompts/node/github/pullRequestDescriptionPrompt.tsx b/src/extension/prompts/node/github/pullRequestDescriptionPrompt.tsx index 73d13626e..eac2175ea 100644 --- a/src/extension/prompts/node/github/pullRequestDescriptionPrompt.tsx +++ b/src/extension/prompts/node/github/pullRequestDescriptionPrompt.tsx @@ -101,14 +101,14 @@ class GitHubPullRequestUserMessage extends PromptElement<GitHubPullRequestUserMe {formattedCommitMessages}<br /> Below is a list of git patches that contain the file changes for all the files that will be included in the pull request:<br /> {formattedPatches}<br /> - {this.props.template ? ( + {this.props.template && ( <> The pull request description should match the following template:<br /> ```<br /> {this.props.template}<br /> ```<br /> </> - ) : ''} + )} Based on the git patches and on the git commit messages above, the title and description of the pull request should be:<br /> </> ); diff --git a/test/outcome/pr-title-and-description-context.json b/test/outcome/pr-title-and-description-context.json index f083a2bf1..cb5e2bf0e 100644 --- a/test/outcome/pr-title-and-description-context.json +++ b/test/outcome/pr-title-and-description-context.json @@ -2,7 +2,7 @@ { "name": "PR Title and Description [context] - Multiple commits without issue information", "requests": [ - "8428a33bed887a6b29356a7e240e42a90fcea91744ec3477395eb5d23dbed752" + "c661b19df36a5e6b216162071bf3a758898a0365e5d29c86deb4c01d5927a4ca" ] } ] \ No newline at end of file diff --git a/test/simulation/cache/layers/dece6d72-483a-4eba-bfb8-762d7146f5e0.sqlite b/test/simulation/cache/layers/dece6d72-483a-4eba-bfb8-762d7146f5e0.sqlite new file mode 100644 index 000000000..df7487edb --- /dev/null +++ b/test/simulation/cache/layers/dece6d72-483a-4eba-bfb8-762d7146f5e0.sqlite @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0da9bacdc60825144f6b0ca8a022c7df1d6c1c07442ffc6072662a659c8bf819 +size 28672