diff --git a/CHANGELOG.md b/CHANGELOG.md index 2dbf03c54a6dd..a116aaa0401db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this p ### Added - Adds AI model status and model switcher to the _Home_ view ([#4064](https://github.com/gitkraken/vscode-gitlens/issues/4064)) +- Adds an optional "Create with AI" button to generate pull requests using AI assistance for GitHub and GitLab. - Adds Anthropic Claude 3.7 Sonnet model for GitLens' AI features ([#4101](https://github.com/gitkraken/vscode-gitlens/issues/4101)) - Adds Google Gemini 2.5 Pro (Experimental) and Gemini 2.0 Flash-Lite model for GitLens' AI features ([#4104](https://github.com/gitkraken/vscode-gitlens/issues/4104)) - Adds integration with Bitbucket Cloud and Data Center ([#3916](https://github.com/gitkraken/vscode-gitlens/issues/3916)) diff --git a/docs/telemetry-events.md b/docs/telemetry-events.md index 0fa3a2779d227..2e7cd5fb7a194 100644 --- a/docs/telemetry-events.md +++ b/docs/telemetry-events.md @@ -208,6 +208,29 @@ or or +```typescript +{ + 'duration': number, + 'failed.error': string, + 'failed.reason': 'user-declined' | 'user-cancelled' | 'error', + 'input.length': number, + 'model.id': string, + 'model.provider.id': 'anthropic' | 'deepseek' | 'gemini' | 'github' | 'gitkraken' | 'huggingface' | 'openai' | 'vscode' | 'xai', + 'model.provider.name': string, + 'output.length': number, + 'retry.count': number, + 'type': 'createPullRequest', + 'usage.completionTokens': number, + 'usage.limits.limit': number, + 'usage.limits.resetsOn': string, + 'usage.limits.used': number, + 'usage.promptTokens': number, + 'usage.totalTokens': number +} +``` + +or + ```typescript { 'duration': number, diff --git a/package.json b/package.json index 46b9ae11c47ef..f8d679bf2bc24 100644 --- a/package.json +++ b/package.json @@ -4152,7 +4152,7 @@ "preview" ] }, - "gitlens.ai.generateCloudPatchMessage.customInstructions": { + "gitlens.ai.generateCreateCloudPatch.customInstructions": { "type": "string", "default": null, "markdownDescription": "Specifies custom instructions to provide to the AI provider when generating a cloud patch title and description", @@ -4162,7 +4162,7 @@ "preview" ] }, - "gitlens.ai.generateCodeSuggestMessage.customInstructions": { + "gitlens.ai.generateCreateCodeSuggest.customInstructions": { "type": "string", "default": null, "markdownDescription": "Specifies custom instructions to provide to the AI provider when generating a code suggest title and description", @@ -4171,6 +4171,16 @@ "tags": [ "preview" ] + }, + "gitlens.ai.generateCreatePullRequest.customInstructions": { + "type": "string", + "default": null, + "markdownDescription": "Specifies custom instructions to provide to the AI provider when generating a pull request title and description", + "scope": "window", + "order": 500, + "tags": [ + "preview" + ] } } }, diff --git a/src/api/gitlens.d.ts b/src/api/gitlens.d.ts index 4bd344866b965..c519d89b55240 100644 --- a/src/api/gitlens.d.ts +++ b/src/api/gitlens.d.ts @@ -1,4 +1,5 @@ import type { Disposable } from 'vscode'; +import type { Sources } from '../constants.telemetry'; export type { Disposable } from 'vscode'; @@ -24,6 +25,8 @@ export interface CreatePullRequestActionContext { readonly url?: string; } | undefined; + readonly source?: Sources; + readonly useAI?: boolean; } export interface OpenPullRequestActionContext { diff --git a/src/commands/createPullRequestOnRemote.ts b/src/commands/createPullRequestOnRemote.ts index 899f26244d74a..a1060ec11ae1a 100644 --- a/src/commands/createPullRequestOnRemote.ts +++ b/src/commands/createPullRequestOnRemote.ts @@ -1,4 +1,5 @@ import { window } from 'vscode'; +import type { Sources } from '../constants.telemetry'; import type { Container } from '../container'; import type { GitRemote } from '../git/models/remote'; import type { RemoteResource } from '../git/models/remoteResource'; @@ -17,6 +18,8 @@ export interface CreatePullRequestOnRemoteCommandArgs { repoPath: string; clipboard?: boolean; + source?: Sources; + useAI?: boolean; } @command() @@ -72,8 +75,33 @@ export class CreatePullRequestOnRemoteCommand extends GlCommandBase { }, compare: { branch: args.compare, - remote: { path: compareRemote.path, url: compareRemote.url }, + remote: { path: compareRemote.path, url: compareRemote.url, name: compareRemote.name }, }, + describePullRequest: !args.useAI + ? undefined + : async (completedResource: RemoteResource & { type: RemoteResourceType.CreatePullRequest }) => { + const base = completedResource.base; + const compare = completedResource.compare; + if (!base?.remote || !compare?.remote || !base?.branch || !compare?.branch) { + return undefined; + } + const baseRef = `${base.remote.name}/${base.branch}`; + const compareRef = `${compare.remote.name}/${compare.branch}`; + try { + const result = await this.container.ai.generatePullRequestMessage( + repo, + baseRef, + compareRef, + { + source: args.source ?? 'scm-input', + }, + ); + return result?.parsed; + } catch (e) { + void window.showErrorMessage(`Unable to generate pull request details: ${e}`); + return undefined; + } + }, }; void (await executeCommand('gitlens.openOnRemote', { diff --git a/src/config.ts b/src/config.ts index 4d0d69aa3dbd0..c94b0c261b963 100644 --- a/src/config.ts +++ b/src/config.ts @@ -212,6 +212,10 @@ interface AIConfig { readonly generateChangelog: { readonly customInstructions: string; }; + readonly generatePullRequestMessage: { + readonly customInstructions: string; + readonly enabled: boolean; + }; readonly generateCommitMessage: { readonly customInstructions: string; readonly enabled: boolean; @@ -219,10 +223,13 @@ interface AIConfig { readonly generateStashMessage: { readonly customInstructions: string; }; - readonly generateCloudPatchMessage: { + readonly generateCreateCloudPatch: { + readonly customInstructions: string; + }; + readonly generateCreateCodeSuggest: { readonly customInstructions: string; }; - readonly generateCodeSuggestMessage: { + readonly generateCreatePullRequest: { readonly customInstructions: string; }; readonly gitkraken: { diff --git a/src/constants.telemetry.ts b/src/constants.telemetry.ts index 257a95fdfe4cf..b32f7a4700a29 100644 --- a/src/constants.telemetry.ts +++ b/src/constants.telemetry.ts @@ -344,10 +344,15 @@ export interface AIGenerateChangelogEventData extends AIEventDataBase { type: 'changelog'; } +export interface AIGenerateCreatePullRequestEventData extends AIEventDataBase { + type: 'createPullRequest'; +} + type AIGenerateEvent = | AIGenerateCommitEventData | AIGenerateDraftEventData | AIGenerateStashEventData + | AIGenerateCreatePullRequestEventData | AIGenerateChangelogEventData; export type AISwitchModelEvent = diff --git a/src/env/node/git/sub-providers/diff.ts b/src/env/node/git/sub-providers/diff.ts index 21800a7c05159..4f22360858929 100644 --- a/src/env/node/git/sub-providers/diff.ts +++ b/src/env/node/git/sub-providers/diff.ts @@ -89,12 +89,12 @@ export class DiffGitSubProvider implements GitDiffSubProvider { repoPath: string, to: string, from?: string, - options?: { context?: number; includeUntracked: boolean; uris?: Uri[] }, + options?: { context?: number; includeUntracked: boolean; uris?: Uri[]; notation?: '..' | '...' }, ): Promise { const scope = getLogScope(); const args = [`-U${options?.context ?? 3}`]; - from = prepareToFromDiffArgs(to, from, args); + from = prepareToFromDiffArgs(to, from, args, options?.notation); let paths: Set | undefined; let untrackedPaths: string[] | undefined; @@ -630,7 +630,7 @@ export class DiffGitSubProvider implements GitDiffSubProvider { } } } -function prepareToFromDiffArgs(to: string, from: string | undefined, args: string[]) { +function prepareToFromDiffArgs(to: string, from: string | undefined, args: string[], notation?: '..' | '...'): string { if (to === uncommitted) { if (from != null) { args.push(from); @@ -656,6 +656,8 @@ function prepareToFromDiffArgs(to: string, from: string | undefined, args: strin } } else if (to === '') { args.push(from); + } else if (notation != null) { + args.push(`${from}${notation}${to}`); } else { args.push(from, to); } diff --git a/src/extension.ts b/src/extension.ts index e89429f752ef7..084257eeee3ca 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -309,6 +309,8 @@ function registerBuiltInActionRunners(container: Container): void { : ctx.branch.name, remote: ctx.remote?.name ?? '', repoPath: ctx.repoPath, + source: ctx.source, + useAI: ctx.useAI, })); }, }), diff --git a/src/features.ts b/src/features.ts index c5f547b1097e0..19599fbd1d639 100644 --- a/src/features.ts +++ b/src/features.ts @@ -40,16 +40,17 @@ export type ProFeatures = | 'startWork' | 'associateIssueWithBranch' | ProAIFeatures; -export type ProAIFeatures = 'generateStashMessage' | 'explainCommit' | 'cloudPatchGenerateTitleAndDescription'; +export type ProAIFeatures = 'explainCommit' | 'generateCreateDraft' | 'generateStashMessage'; export type AdvancedFeatures = AdvancedAIFeatures; -export type AdvancedAIFeatures = 'generateChangelog'; +export type AdvancedAIFeatures = 'generateChangelog' | 'generateCreatePullRequest'; export type AIFeatures = ProAIFeatures | AdvancedAIFeatures; export function isAdvancedFeature(feature: PlusFeatures): feature is AdvancedFeatures { switch (feature) { case 'generateChangelog': + case 'generateCreatePullRequest': return true; default: return false; diff --git a/src/git/gitProvider.ts b/src/git/gitProvider.ts index 90727e5291d18..bc9384caaed36 100644 --- a/src/git/gitProvider.ts +++ b/src/git/gitProvider.ts @@ -363,7 +363,7 @@ export interface GitDiffSubProvider { repoPath: string | Uri, to: string, from?: string, - options?: { context?: number; includeUntracked?: boolean; uris?: Uri[] }, + options?: { context?: number; includeUntracked?: boolean; uris?: Uri[]; notation?: '..' | '...' }, ): Promise; getDiffFiles?(repoPath: string | Uri, contents: string): Promise; getDiffStatus( diff --git a/src/git/gitProviderService.ts b/src/git/gitProviderService.ts index 084dfd38f452c..909ffd4bd03ff 100644 --- a/src/git/gitProviderService.ts +++ b/src/git/gitProviderService.ts @@ -779,10 +779,11 @@ export class GitProviderService implements Disposable { feature === 'launchpad' || feature === 'startWork' || feature === 'associateIssueWithBranch' || - feature === 'generateStashMessage' || feature === 'explainCommit' || - feature === 'cloudPatchGenerateTitleAndDescription' || - feature === 'generateChangelog' + feature === 'generateChangelog' || + feature === 'generateCreateDraft' || + feature === 'generateCreatePullRequest' || + feature === 'generateStashMessage' ) { return { allowed: false, subscription: { current: subscription, required: SubscriptionPlanId.Pro } }; } diff --git a/src/git/models/remoteResource.ts b/src/git/models/remoteResource.ts index 70e7daf346fe6..554463e47fd52 100644 --- a/src/git/models/remoteResource.ts +++ b/src/git/models/remoteResource.ts @@ -35,12 +35,15 @@ export type RemoteResource = type: RemoteResourceType.CreatePullRequest; base: { branch: string | undefined; - remote: { path: string; url: string }; + remote: { path: string; url: string; name: string }; }; compare: { branch: string; - remote: { path: string; url: string }; + remote: { path: string; url: string; name: string }; }; + describePullRequest?: ( + completedResource: RemoteResource & { type: RemoteResourceType.CreatePullRequest }, + ) => Promise<{ summary: string; body: string } | undefined>; } | { type: RemoteResourceType.File; diff --git a/src/git/remotes/github.ts b/src/git/remotes/github.ts index 02def406fdb0f..d65165a91fa55 100644 --- a/src/git/remotes/github.ts +++ b/src/git/remotes/github.ts @@ -280,12 +280,16 @@ export class GitHubRemote extends RemoteProvider { return this.encodeUrl(`${this.baseUrl}/compare/${base}${notation}${head}`); } - protected override getUrlForCreatePullRequest( + protected override async getUrlForCreatePullRequest( base: { branch?: string; remote: { path: string; url: string } }, head: { branch: string; remote: { path: string; url: string } }, - options?: { title?: string; description?: string }, - ): string | undefined { - const query = new URLSearchParams(); + options?: { + title?: string; + description?: string; + describePullRequest?: () => Promise<{ summary: string; body: string } | undefined>; + }, + ): Promise { + const query = new URLSearchParams({ expand: '1' }); if (options?.title) { query.set('title', options.title); } @@ -293,15 +297,25 @@ export class GitHubRemote extends RemoteProvider { query.set('body', options.description); } + if ((!options?.title || !options?.description) && options?.describePullRequest) { + const result = await options.describePullRequest(); + if (result?.summary) { + query.set('title', result.summary); + } + if (result?.body) { + query.set('body', result.body); + } + } + if (base.remote.url === head.remote.url) { - return `${this.encodeUrl( - `${this.baseUrl}/pull/new/${base.branch ?? 'HEAD'}...${head.branch}`, - )}?${query.toString()}`; + return base.branch + ? `${this.encodeUrl(`${this.baseUrl}/compare/${base.branch}...${head.branch}`)}?${query.toString()}` + : `${this.encodeUrl(`${this.baseUrl}/compare/${head.branch}`)}?${query.toString()}`; } const [owner] = head.remote.path.split('/', 1); return `${this.encodeUrl( - `${this.baseUrl}/pull/new/${base.branch ?? 'HEAD'}...${owner}:${head.branch}`, + `${this.baseUrl}/compare/${base.branch ?? 'HEAD'}...${owner}:${head.branch}`, )}?${query.toString()}`; } diff --git a/src/git/remotes/gitlab.ts b/src/git/remotes/gitlab.ts index e7f4d7eb72b51..e314c673bf9fb 100644 --- a/src/git/remotes/gitlab.ts +++ b/src/git/remotes/gitlab.ts @@ -395,7 +395,11 @@ export class GitLabRemote extends RemoteProvider { protected override async getUrlForCreatePullRequest( base: { branch?: string; remote: { path: string; url: string } }, head: { branch: string; remote: { path: string; url: string } }, - options?: { title?: string; description?: string }, + options?: { + title?: string; + description?: string; + describePullRequest?: () => Promise<{ summary: string; body: string } | undefined>; + }, ): Promise { const query = new URLSearchParams({ utf8: '✓', @@ -425,6 +429,15 @@ export class GitLabRemote extends RemoteProvider { if (options?.description) { query.set('merge_request[description]', options.description); } + if ((!options?.title || !options?.description) && options?.describePullRequest) { + const result = await options.describePullRequest(); + if (result?.summary) { + query.set('merge_request[title]', result.summary); + } + if (result?.body) { + query.set('merge_request[description]', result.body); + } + } return `${this.encodeUrl(`${this.getRepoBaseUrl(head.remote.path)}/-/merge_requests/new`)}?${query.toString()}`; } diff --git a/src/git/remotes/remoteProvider.ts b/src/git/remotes/remoteProvider.ts index 21b6cf31c0a26..bbb756569fe2d 100644 --- a/src/git/remotes/remoteProvider.ts +++ b/src/git/remotes/remoteProvider.ts @@ -25,6 +25,13 @@ export type RemoteProviderId = | 'gitlab' | 'google-source'; +export const remotesSupportTitleOnPullRequestCreation: RemoteProviderId[] = [ + 'github', + 'gitlab', + 'cloud-github-enterprise', + 'cloud-gitlab-self-hosted', +]; + export abstract class RemoteProvider implements ProviderReference { protected readonly _name: string | undefined; @@ -132,7 +139,9 @@ export abstract class RemoteProvider Promise<{ summary: string; body: string } | undefined>; + }, ): Promise | string | undefined; protected abstract getUrlForFile(fileName: string, branch?: string, sha?: string, range?: Range): string; diff --git a/src/git/utils/-webview/branch.utils.ts b/src/git/utils/-webview/branch.utils.ts index 44f557e6e712d..79122c609415c 100644 --- a/src/git/utils/-webview/branch.utils.ts +++ b/src/git/utils/-webview/branch.utils.ts @@ -48,7 +48,7 @@ export async function getDefaultBranchName( const integration = await remote.getIntegration(); const defaultBranch = await integration?.getDefaultBranch?.(remote.provider.repoDesc, options); - return `${remote.name}/${defaultBranch?.name}`; + return defaultBranch && `${remote.name}/${defaultBranch?.name}`; } export async function getTargetBranchName( diff --git a/src/plus/ai/aiProviderService.ts b/src/plus/ai/aiProviderService.ts index 6082bf5ab6704..f083199fff2fd 100644 --- a/src/plus/ai/aiProviderService.ts +++ b/src/plus/ai/aiProviderService.ts @@ -518,6 +518,59 @@ export class AIProviderService implements Disposable { return result != null ? { ...result, parsed: parseSummarizeResult(result.content) } : undefined; } + async generatePullRequestMessage( + repo: Repository, + baseRef: string, + compareRef: string, + source: Source, + options?: { + cancellation?: CancellationToken; + context?: string; + generating?: Deferred; + progress?: ProgressOptions; + }, + ): Promise { + if (!(await this.ensureFeatureAccess('generateCreatePullRequest', source))) { + return undefined; + } + + const diff = await repo.git.diff().getDiff?.(compareRef, baseRef, { notation: '...' }); + + const log = await this.container.git.commits(repo.path).getLog(`${baseRef}..${compareRef}`); + const commits: [string, number][] = []; + for (const [_sha, commit] of log?.commits ?? []) { + commits.push([commit.message ?? '', commit.date.getTime()]); + } + + if (!diff?.contents && !commits.length) { + throw new Error('No changes found to generate a pull request message from.'); + } + + const result = await this.sendRequest( + 'generate-create-pullRequest', + () => ({ + diff: diff?.contents ?? '', + data: commits.sort((a, b) => a[1] - b[1]).map(c => c[0]), + context: options?.context ?? '', + instructions: configuration.get('ai.generateCreatePullRequest.customInstructions') ?? '', + }), + m => `Generating pull request details with ${m.name}...`, + source, + m => ({ + key: 'ai/generate', + data: { + type: 'createPullRequest', + 'model.id': m.id, + 'model.provider.id': m.provider.id, + 'model.provider.name': m.provider.name, + 'retry.count': 0, + }, + }), + options, + ); + return result != null ? { ...result, parsed: parseSummarizeResult(result.content) } : undefined; + } + async generateDraftMessage( changesOrRepo: string | string[] | Repository, sourceContext: Source & { type: AIGenerateDraftEventData['draftType'] }, @@ -529,7 +582,7 @@ export class AIProviderService implements Disposable { codeSuggestion?: boolean; }, ): Promise { - if (!(await this.ensureFeatureAccess('cloudPatchGenerateTitleAndDescription', sourceContext))) { + if (!(await this.ensureFeatureAccess('generateCreateDraft', sourceContext))) { return undefined; } @@ -545,8 +598,8 @@ export class AIProviderService implements Disposable { context: options?.context ?? '', instructions: (options?.codeSuggestion - ? configuration.get('ai.generateCodeSuggestMessage.customInstructions') - : configuration.get('ai.generateCloudPatchMessage.customInstructions')) ?? '', + ? configuration.get('ai.generateCreateCodeSuggest.customInstructions') + : configuration.get('ai.generateCreateCloudPatch.customInstructions')) ?? '', }), m => `Generating ${options?.codeSuggestion ? 'code suggestion' : 'cloud patch'} description with ${ diff --git a/src/plus/ai/prompts.ts b/src/plus/ai/prompts.ts index 7de91fa249f41..67c528cda874d 100644 --- a/src/plus/ai/prompts.ts +++ b/src/plus/ai/prompts.ts @@ -49,6 +49,61 @@ Fixes #789 Based on the provided code diff and any additional context, create a concise but meaningful commit message following the instructions above.`; +export const generatePullRequestMessageUserPrompt = `You are an advanced AI programming assistant and are tasked with summarizing code changes into a concise but meaningful pull request title and description. You will be provided with a code diff and a list of commits. Your goal is to analyze the changes and create a clear, informative title and description that accurately represents the modifications made to the code. +First, examine the following code changes provided in Git diff format: +<~~diff~~> +\${diff} + + +Then, review the list of commits to help understand the motivation behind the changes and any relevant background information: +<~~data~~> +\${data} + + +Now, if provided, use this context to understand the motivation behind the changes and any relevant background information: +<~~additional-context~~> +\${context} + + +To create an effective pull request title and description, follow these steps: + +1. Carefully analyze the diff, commit messages, context, focusing on: + - The purpose and rationale of the changes + - Any problems addressed or benefits introduced + - Any significant logic changes or algorithmic improvements +2. Ensure the following when composing the pull request title and description: + - Emphasize the 'why' of the change, its benefits, or the problem it addresses + - Use an informal yet professional tone + - Use a future-oriented manner, third-person singular present tense (e.g., 'Fixes', 'Updates', 'Improves', 'Adds', 'Removes') + - Be clear and concise + - Synthesize only meaningful information from the diff and context + - Avoid outputting code, specific code identifiers, names, or file names unless crucial for understanding + - Avoid repeating information, broad generalities, and unnecessary phrases like "this", "this commit", or "this change" +3. Summarize the main purpose of the changes in a single, concise sentence, which will be the title of your pull request message + - Start with a third-person singular present tense verb + - Limit to 50 characters if possible +4. If necessary, provide a brief explanation of the changes, which will be the body of your pull request message + - Add line breaks for readability and to separate independent ideas + - Focus on the "why" rather than the "what" of the changes. + - Structure the body with markdown bullets and headings for clarity +5. If the changes are related to a specific issue or ticket, include the reference (e.g., "Fixes #123" or "Relates to JIRA-456") at the end of the pull request message. + +Write your title inside tags and your description inside tags and include no other text: + + +Implements user authentication feature + + +Adds login and registration endpoints: +- Updates user model to include password hashing +- Integrates JWT for secure token generation + +Fixes #789 + +\${instructions} + +Based on the provided code diff, commit list, and any additional context, create a concise but meaningful pull request title and body following the instructions above.`; + export const generateStashMessageUserPrompt = `You are an advanced AI programming assistant and are tasked with creating a concise but descriptive stash message. You will be provided with a code diff of uncommitted changes. Your goal is to analyze the changes and create a clear, single-line stash message that accurately represents the work in progress being stashed. First, examine the following code changes provided in Git diff format: diff --git a/src/plus/ai/utils/-webview/ai.utils.ts b/src/plus/ai/utils/-webview/ai.utils.ts index ea0eaf6153134..0050e2e3daa81 100644 --- a/src/plus/ai/utils/-webview/ai.utils.ts +++ b/src/plus/ai/utils/-webview/ai.utils.ts @@ -28,11 +28,13 @@ export function getActionName(action: AIActionType): string { case 'generate-stashMessage': return 'Generate Stash Message'; case 'generate-changelog': - return 'Generate Changelog'; + return 'Generate Changelog (Preview)'; case 'generate-create-cloudPatch': return 'Create Cloud Patch Details'; case 'generate-create-codeSuggestion': return 'Create Code Suggestion Details'; + case 'generate-create-pullRequest': + return 'Create Pull Request Details (Preview)'; case 'explain-changes': return 'Explain Changes'; default: diff --git a/src/plus/ai/utils/-webview/prompt.utils.ts b/src/plus/ai/utils/-webview/prompt.utils.ts index 0696d9e101c14..a7c595e9f8f6b 100644 --- a/src/plus/ai/utils/-webview/prompt.utils.ts +++ b/src/plus/ai/utils/-webview/prompt.utils.ts @@ -8,6 +8,7 @@ import { generateCloudPatchMessageUserPrompt, generateCodeSuggestMessageUserPrompt, generateCommitMessageUserPrompt, + generatePullRequestMessageUserPrompt, generateStashMessageUserPrompt, } from '../../prompts'; @@ -27,7 +28,7 @@ export function getLocalPromptTemplate(action: T, _model }; case 'generate-changelog': return { - name: 'Generate Changelog', + name: 'Generate Changelog (Preview)', template: generateChangelogUserPrompt, variables: ['data', 'instructions'], }; @@ -43,6 +44,12 @@ export function getLocalPromptTemplate(action: T, _model template: generateCodeSuggestMessageUserPrompt, variables: ['diff', 'context', 'instructions'], }; + case 'generate-create-pullRequest': + return { + name: 'Generate Pull Request Details (Preview)', + template: generatePullRequestMessageUserPrompt, + variables: ['diff', 'data', 'context', 'instructions'], + }; case 'explain-changes': return { name: 'Explain Changes', diff --git a/src/quickpicks/remoteProviderPicker.ts b/src/quickpicks/remoteProviderPicker.ts index 0958081d274f5..71d2c00bafb60 100644 --- a/src/quickpicks/remoteProviderPicker.ts +++ b/src/quickpicks/remoteProviderPicker.ts @@ -71,7 +71,10 @@ export class CopyOrOpenRemoteCommandQuickPickItem extends CommandQuickPickItem { resource = { ...resource, - base: { branch: branch, remote: { path: this.remote.path, url: this.remote.url } }, + base: { + branch: branch, + remote: { path: this.remote.path, url: this.remote.url, name: this.remote.name }, + }, }; if ( diff --git a/src/webviews/apps/plus/home/components/branch-card.ts b/src/webviews/apps/plus/home/components/branch-card.ts index a4ea1e04e68a6..36e83bfab9e75 100644 --- a/src/webviews/apps/plus/home/components/branch-card.ts +++ b/src/webviews/apps/plus/home/components/branch-card.ts @@ -163,10 +163,34 @@ export const branchCardStyles = css` margin-inline-end: auto; } + .branch-item__row { + display: flex; + gap: 0.8rem; + container-type: inline-size; + contain: layout; + } + + .branch-item__row [full] { + flex-grow: 1; + } + .branch-item__missing { --button-foreground: inherit; } + .branch-item__is-narrow { + display: none; + } + + @container (max-width: 330px) { + .branch-item__is-narrow { + display: block; + } + .branch-item__is-wide { + display: none; + } + } + :host-context(.vscode-dark) .branch-item__missing, :host-context(.vscode-high-contrast) .branch-item__missing { --button-background: color-mix(in lab, var(--vscode-sideBar-background) 100%, #fff 3%); @@ -640,7 +664,7 @@ export abstract class GlBranchCardBase extends GlElement { } protected createCommandLink(command: GlCommands, args?: T | any): string { - return createCommandLink(command, args ?? this.branchRef); + return createCommandLink(command, args ? { ...args, ...this.branchRef } : this.branchRef); } protected renderTimestamp(): TemplateResult | NothingType { @@ -715,13 +739,30 @@ export abstract class GlBranchCardBase extends GlElement { if (!this.pr) { if (this.branch.upstream?.missing === false && this.expanded) { return html` - Create a Pull Request +
+ Create a Pull Request + ${this.branch.aiPullRequestCreationAvailable + ? html` + + + Create with AI + ` + : nothing} +
`; } return nothing; diff --git a/src/webviews/home/homeWebview.ts b/src/webviews/home/homeWebview.ts index f09807b4dd7c9..91ebaf5bfb4e1 100644 --- a/src/webviews/home/homeWebview.ts +++ b/src/webviews/home/homeWebview.ts @@ -12,7 +12,7 @@ import { supportedCloudIntegrationDescriptors, supportedOrderedCloudIntegrationIds, } from '../../constants.integrations'; -import type { HomeTelemetryContext, Source } from '../../constants.telemetry'; +import type { HomeTelemetryContext, Source, Sources } from '../../constants.telemetry'; import type { Container } from '../../container'; import { executeGitCommand } from '../../git/actions'; import { openComparisonChanges } from '../../git/actions/commit'; @@ -29,6 +29,7 @@ import { RepositoryChange, RepositoryChangeComparisonMode } from '../../git/mode import { uncommitted } from '../../git/models/revision'; import type { GitStatus } from '../../git/models/status'; import type { GitWorktree } from '../../git/models/worktree'; +import { remotesSupportTitleOnPullRequestCreation } from '../../git/remotes/remoteProvider'; import { getAssociatedIssuesForBranch } from '../../git/utils/-webview/branch.issue.utils'; import { getBranchTargetInfo } from '../../git/utils/-webview/branch.utils'; import { getReferenceFromBranch } from '../../git/utils/-webview/reference.utils'; @@ -756,10 +757,15 @@ export class HomeWebviewProvider implements WebviewProvider this.getBranchOverviewType(branch, worktreesByBranch) === 'active', )!; + const aiPullRequestCreationAvailable = + (await repo.git.remotes().getBestRemotesWithProviders()).find(r => + remotesSupportTitleOnPullRequestCreation.includes(r.provider.id), + ) != null; const isPro = await this.isSubscriptionPro(); const [activeOverviewBranch] = getOverviewBranchesCore( [activeBranch], + aiPullRequestCreationAvailable, branchesAndWorktrees.worktreesByBranch, isPro, this.container, @@ -791,6 +797,10 @@ export class HomeWebviewProvider implements WebviewProvider + remotesSupportTitleOnPullRequestCreation.includes(r.provider.id), + ) != null; const recentBranches = branchesAndWorktrees.branches.filter( branch => this.getBranchOverviewType(branch, branchesAndWorktrees.worktreesByBranch) === 'recent', @@ -823,6 +833,7 @@ export class HomeWebviewProvider implements WebviewProvider({ args: { 0: r => `${r.branchId}, upstream: ${r.branchUpstreamName}` }, }) - private async pullRequestCreate(ref: BranchRef) { + private async pullRequestCreate(ref: BranchRef & { source?: Sources; useAI?: boolean }) { const { branch } = await this.getRepoInfoFromRef(ref); if (branch == null) return; @@ -1374,6 +1391,8 @@ export class HomeWebviewProvider implements WebviewProvider, isPro: boolean, container: Container, @@ -1516,6 +1536,7 @@ function getOverviewBranchesCore( reference: getReferenceFromBranch(branch), repoPath: branch.repoPath, id: branch.id, + aiPullRequestCreationAvailable: aiPullRequestCreationAvailable, name: branch.name, opened: isActive, timestamp: timestamp, diff --git a/src/webviews/home/protocol.ts b/src/webviews/home/protocol.ts index dad3d659e6bcb..bb62e3d8bbebc 100644 --- a/src/webviews/home/protocol.ts +++ b/src/webviews/home/protocol.ts @@ -74,6 +74,7 @@ export interface GetOverviewBranch { id: string; name: string; opened: boolean; + aiPullRequestCreationAvailable: boolean; timestamp?: number; status: GitBranchStatus; upstream: GitTrackingUpstream | undefined;