From 9cc68f7c88da603de36b74126b9e398c7deaa2d9 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 21 Oct 2025 10:38:32 +0200 Subject: [PATCH 001/402] Bump playwright from 1.54.1 to 1.56.1 (#8026) Bumps [playwright](https://github.com/microsoft/playwright) from 1.54.1 to 1.56.1. - [Release notes](https://github.com/microsoft/playwright/releases) - [Commits](https://github.com/microsoft/playwright/compare/v1.54.1...v1.56.1) --- updated-dependencies: - dependency-name: playwright dependency-version: 1.56.1 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 44 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 37 insertions(+), 7 deletions(-) diff --git a/yarn.lock b/yarn.lock index d748175626..4a5f8597e8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5899,12 +5899,17 @@ playwright-core@1.54.1: resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.54.1.tgz#d32edcce048c9d83ceac31e294a7b60ef586960b" integrity sha512-Nbjs2zjj0htNhzgiy5wu+3w09YetDx5pkrpI/kZotDlDUaYk0HVA5xrBVPdow4SAUIlhgKcJeJg4GRKW6xHusA== +playwright-core@1.56.1: + version "1.56.1" + resolved "https://registry.yarnpkg.com/playwright-core/-/playwright-core-1.56.1.tgz#24a66481e5cd33a045632230aa2c4f0cb6b1db3d" + integrity sha512-hutraynyn31F+Bifme+Ps9Vq59hKuUCz7H1kDOcBs+2oGguKkWTU50bBWrtz34OUWmIwpBTWDxaRPXrIXkgvmQ== + playwright@^1.53.1: - version "1.54.1" - resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.54.1.tgz#128d66a8d5182b5330e6440be3a72ca313362788" - integrity sha512-peWpSwIBmSLi6aW2auvrUtf2DqY16YYcCMO8rTVx486jKmDTJg7UAhyrraP98GB8BoPURZP8+nxO7TSd4cPr5g== + version "1.56.1" + resolved "https://registry.yarnpkg.com/playwright/-/playwright-1.56.1.tgz#62e3b99ddebed0d475e5936a152c88e68be55fbf" + integrity sha512-aFi5B0WovBHTEvpM3DzXTUaeN6eN0qWnTkKx4NQaH4Wvcmc153PdaY2UBdSYKaGYw+UyWXSVyxDUg5DoPEttjw== dependencies: - playwright-core "1.54.1" + playwright-core "1.56.1" optionalDependencies: fsevents "2.3.2" @@ -6776,7 +6781,16 @@ streamx@^2.15.0, streamx@^2.21.0: optionalDependencies: bare-events "^2.2.0" -"string-width-cjs@npm:string-width@^4.2.0", string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0, string-width@^4.2.0, string-width@^4.2.3: version "4.2.3" resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== @@ -6866,7 +6880,7 @@ stringify-entities@^4.0.0: character-entities-html4 "^2.0.0" character-entities-legacy "^3.0.0" -"strip-ansi-cjs@npm:strip-ansi@^6.0.1", strip-ansi@^6.0.0, strip-ansi@^6.0.1: +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== @@ -6880,6 +6894,13 @@ strip-ansi@^4.0.0: dependencies: ansi-regex "^3.0.0" +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + strip-ansi@^7.0.1: version "7.1.2" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.2.tgz#132875abde678c7ea8d691533f2e7e22bb744dba" @@ -7890,7 +7911,16 @@ workerpool@^9.2.0: resolved "https://registry.yarnpkg.com/workerpool/-/workerpool-9.3.4.tgz#f6c92395b2141afd78e2a889e80cb338fe9fca41" integrity sha512-TmPRQYYSAnnDiEB0P/Ytip7bFGvqnSU6I2BcuSw7Hx+JSg/DsUi5ebYfc8GYaSdpuvOcEs6dXxPurOYpe9QFwg== -"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0", wrap-ansi@^7.0.0: +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== From e7dd890e39ea69052d317fb0da54a72f9c3d061d Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Wed, 22 Oct 2025 16:00:20 -0700 Subject: [PATCH 002/402] contribute chat/multiDiff/context to chatSessionType == copilot-cloud-agent (#8033) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index baa8497a14..16531290d3 100644 --- a/package.json +++ b/package.json @@ -3551,11 +3551,11 @@ "chat/multiDiff/context": [ { "command": "pr.checkoutFromDescription", - "when": "chatSessionType == copilot-swe-agent" + "when": "chatSessionType == copilot-swe-agent || chatSessionType == 'copilot-cloud-agent'" }, { "command": "pr.applyChangesFromDescription", - "when": "chatSessionType == copilot-swe-agent" + "when": "chatSessionType == copilot-swe-agent || chatSessionType == 'copilot-cloud-agent'" } ] }, From 8a5c9a145cd80ee364a3bed9cf616b2bd8ac74c2 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega <48293249+osortega@users.noreply.github.com> Date: Wed, 22 Oct 2025 16:35:53 -0700 Subject: [PATCH 003/402] Adding inline commands to the new migrated CCA (#8034) --- package.json | 8 ++++---- src/commands.ts | 43 +++++++++++++++++++++++++++++++++------- src/github/copilotApi.ts | 18 +++++++++++++++++ 3 files changed, 58 insertions(+), 11 deletions(-) diff --git a/package.json b/package.json index 16531290d3..f5df22a806 100644 --- a/package.json +++ b/package.json @@ -3529,22 +3529,22 @@ "chat/chatSessions": [ { "command": "pr.openChanges", - "when": "chatSessionType == copilot-swe-agent", + "when": "chatSessionType == copilot-swe-agent || chatSessionType == copilot-cloud-agent", "group": "inline" }, { "command": "pr.checkoutChatSessionPullRequest", - "when": "chatSessionType == copilot-swe-agent", + "when": "chatSessionType == copilot-swe-agent || chatSessionType == copilot-cloud-agent", "group": "context" }, { "command": "pr.closeChatSessionPullRequest", - "when": "chatSessionType == copilot-swe-agent", + "when": "chatSessionType == copilot-swe-agent || chatSessionType == copilot-cloud-agent", "group": "context" }, { "command": "pr.cancelCodingAgent", - "when": "chatSessionType == copilot-swe-agent", + "when": "chatSessionType == copilot-swe-agent || chatSessionType == copilot-cloud-agent", "group": "context" } ], diff --git a/src/commands.ts b/src/commands.ts index 0e5c49db0d..be11035096 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -19,7 +19,7 @@ import { asTempStorageURI, fromPRUri, fromReviewUri, Schemes, toPRUri } from './ import { formatError } from './common/utils'; import { EXTENSION_ID } from './constants'; import { ICopilotRemoteAgentCommandArgs } from './github/common'; -import { ChatSessionWithPR } from './github/copilotApi'; +import { ChatSessionWithPR, CrossChatSessionWithPR } from './github/copilotApi'; import { CopilotRemoteAgentManager } from './github/copilotRemoteAgent'; import { FolderRepositoryManager } from './github/folderRepositoryManager'; import { GitHubRepository } from './github/githubRepository'; @@ -198,6 +198,11 @@ function isChatSessionWithPR(value: any): value is ChatSessionWithPR { return !!asChatSessionWithPR.pullRequest; } +function isCrossChatSessionWithPR(value: any): value is CrossChatSessionWithPR { + const asCrossChatSessionWithPR = value as Partial; + return !!asCrossChatSessionWithPR.pullRequestDetails; +} + export function registerCommands( context: vscode.ExtensionContext, reposManager: RepositoriesManager, @@ -700,6 +705,14 @@ export function registerCommands( pullRequestModel = pr; } else if (isChatSessionWithPR(pr)) { pullRequestModel = pr.pullRequest; + } else if (isCrossChatSessionWithPR(pr)) { + const resolved = await resolvePr({ + owner: pr.pullRequestDetails.repository.owner.login, + repo: pr.pullRequestDetails.repository.name, + number: pr.pullRequestDetails.number, + preventDefaultContextMenuItems: true, + }); + pullRequestModel = resolved?.pr; } else { const resolved = await resolvePr(pr as OverviewContext); pullRequestModel = resolved?.pr; @@ -963,8 +976,14 @@ export function registerCommands( await openDescription(telemetry, issueModel, descriptionNode, folderManager, revealDescription, !(argument instanceof RepositoryChangesNode)); } - async function checkoutChatSessionPullRequest(argument: ChatSessionWithPR) { - const pr = argument.pullRequest; + async function checkoutChatSessionPullRequest(argument: ChatSessionWithPR | CrossChatSessionWithPR) { + const pr = isChatSessionWithPR(argument) ? argument.pullRequest : await resolvePr({ + owner: argument.pullRequestDetails.repository.owner.login, + repo: argument.pullRequestDetails.repository.name, + number: argument.pullRequestDetails.number, + preventDefaultContextMenuItems: true, + }).then(resolved => resolved?.pr); + if (!pr) { Logger.warn(`No pull request found in chat session`, logId); return; @@ -979,8 +998,13 @@ export function registerCommands( return switchToPr(folderManager, pr, folderManager.repository, false); } - async function closeChatSessionPullRequest(argument: ChatSessionWithPR) { - const pr = argument.pullRequest; + async function closeChatSessionPullRequest(argument: ChatSessionWithPR | CrossChatSessionWithPR) { + const pr = isChatSessionWithPR(argument) ? argument.pullRequest : await resolvePr({ + owner: argument.pullRequestDetails.repository.owner.login, + repo: argument.pullRequestDetails.repository.name, + number: argument.pullRequestDetails.number, + preventDefaultContextMenuItems: true, + }).then(resolved => resolved?.pr); if (!pr) { Logger.warn(`No pull request found in chat session`, logId); return; @@ -989,8 +1013,13 @@ export function registerCommands( copilotRemoteAgentManager.refreshChatSessions(); } - async function cancelCodingAgent(argument: ChatSessionWithPR) { - const pr = argument.pullRequest; + async function cancelCodingAgent(argument: ChatSessionWithPR | CrossChatSessionWithPR) { + const pr = isChatSessionWithPR(argument) ? argument.pullRequest : await resolvePr({ + owner: argument.pullRequestDetails.repository.owner.login, + repo: argument.pullRequestDetails.repository.name, + number: argument.pullRequestDetails.number, + preventDefaultContextMenuItems: true, + }).then(resolved => resolved?.pr); if (!pr) { Logger.warn(`No pull request found in chat session`, logId); return; diff --git a/src/github/copilotApi.ts b/src/github/copilotApi.ts index 159cf38a2f..499ba2a253 100644 --- a/src/github/copilotApi.ts +++ b/src/github/copilotApi.ts @@ -52,6 +52,24 @@ export interface ChatSessionWithPR extends vscode.ChatSessionItem { pullRequest: PullRequestModel; } + +/** + * This is temporary for the migration of CCA only. + * Once fully migrated we can rename to ChatSessionWithPR and remove the old one. + **/ +export interface CrossChatSessionWithPR extends vscode.ChatSessionItem { + pullRequestDetails: { + id: string; + number: number; + repository: { + owner: { + login: string; + }; + name: string; + }; + }; +} + export class CopilotApi { protected static readonly ID = 'copilotApi'; From 45289d35ac19f1fb32b2e3ae22d8b46071701f8c Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Thu, 23 Oct 2025 13:30:44 -0700 Subject: [PATCH 004/402] improve apply changes notification (#8035) --- src/commands.ts | 70 +++++++++++++++++++++++++++++-------------------- 1 file changed, 42 insertions(+), 28 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index be11035096..441f949444 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -571,7 +571,7 @@ export function registerCommands( return { folderManager, pr }; }; - const applyPullRequestChanges = async (folderManager: FolderRepositoryManager, pullRequest: PullRequestModel): Promise => { + const applyPullRequestChanges = async (task: vscode.Progress<{ message?: string; increment?: number; }>, folderManager: FolderRepositoryManager, pullRequest: PullRequestModel): Promise => { let patch: string | undefined; try { patch = await pullRequest.getPatch(); @@ -589,22 +589,13 @@ export function registerCommands( const encoder = new TextEncoder(); const tempUri = vscode.Uri.file(tempFilePath); - await vscode.window.withProgress( - { - location: vscode.ProgressLocation.Notification, - title: vscode.l10n.t('Applying changes from pull request #{0}', pullRequest.number.toString()), - cancellable: false - }, - async (task) => { - await vscode.workspace.fs.writeFile(tempUri, encoder.encode(patch)); - try { - await folderManager.repository.apply(tempFilePath, false); - task.report({ message: vscode.l10n.t('Successfully applied changes from pull request #{0}', pullRequest.number.toString()), increment: 100 }); - } finally { - await vscode.workspace.fs.delete(tempUri); - } - } - ); + await vscode.workspace.fs.writeFile(tempUri, encoder.encode(patch)); + try { + await folderManager.repository.apply(tempFilePath, false); + task.report({ message: vscode.l10n.t('Successfully applied changes from pull request #{0}', pullRequest.number.toString()), increment: 100 }); + } finally { + await vscode.workspace.fs.delete(tempUri); + } } catch (error) { const errorMessage = formatError(error); @@ -672,21 +663,44 @@ export function registerCommands( if (Number.isNaN(prNumber)) { return vscode.window.showErrorMessage(vscode.l10n.t('Unable to parse pull request number.')); } - const folderManager = reposManager.folderManagers[0]; - const pullRequest = await folderManager.fetchById(folderManager.gitHubRepositories[0], Number(prNumber)); - if (!pullRequest) { - return vscode.window.showErrorMessage(vscode.l10n.t('Unable to find pull request #{0}', prNumber.toString())); - } - return applyPullRequestChanges(folderManager, pullRequest); - } + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: vscode.l10n.t('Applying changes from pull request #{0}', prNumber.toString()), + cancellable: false + }, + async (task) => { + task.report({ increment: 30 }); - const resolved = await resolvePr(ctx); - if (!resolved) { - return vscode.window.showErrorMessage(vscode.l10n.t('Unable to resolve pull request for applying changes.')); + const folderManager = reposManager.folderManagers[0]; + const pullRequest = await folderManager.fetchById(folderManager.gitHubRepositories[0], Number(prNumber)); + if (!pullRequest) { + return vscode.window.showErrorMessage(vscode.l10n.t('Unable to find pull request #{0}', prNumber.toString())); + } + + return applyPullRequestChanges(task, folderManager, pullRequest); + }); + + return; } - return applyPullRequestChanges(resolved.folderManager, resolved.pr); + await vscode.window.withProgress( + { + location: vscode.ProgressLocation.Notification, + title: vscode.l10n.t('Applying changes from pull request'), + cancellable: false + }, + async (task) => { + task.report({ increment: 30 }); + + const resolved = await resolvePr(ctx); + if (!resolved) { + return vscode.window.showErrorMessage(vscode.l10n.t('Unable to resolve pull request for applying changes.')); + } + return applyPullRequestChanges(task, resolved.folderManager, resolved.pr); + } + ); })); context.subscriptions.push( From d3bec0d86ecc5720b0b02d1603454f31411a3aa7 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Thu, 23 Oct 2025 13:49:56 -0700 Subject: [PATCH 005/402] support https://github.com/microsoft/vscode-copilot-chat/pull/1516 (#8036) --- package.json | 2 +- src/commands.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index f5df22a806..ea732b5576 100644 --- a/package.json +++ b/package.json @@ -3539,7 +3539,7 @@ }, { "command": "pr.closeChatSessionPullRequest", - "when": "chatSessionType == copilot-swe-agent || chatSessionType == copilot-cloud-agent", + "when": "chatSessionType == copilot-swe-agent", "group": "context" }, { diff --git a/src/commands.ts b/src/commands.ts index 441f949444..9ee47145c6 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -1023,7 +1023,7 @@ export function registerCommands( Logger.warn(`No pull request found in chat session`, logId); return; } - pr.close(); + await pr.close(); copilotRemoteAgentManager.refreshChatSessions(); } From f1194f7e77437596dada10f7999f9777244bb024 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Thu, 23 Oct 2025 15:33:39 -0700 Subject: [PATCH 006/402] Support for resource based session API changes (#8027) * Initial support for new resource based chat sessions * Update tests * Bump proposal version --------- Co-authored-by: Josh Spicer <23246594+joshspicer@users.noreply.github.com> --- package.json | 4 +- ...ode.proposed.chatParticipantAdditions.d.ts | 1 + .../vscode.proposed.chatSessionsProvider.d.ts | 102 +++++++++++------- src/extension.ts | 10 +- src/github/copilotRemoteAgent.ts | 36 ++++--- src/github/pullRequestOverview.ts | 2 +- src/test/github/copilotRemoteAgent.test.ts | 9 +- 7 files changed, 99 insertions(+), 65 deletions(-) diff --git a/package.json b/package.json index ea732b5576..0a08a52975 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "activeComment", "chatParticipantAdditions", "chatParticipantPrivate", - "chatSessionsProvider@2", + "chatSessionsProvider@3", "codiconDecoration", "codeActionRanges", "commentingRangeHint", @@ -4364,4 +4364,4 @@ "string_decoder": "^1.3.0" }, "license": "MIT" -} +} \ No newline at end of file diff --git a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts index 778d6d7a40..c0b38dffc4 100644 --- a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts @@ -652,6 +652,7 @@ declare module 'vscode' { } export interface ChatRequestModeInstructions { + readonly name: string; readonly content: string; readonly toolReferences?: readonly ChatLanguageModelToolReference[]; readonly metadata?: Record; diff --git a/src/@types/vscode.proposed.chatSessionsProvider.d.ts b/src/@types/vscode.proposed.chatSessionsProvider.d.ts index 534c45a192..3e7c795fbe 100644 --- a/src/@types/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/@types/vscode.proposed.chatSessionsProvider.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -// version: 2 +// version: 3 declare module 'vscode' { /** @@ -35,6 +35,14 @@ declare module 'vscode' { */ readonly onDidChangeChatSessionItems: Event; + /** + * Provides a list of chat sessions. + */ + // TODO: Do we need a flag to try auth if needed? + provideChatSessionItems(token: CancellationToken): ProviderResult; + + // #region Unstable parts of API + /** * Event that the provider can fire to signal that the current (original) chat session should be replaced with a new (modified) chat session. * The UI can use this information to gracefully migrate the user to the new session. @@ -61,27 +69,16 @@ declare module 'vscode' { metadata?: any; }, token: CancellationToken): ProviderResult; - /** - * Provides a list of chat sessions. - */ - // TODO: Do we need a flag to try auth if needed? - provideChatSessionItems(token: CancellationToken): ProviderResult; + // #endregion } export interface ChatSessionItem { - /** - * Unique identifier for the chat session. - * - * @deprecated Will be replaced by `resource` - */ - id: string; - /** * The resource associated with the chat session. * * This is uniquely identifies the chat session and is used to open the chat session. */ - resource: Uri | undefined; + resource: Uri; /** * Human readable name of the session shown in the UI @@ -149,9 +146,12 @@ declare module 'vscode' { readonly history: ReadonlyArray; /** - * Options configured for this session. + * Options configured for this session as key-value pairs. + * Keys correspond to option group IDs (e.g., 'models', 'subagents') + * and values are the selected option item IDs. + * TODO: Strongly type the keys */ - readonly options?: { model?: LanguageModelChatInformation }; + readonly options?: Record; /** * Callback invoked by the editor for a currently running response. This allows the session to push items for the @@ -172,22 +172,28 @@ declare module 'vscode' { readonly requestHandler: ChatRequestHandler | undefined; } + /** + * Provides the content for a chat session rendered using the native chat UI. + */ export interface ChatSessionContentProvider { /** - * Resolves a chat session into a full `ChatSession` object. + * Provides the chat session content for a given uri. * - * @param sessionId The id of the chat session to open. + * The returned {@linkcode ChatSession} is used to populate the history of the chat UI. + * + * @param resource The URI of the chat session to resolve. * @param token A cancellation token that can be used to cancel the operation. + * + * @return The {@link ChatSession chat session} associated with the given URI. */ - provideChatSessionContent(sessionId: string, token: CancellationToken): Thenable | ChatSession; + provideChatSessionContent(resource: Uri, token: CancellationToken): Thenable | ChatSession; /** - * - * @param sessionId Identifier of the chat session being updated. + * @param resource Identifier of the chat session being updated. * @param updates Collection of option identifiers and their new values. Only the options that changed are included. * @param token A cancellation token that can be used to cancel the notification if the session is disposed. */ - provideHandleOptionsChange?(sessionId: string, updates: ReadonlyArray, token: CancellationToken): void; + provideHandleOptionsChange?(resource: Uri, updates: ReadonlyArray, token: CancellationToken): void; /** * Called as soon as you register (call me once) @@ -224,12 +230,12 @@ declare module 'vscode' { /** * Registers a new {@link ChatSessionContentProvider chat session content provider}. * - * @param chatSessionType A unique identifier for the chat session type. This is used to differentiate between different chat session providers. + * @param scheme The uri-scheme to register for. This must be unique. * @param provider The provider to register. * * @returns A disposable that unregisters the provider when disposed. */ - export function registerChatSessionContentProvider(chatSessionType: string, provider: ChatSessionContentProvider, chatParticipant: ChatParticipant, capabilities?: ChatSessionCapabilities): Disposable; + export function registerChatSessionContentProvider(scheme: string, provider: ChatSessionContentProvider, chatParticipant: ChatParticipant, capabilities?: ChatSessionCapabilities): Disposable; } export interface ChatContext { @@ -252,31 +258,51 @@ declare module 'vscode' { supportsInterruptions?: boolean; } - export interface ChatSessionProviderOptions { + /** + * Represents a single selectable item within a provider option group. + */ + export interface ChatSessionProviderOptionItem { + /** + * Unique identifier for the option item. + */ + readonly id: string; + /** - * Set of available models. + * Human-readable name displayed in the UI. */ - models?: LanguageModelChatInformation[]; + readonly name: string; } /** - * @deprecated + * Represents a group of related provider options (e.g., models, sub-agents). */ - export interface ChatSessionShowOptions { + export interface ChatSessionProviderOptionGroup { /** - * The editor view column to show the chat session in. - * - * If not provided, the chat session will be shown in the chat panel instead. + * Unique identifier for the option group (e.g., "models", "subagents"). */ - readonly viewColumn?: ViewColumn; + readonly id: string; + + /** + * Human-readable name for the option group. + */ + readonly name: string; + + /** + * Optional description providing context about this option group. + */ + readonly description?: string; + + /** + * The selectable items within this option group. + */ + readonly items: ChatSessionProviderOptionItem[]; } - export namespace window { + export interface ChatSessionProviderOptions { /** - * Shows a chat session in the panel or editor. - * - * @deprecated + * Provider-defined option groups (0-2 groups supported). + * Examples: models picker, sub-agents picker, etc. */ - export function showChatSession(chatSessionType: string, sessionId: string, options: ChatSessionShowOptions): Thenable; + optionGroups?: ChatSessionProviderOptionGroup[]; } } diff --git a/src/extension.ts b/src/extension.ts index 251c7451a6..31e8013536 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -447,12 +447,12 @@ async function deferredActivate(context: vscode.ExtensionContext, showPRControll const provider = new class implements vscode.ChatSessionContentProvider, vscode.ChatSessionItemProvider { label = vscode.l10n.t('GitHub Copilot Coding Agent'); - provideChatSessionItems = async (token) => { + async provideChatSessionItems(token: vscode.CancellationToken) { return await copilotRemoteAgentManager.provideChatSessions(token); - }; - provideChatSessionContent = async (id, token) => { - return await copilotRemoteAgentManager.provideChatSessionContent(id, token); - }; + } + async provideChatSessionContent(resource: vscode.Uri, token: vscode.CancellationToken) { + return await copilotRemoteAgentManager.provideChatSessionContent(resource, token); + } onDidChangeChatSessionItems = copilotRemoteAgentManager.onDidChangeChatSessions; onDidCommitChatSessionItem = copilotRemoteAgentManager.onDidCommitChatSession; }(); diff --git a/src/github/copilotRemoteAgent.ts b/src/github/copilotRemoteAgent.ts index d4d6437382..ff5d2dfd17 100644 --- a/src/github/copilotRemoteAgent.ts +++ b/src/github/copilotRemoteAgent.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as pathLib from 'path'; +import { URI } from '@vscode/prompt-tsx/dist/base/util/vs/common/uri'; import * as marked from 'marked'; import vscode, { ChatPromptReference, ChatSessionItem } from 'vscode'; import { copilotPRStatusToSessionStatus, IAPISessionLogs, ICopilotRemoteAgentCommandArgs, ICopilotRemoteAgentCommandResponse, OctokitCommon, RemoteAgentResult, RepoInfo } from './common'; @@ -56,12 +57,14 @@ export namespace SessionIdForPr { const prefix = 'pull-session-by-index'; - export function getId(prNumber: number, sessionIndex: number): string { - return `${prefix}-${prNumber}-${sessionIndex}`; + export function getResource(prNumber: number, sessionIndex: number): vscode.Uri { + return vscode.Uri.from({ + scheme: COPILOT_SWE_AGENT, path: `/${prefix}-${prNumber}-${sessionIndex}`, + }); } - export function parse(id: string): { prNumber: number; sessionIndex: number } | undefined { - const match = id.match(new RegExp(`^${prefix}-(\\d+)-(\\d+)$`)); + export function parse(resource: vscode.Uri): { prNumber: number; sessionIndex: number } | undefined { + const match = resource.path.match(new RegExp(`^/${prefix}-(\\d+)-(\\d+)$`)); if (match) { return { prNumber: parseInt(match[1], 10), @@ -141,7 +144,7 @@ export class CopilotRemoteAgentManager extends Disposable { const card = new vscode.ChatResponsePullRequestPart(uri, pullRequest.title, plaintextBody, pullRequest.author.specialDisplayName ?? pullRequest.author.login, `#${pullRequest.number}`); stream.push(card); stream.markdown(vscode.l10n.t('GitHub Copilot coding agent has begun working on your request. Follow its progress in the associated chat and pull request.')); - vscode.window.showChatSession(COPILOT_SWE_AGENT, String(number), { viewColumn: vscode.ViewColumn.Active }); + vscode.commands.executeCommand('vscode.open', vscode.Uri.from({ scheme: COPILOT_SWE_AGENT, path: '/' + number }), { viewColumn: vscode.ViewColumn.Active }); break; default: stream.warning(`Unknown confirmation step: ${data.step}\n\n`); @@ -167,7 +170,10 @@ export class CopilotRemoteAgentManager extends Disposable { return {}; } // Tell UI to the new chat session - const modified: vscode.ChatSessionItem = { id: String(number), label: `Pull Request ${number}` } as unknown as vscode.ChatSessionItem; + const modified: vscode.ChatSessionItem = { + resource: vscode.Uri.from({ scheme: COPILOT_SWE_AGENT, path: '/' + number }), + label: `Pull Request ${number}`, + }; this._onDidCommitChatSession.fire({ original: context.chatSessionContext.chatSessionItem, modified }); } else if (context.chatSessionContext) { /* Follow up to an existing coding agent session */ @@ -185,9 +191,9 @@ export class CopilotRemoteAgentManager extends Disposable { stream.progress(vscode.l10n.t('Preparing')); - const pullRequest = await this.findPullRequestById(parseInt(context.chatSessionContext.chatSessionItem.id, 10), true); + const pullRequest = await this.findPullRequestById(parseInt(context.chatSessionContext.chatSessionItem.resource.path.slice(1), 10), true); if (!pullRequest) { - stream.warning(vscode.l10n.t('Could not find the associated pull request {0} for this chat session.', context.chatSessionContext.chatSessionItem.id)); + stream.warning(vscode.l10n.t('Could not find the associated pull request {0} for this chat session.', context.chatSessionContext.chatSessionItem.resource.toString)); return {}; } @@ -705,7 +711,7 @@ export class CopilotRemoteAgentManager extends Disposable { } else { await this.provideChatSessions(new vscode.CancellationTokenSource().token); if (pr) { - vscode.window.showChatSession(COPILOT_SWE_AGENT, `${pr.number}`, {}); + vscode.commands.executeCommand('vscode.open', vscode.Uri.from({ scheme: COPILOT_SWE_AGENT, path: '/' + pr.number })); } } @@ -1060,7 +1066,7 @@ export class CopilotRemoteAgentManager extends Disposable { } const description = new vscode.MarkdownString(`[${repoInfo}#${pullRequest.number}](${uri.toString()} "${prLinkTitle}")`); // pullRequest.base.ref === defaultBranch ? `PR #${pullRequest.number}`: `PR #${pullRequest.number} → ${pullRequest.base.ref}`; const chatSession: ChatSessionWithPR = { - id: `${pullRequest.number}`, + resource: vscode.Uri.from({ scheme: COPILOT_SWE_AGENT, path: '/' + pullRequest.number }), label: pullRequest.title || `Session ${pullRequest.number}`, iconPath: this.getIconForSession(status), pullRequest: pullRequest, @@ -1074,7 +1080,7 @@ export class CopilotRemoteAgentManager extends Disposable { insertions: pullRequest.item.additions, deletions: pullRequest.item.deletions } : undefined - } as unknown as ChatSessionWithPR; + }; return chatSession; })); } catch (error) { @@ -1083,7 +1089,7 @@ export class CopilotRemoteAgentManager extends Disposable { return []; } - public async provideChatSessionContent(id: string, token: vscode.CancellationToken): Promise { + public async provideChatSessionContent(resource: URI, token: vscode.CancellationToken): Promise { try { const capi = await this.copilotApi; if (!capi || token.isCancellationRequested) { @@ -1095,16 +1101,16 @@ export class CopilotRemoteAgentManager extends Disposable { let pullRequestNumber: number | undefined; let sessionIndex: number | undefined; - const indexedSessionId = SessionIdForPr.parse(id); + const indexedSessionId = SessionIdForPr.parse(resource); if (indexedSessionId) { pullRequestNumber = indexedSessionId.prNumber; sessionIndex = indexedSessionId.sessionIndex; } if (typeof pullRequestNumber === 'undefined') { - pullRequestNumber = parseInt(id); + pullRequestNumber = parseInt(resource.path.slice(1)); if (isNaN(pullRequestNumber)) { - Logger.error(`Invalid pull request number: ${id}`, CopilotRemoteAgentManager.ID); + Logger.error(`Invalid pull request number: ${resource}`, CopilotRemoteAgentManager.ID); return this.createEmptySession(); } } diff --git a/src/github/pullRequestOverview.ts b/src/github/pullRequestOverview.ts index 74d71b35f8..47dbe5ce23 100644 --- a/src/github/pullRequestOverview.ts +++ b/src/github/pullRequestOverview.ts @@ -509,7 +509,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel): Promise { try { - return vscode.window.showChatSession(COPILOT_SWE_AGENT, SessionIdForPr.getId(this._item.number, message.args.link.sessionIndex), {}); + return vscode.commands.executeCommand('vscode.open', COPILOT_SWE_AGENT, SessionIdForPr.getResource(this._item.number, message.args.link.sessionIndex)); } catch (e) { Logger.error(`Open session log view failed: ${formatError(e)}`, PullRequestOverviewPanel.ID); } diff --git a/src/test/github/copilotRemoteAgent.test.ts b/src/test/github/copilotRemoteAgent.test.ts index b18a6301b0..f0618b8bb1 100644 --- a/src/test/github/copilotRemoteAgent.test.ts +++ b/src/test/github/copilotRemoteAgent.test.ts @@ -6,7 +6,7 @@ import { default as assert } from 'assert'; import { SinonSandbox, createSandbox } from 'sinon'; import * as vscode from 'vscode'; -import { CopilotRemoteAgentManager } from '../../github/copilotRemoteAgent'; +import { CopilotRemoteAgentManager, SessionIdForPr } from '../../github/copilotRemoteAgent'; import { MockCommandRegistry } from '../mocks/mockCommandRegistry'; import { MockTelemetry } from '../mocks/mockTelemetry'; import { CredentialStore } from '../../github/credentials'; @@ -21,6 +21,7 @@ import { ReposManagerState } from '../../github/folderRepositoryManager'; import { GitApiImpl } from '../../api/api1'; import { MockPrsTreeModel } from '../mocks/mockPRsTreeModel'; import { PrsTreeModel } from '../../view/prsTreeModel'; +import { COPILOT_SWE_AGENT } from '../../common/copilot'; const telemetry = new MockTelemetry(); const protocol = new Protocol('https://github.com/github/test.git'); @@ -271,7 +272,7 @@ describe('CopilotRemoteAgentManager', function () { it('should return empty session when copilot API is not available', async function () { const token = new vscode.CancellationTokenSource().token; - const result = await manager.provideChatSessionContent('123', token); + const result = await manager.provideChatSessionContent(SessionIdForPr.getResource(123, 0), token); assert.strictEqual(Array.isArray(result.history), true); assert.strictEqual(result.history.length, 0); @@ -282,7 +283,7 @@ describe('CopilotRemoteAgentManager', function () { const tokenSource = new vscode.CancellationTokenSource(); tokenSource.cancel(); - const result = await manager.provideChatSessionContent('123', tokenSource.token); + const result = await manager.provideChatSessionContent(SessionIdForPr.getResource(123, 0), tokenSource.token); assert.strictEqual(Array.isArray(result.history), true); assert.strictEqual(result.history.length, 0); @@ -291,7 +292,7 @@ describe('CopilotRemoteAgentManager', function () { it('should return empty session for invalid PR number', async function () { const token = new vscode.CancellationTokenSource().token; - const result = await manager.provideChatSessionContent('invalid', token); + const result = await manager.provideChatSessionContent(vscode.Uri.from({ scheme: COPILOT_SWE_AGENT, path: '/invalid' }), token); assert.strictEqual(Array.isArray(result.history), true); assert.strictEqual(result.history.length, 0); From 040f821938ff081fd67dbae3b374c1b07288c224 Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Thu, 23 Oct 2025 17:36:41 -0700 Subject: [PATCH 007/402] Update coding agent configuration (#8037) --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 0a08a52975..2023fa0056 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "name": "copilot", "displayName": "GitHub Copilot coding agent", "description": "Delegate tasks to the GitHub Copilot coding agent. The agent works asynchronously to implement changes, iterates via chat, and can create or update pull requests as needed.", - "when": "config.chat.agentSessionsViewLocation && config.chat.agentSessionsViewLocation != 'disabled' && !config.github.copilot.chat.advanced.copilotCodingAgent.enabled", + "when": "config.chat.agentSessionsViewLocation && config.chat.agentSessionsViewLocation != 'disabled' && config.github.copilot.chat.advanced.copilotCodingAgentV0.enabled", "capabilities": { "supportsFileAttachments": true } @@ -88,7 +88,7 @@ "displayName": "GitHub Copilot coding agent", "description": "Copilot coding agent is a remote, autonomous software development agent. Developers delegate tasks to the agent, which iterates on pull requests based on feedback and reviews.", "followUpRegex": "open-pull-request-webview.*((%7B.*?%7D)|(\\{.*?\\}))", - "when": "config.githubPullRequests.codingAgent.enabled && config.githubPullRequests.codingAgent.uiIntegration && copilotCodingAgentAssignable && !config.github.copilot.chat.advanced.copilotCodingAgent.enabled" + "when": "config.githubPullRequests.codingAgent.enabled && config.githubPullRequests.codingAgent.uiIntegration && copilotCodingAgentAssignable && config.github.copilot.chat.advanced.copilotCodingAgentV0.enabled" } ], "chatParticipants": [ From 5c8a91fd97948a367a0dd8181cb545f264108fe2 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Thu, 23 Oct 2025 17:53:37 -0700 Subject: [PATCH 008/402] update chatSessionType -> resourceScheme (#8038) --- package.json | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 2023fa0056..43e0952f67 100644 --- a/package.json +++ b/package.json @@ -3529,33 +3529,33 @@ "chat/chatSessions": [ { "command": "pr.openChanges", - "when": "chatSessionType == copilot-swe-agent || chatSessionType == copilot-cloud-agent", + "when": "resourceScheme == copilot-swe-agent || resourceScheme == copilot-cloud-agent", "group": "inline" }, { "command": "pr.checkoutChatSessionPullRequest", - "when": "chatSessionType == copilot-swe-agent || chatSessionType == copilot-cloud-agent", + "when": "resourceScheme == copilot-swe-agent || resourceScheme == copilot-cloud-agent", "group": "context" }, { "command": "pr.closeChatSessionPullRequest", - "when": "chatSessionType == copilot-swe-agent", + "when": "resourceScheme == copilot-swe-agent", "group": "context" }, { "command": "pr.cancelCodingAgent", - "when": "chatSessionType == copilot-swe-agent || chatSessionType == copilot-cloud-agent", + "when": "resourceScheme == copilot-swe-agent || resourceScheme == copilot-cloud-agent", "group": "context" } ], "chat/multiDiff/context": [ { "command": "pr.checkoutFromDescription", - "when": "chatSessionType == copilot-swe-agent || chatSessionType == 'copilot-cloud-agent'" + "when": "resourceScheme == copilot-swe-agent || resourceScheme == 'copilot-cloud-agent'" }, { "command": "pr.applyChangesFromDescription", - "when": "chatSessionType == copilot-swe-agent || chatSessionType == 'copilot-cloud-agent'" + "when": "resourceScheme == copilot-swe-agent || resourceScheme == 'copilot-cloud-agent'" } ] }, From c36362a406176c13e8504a3929c60c4235c78f5f Mon Sep 17 00:00:00 2001 From: Peng Lyu Date: Thu, 23 Oct 2025 20:35:48 -0700 Subject: [PATCH 009/402] avoid pr extension command conflict with copilot chat (#8040) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 43e0952f67..f7de571107 100644 --- a/package.json +++ b/package.json @@ -3544,7 +3544,7 @@ }, { "command": "pr.cancelCodingAgent", - "when": "resourceScheme == copilot-swe-agent || resourceScheme == copilot-cloud-agent", + "when": "resourceScheme == copilot-swe-agent", "group": "context" } ], From 536ac4653ede6e53f0f3251f16c9e5eeb7c55b0a Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Fri, 24 Oct 2025 11:00:45 +0200 Subject: [PATCH 010/402] Run nightly build on Fridays too (#8041) --- azure-pipeline.nightly.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipeline.nightly.yml b/azure-pipeline.nightly.yml index d4f1eeccb7..28d5448034 100644 --- a/azure-pipeline.nightly.yml +++ b/azure-pipeline.nightly.yml @@ -3,7 +3,7 @@ trigger: none pr: none schedules: - - cron: '0 4 * * Mon-Thu' + - cron: '0 4 * * Mon-Fri' displayName: Nightly Release Schedule always: true branches: From ac3d28cbcc5343ab414e63a17fcd59e6e8453e8b Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Fri, 24 Oct 2025 13:07:17 +0200 Subject: [PATCH 011/402] Script to update codicons (#8042) * Use codicon names for icons * Move codicons into folder * Script to update codicons * yarn --- .vscodeignore | 3 +- build/filters.js | 1 + build/update-codicons.ts | 107 ++++++++++++++++++ eslint.config.mjs | 1 + package.json | 4 +- resources/icons/alert.svg | 3 - resources/icons/assignee.svg | 10 -- resources/icons/check.svg | 3 - resources/icons/chevron.svg | 3 - resources/icons/chevron_down.svg | 3 - resources/icons/close.svg | 3 - resources/icons/codicons/account.svg | 1 + resources/icons/codicons/add.svg | 1 + resources/icons/codicons/check.svg | 1 + resources/icons/codicons/chevron-down.svg | 1 + resources/icons/codicons/circle-filled.svg | 1 + resources/icons/codicons/close.svg | 1 + resources/icons/codicons/comment.svg | 1 + resources/icons/codicons/copilot.svg | 1 + resources/icons/codicons/copy.svg | 1 + resources/icons/codicons/edit.svg | 1 + resources/icons/codicons/error.svg | 1 + resources/icons/codicons/feedback.svg | 1 + resources/icons/codicons/git-commit.svg | 1 + resources/icons/codicons/git-compare.svg | 1 + resources/icons/codicons/git-merge.svg | 1 + .../codicons/git-pull-request-closed.svg | 1 + .../icons/codicons/git-pull-request-draft.svg | 1 + resources/icons/codicons/git-pull-request.svg | 1 + resources/icons/codicons/github-project.svg | 1 + resources/icons/codicons/issues.svg | 1 + resources/icons/codicons/loading.svg | 1 + resources/icons/codicons/milestone.svg | 1 + resources/icons/codicons/pass.svg | 1 + resources/icons/codicons/quote.svg | 1 + resources/icons/codicons/request-changes.svg | 1 + resources/icons/codicons/settings-gear.svg | 1 + resources/icons/codicons/sparkle.svg | 1 + resources/icons/codicons/stop-circle.svg | 1 + resources/icons/codicons/sync.svg | 1 + resources/icons/codicons/tag.svg | 1 + resources/icons/codicons/tasklist.svg | 1 + resources/icons/codicons/three-bars.svg | 1 + resources/icons/codicons/trash.svg | 1 + resources/icons/codicons/warning.svg | 1 + resources/icons/comment.svg | 4 - resources/icons/commit_icon.svg | 3 - resources/icons/copilot.svg | 1 - resources/icons/copy.svg | 1 - resources/icons/delete.svg | 3 - resources/icons/dot.svg | 3 - resources/icons/edit.svg | 4 - resources/icons/error.svg | 1 - resources/icons/gear.svg | 3 - ...bview.svg => git-pull-request_webview.svg} | 0 resources/icons/github-project.svg | 4 - resources/icons/issue.svg | 1 - resources/icons/issue_closed.svg | 1 - resources/icons/label.svg | 3 - resources/icons/loading.svg | 1 - resources/icons/merge_icon.svg | 3 - resources/icons/merge_method.svg | 3 - resources/icons/milestone.svg | 3 - resources/icons/output.svg | 1 - resources/icons/plus.svg | 3 - resources/icons/pr.svg | 3 - resources/icons/pr_base.svg | 3 - resources/icons/pr_closed.svg | 4 - resources/icons/pr_draft.svg | 6 - resources/icons/quote.svg | 1 - resources/icons/request_changes.svg | 3 - resources/icons/reviewer.svg | 3 - resources/icons/settings.svg | 3 - resources/icons/sparkle.svg | 1 - resources/icons/stop-circle.svg | 1 - resources/icons/sync.svg | 1 - resources/icons/tasklist.svg | 1 - resources/icons/three-bars.svg | 1 - ...ode.proposed.chatParticipantAdditions.d.ts | 9 +- src/github/pullRequestOverview.ts | 4 +- tsconfig.scripts.json | 14 +++ webviews/components/comment.tsx | 4 +- webviews/components/dropdown.tsx | 4 +- webviews/components/header.tsx | 10 +- webviews/components/icon.tsx | 81 +++++++------ webviews/components/merge.tsx | 14 +-- webviews/components/reviewer.tsx | 6 +- webviews/components/timeline.tsx | 8 +- webviews/createPullRequestViewNew/app.tsx | 10 +- yarn.lock | 90 ++++++++++++++- 90 files changed, 327 insertions(+), 179 deletions(-) create mode 100644 build/update-codicons.ts delete mode 100644 resources/icons/alert.svg delete mode 100644 resources/icons/assignee.svg delete mode 100644 resources/icons/check.svg delete mode 100644 resources/icons/chevron.svg delete mode 100644 resources/icons/chevron_down.svg delete mode 100644 resources/icons/close.svg create mode 100644 resources/icons/codicons/account.svg create mode 100644 resources/icons/codicons/add.svg create mode 100644 resources/icons/codicons/check.svg create mode 100644 resources/icons/codicons/chevron-down.svg create mode 100644 resources/icons/codicons/circle-filled.svg create mode 100644 resources/icons/codicons/close.svg create mode 100644 resources/icons/codicons/comment.svg create mode 100644 resources/icons/codicons/copilot.svg create mode 100644 resources/icons/codicons/copy.svg create mode 100644 resources/icons/codicons/edit.svg create mode 100644 resources/icons/codicons/error.svg create mode 100644 resources/icons/codicons/feedback.svg create mode 100644 resources/icons/codicons/git-commit.svg create mode 100644 resources/icons/codicons/git-compare.svg create mode 100644 resources/icons/codicons/git-merge.svg create mode 100644 resources/icons/codicons/git-pull-request-closed.svg create mode 100644 resources/icons/codicons/git-pull-request-draft.svg create mode 100644 resources/icons/codicons/git-pull-request.svg create mode 100644 resources/icons/codicons/github-project.svg create mode 100644 resources/icons/codicons/issues.svg create mode 100644 resources/icons/codicons/loading.svg create mode 100644 resources/icons/codicons/milestone.svg create mode 100644 resources/icons/codicons/pass.svg create mode 100644 resources/icons/codicons/quote.svg create mode 100644 resources/icons/codicons/request-changes.svg create mode 100644 resources/icons/codicons/settings-gear.svg create mode 100644 resources/icons/codicons/sparkle.svg create mode 100644 resources/icons/codicons/stop-circle.svg create mode 100644 resources/icons/codicons/sync.svg create mode 100644 resources/icons/codicons/tag.svg create mode 100644 resources/icons/codicons/tasklist.svg create mode 100644 resources/icons/codicons/three-bars.svg create mode 100644 resources/icons/codicons/trash.svg create mode 100644 resources/icons/codicons/warning.svg delete mode 100644 resources/icons/comment.svg delete mode 100644 resources/icons/commit_icon.svg delete mode 100644 resources/icons/copilot.svg delete mode 100644 resources/icons/copy.svg delete mode 100644 resources/icons/delete.svg delete mode 100644 resources/icons/dot.svg delete mode 100644 resources/icons/edit.svg delete mode 100644 resources/icons/error.svg delete mode 100644 resources/icons/gear.svg rename resources/icons/{pr_webview.svg => git-pull-request_webview.svg} (100%) delete mode 100644 resources/icons/github-project.svg delete mode 100644 resources/icons/issue.svg delete mode 100644 resources/icons/issue_closed.svg delete mode 100644 resources/icons/label.svg delete mode 100644 resources/icons/loading.svg delete mode 100644 resources/icons/merge_icon.svg delete mode 100644 resources/icons/merge_method.svg delete mode 100644 resources/icons/milestone.svg delete mode 100644 resources/icons/output.svg delete mode 100644 resources/icons/plus.svg delete mode 100644 resources/icons/pr.svg delete mode 100644 resources/icons/pr_base.svg delete mode 100644 resources/icons/pr_closed.svg delete mode 100644 resources/icons/pr_draft.svg delete mode 100644 resources/icons/quote.svg delete mode 100644 resources/icons/request_changes.svg delete mode 100644 resources/icons/reviewer.svg delete mode 100644 resources/icons/settings.svg delete mode 100644 resources/icons/sparkle.svg delete mode 100644 resources/icons/stop-circle.svg delete mode 100644 resources/icons/sync.svg delete mode 100644 resources/icons/tasklist.svg delete mode 100644 resources/icons/three-bars.svg create mode 100644 tsconfig.scripts.json diff --git a/.vscodeignore b/.vscodeignore index 3c9c0e53af..ec86cb30a1 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -29,8 +29,7 @@ azure-pipeline.* yarn.lock **/*.map **/*.svg -!**/output.svg -!**/pr_webview.svg +!**/git-pull-request_webview.svg **/*.ts *.vsix **/*.bak diff --git a/build/filters.js b/build/filters.js index 7f8f92eaa1..1425e36c2f 100644 --- a/build/filters.js +++ b/build/filters.js @@ -64,6 +64,7 @@ module.exports.copyrightFilter = [ '!tsconfig.json', '!tsconfig.test.json', '!tsconfig.webviews.json', + '!tsconfig.scripts.json', '!tsfmt.json', '!**/queries*.gql', '!**/*.yml', diff --git a/build/update-codicons.ts b/build/update-codicons.ts new file mode 100644 index 0000000000..1419385c93 --- /dev/null +++ b/build/update-codicons.ts @@ -0,0 +1,107 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as fs from 'fs'; +import * as path from 'path'; +import * as https from 'https'; + +const CODICONS_DIR = path.join(__dirname, '..', 'resources', 'icons', 'codicons'); +const BASE_URL = 'https://raw.githubusercontent.com/microsoft/vscode-codicons/refs/heads/mrleemurray/new-icons/src/icons'; + +interface UpdateResult { + filename: string; + status: 'updated' | 'unchanged' | 'error'; + error?: string; +} + +function readLocalIconFilenames(): string[] { + return fs.readdirSync(CODICONS_DIR).filter(f => f.endsWith('.svg')); +} + +function fetchRemoteIcon(filename: string): Promise { + const url = `${BASE_URL}/${encodeURIComponent(filename)}`; + return new Promise((resolve, reject) => { + https.get(url, res => { + const { statusCode } = res; + if (statusCode !== 200) { + res.resume(); // drain + return reject(new Error(`Failed to fetch ${filename}: HTTP ${statusCode}`)); + } + let data = ''; + res.setEncoding('utf8'); + res.on('data', chunk => { data += chunk; }); + res.on('end', () => resolve(data)); + }).on('error', reject); + }); +} + +async function updateIcon(filename: string): Promise { + const localPath = path.join(CODICONS_DIR, filename); + const oldContent = fs.readFileSync(localPath, 'utf8'); + try { + const newContent = await fetchRemoteIcon(filename); + if (normalize(oldContent) === normalize(newContent)) { + return { filename, status: 'unchanged' }; + } + fs.writeFileSync(localPath, newContent, 'utf8'); + return { filename, status: 'updated' }; + } catch (err: any) { + return { filename, status: 'error', error: err?.message ?? String(err) }; + } +} + +function normalize(svg: string): string { + return svg.replace(/\r\n?/g, '\n').trim(); +} + +async function main(): Promise { + const icons = readLocalIconFilenames(); + if (!icons.length) { + console.log('No codicon SVGs found to update.'); + return; + } + console.log(`Updating ${icons.length} codicon(s) from upstream...`); + + const concurrency = 8; + const queue = icons.slice(); + const results: UpdateResult[] = []; + + async function worker(): Promise { + while (queue.length) { + const file = queue.shift(); + if (!file) { + break; + } + const result = await updateIcon(file); + results.push(result); + if (result.status === 'updated') { + console.log(` ✔ ${file} updated`); + } else if (result.status === 'unchanged') { + console.log(` • ${file} unchanged`); + } else { + // allow-any-unicode-next-line + console.warn(` ✖ ${file} ${result.error}`); + } + } + } + + const workers = Array.from({ length: Math.min(concurrency, icons.length) }, () => worker()); + await Promise.all(workers); + + const updated = results.filter(r => r.status === 'updated').length; + const unchanged = results.filter(r => r.status === 'unchanged').length; + const errored = results.filter(r => r.status === 'error').length; + console.log(`Done. Updated: ${updated}, Unchanged: ${unchanged}, Errors: ${errored}.`); + if (errored) { + process.exitCode = 1; + } +} + +main().catch(err => { + console.error(err?.stack || err?.message || String(err)); + process.exit(1); +}); + +export { }; // ensure this file is treated as a module diff --git a/eslint.config.mjs b/eslint.config.mjs index 088acb5aee..2f509573d0 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -15,6 +15,7 @@ export default defineConfig([ // Global ignore patterns { ignores: [ + 'build', 'dist/**/*', 'out/**/*', 'src/@types/**/*.d.ts', diff --git a/package.json b/package.json index f7de571107..42135dabe6 100644 --- a/package.json +++ b/package.json @@ -4257,7 +4257,8 @@ "watch": "webpack --watch --mode development --env esbuild", "watch:web": "webpack --watch --mode development --config-name extension:webworker --config-name webviews", "hygiene": "node ./build/hygiene.js", - "prepare": "husky install" + "prepare": "husky install", + "update:codicons": "npx ts-node --project tsconfig.scripts.json build/update-codicons.ts" }, "devDependencies": { "@eslint/js": "^9.36.0", @@ -4321,6 +4322,7 @@ "terser-webpack-plugin": "5.1.1", "timers-browserify": "^2.0.12", "ts-loader": "9.5.2", + "ts-node": "^10.9.2", "tty": "1.0.1", "typescript": "^5.9.2", "typescript-eslint": "^8.44.0", diff --git a/resources/icons/alert.svg b/resources/icons/alert.svg deleted file mode 100644 index 3c493a4c29..0000000000 --- a/resources/icons/alert.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/assignee.svg b/resources/icons/assignee.svg deleted file mode 100644 index 2bb8758146..0000000000 --- a/resources/icons/assignee.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/resources/icons/check.svg b/resources/icons/check.svg deleted file mode 100644 index f94e91f51b..0000000000 --- a/resources/icons/check.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/chevron.svg b/resources/icons/chevron.svg deleted file mode 100644 index f6f3e9e3eb..0000000000 --- a/resources/icons/chevron.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/chevron_down.svg b/resources/icons/chevron_down.svg deleted file mode 100644 index d369b3dc9d..0000000000 --- a/resources/icons/chevron_down.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/close.svg b/resources/icons/close.svg deleted file mode 100644 index f8af265cc4..0000000000 --- a/resources/icons/close.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/codicons/account.svg b/resources/icons/codicons/account.svg new file mode 100644 index 0000000000..851e0511fb --- /dev/null +++ b/resources/icons/codicons/account.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/add.svg b/resources/icons/codicons/add.svg new file mode 100644 index 0000000000..3d5d0221c4 --- /dev/null +++ b/resources/icons/codicons/add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/check.svg b/resources/icons/codicons/check.svg new file mode 100644 index 0000000000..6217cb9572 --- /dev/null +++ b/resources/icons/codicons/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/chevron-down.svg b/resources/icons/codicons/chevron-down.svg new file mode 100644 index 0000000000..4c4a6d1369 --- /dev/null +++ b/resources/icons/codicons/chevron-down.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/circle-filled.svg b/resources/icons/codicons/circle-filled.svg new file mode 100644 index 0000000000..3e224dabc8 --- /dev/null +++ b/resources/icons/codicons/circle-filled.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/close.svg b/resources/icons/codicons/close.svg new file mode 100644 index 0000000000..f6fc1bca88 --- /dev/null +++ b/resources/icons/codicons/close.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/comment.svg b/resources/icons/codicons/comment.svg new file mode 100644 index 0000000000..6430691a6b --- /dev/null +++ b/resources/icons/codicons/comment.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/copilot.svg b/resources/icons/codicons/copilot.svg new file mode 100644 index 0000000000..6d52a36692 --- /dev/null +++ b/resources/icons/codicons/copilot.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/copy.svg b/resources/icons/codicons/copy.svg new file mode 100644 index 0000000000..39a62984ea --- /dev/null +++ b/resources/icons/codicons/copy.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/edit.svg b/resources/icons/codicons/edit.svg new file mode 100644 index 0000000000..7642adb9f7 --- /dev/null +++ b/resources/icons/codicons/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/error.svg b/resources/icons/codicons/error.svg new file mode 100644 index 0000000000..1e2337f80d --- /dev/null +++ b/resources/icons/codicons/error.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/feedback.svg b/resources/icons/codicons/feedback.svg new file mode 100644 index 0000000000..2de89fd2ae --- /dev/null +++ b/resources/icons/codicons/feedback.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/git-commit.svg b/resources/icons/codicons/git-commit.svg new file mode 100644 index 0000000000..ecce26c503 --- /dev/null +++ b/resources/icons/codicons/git-commit.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/git-compare.svg b/resources/icons/codicons/git-compare.svg new file mode 100644 index 0000000000..193a80cf96 --- /dev/null +++ b/resources/icons/codicons/git-compare.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/git-merge.svg b/resources/icons/codicons/git-merge.svg new file mode 100644 index 0000000000..63dbdc36e0 --- /dev/null +++ b/resources/icons/codicons/git-merge.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/git-pull-request-closed.svg b/resources/icons/codicons/git-pull-request-closed.svg new file mode 100644 index 0000000000..bce2914a6e --- /dev/null +++ b/resources/icons/codicons/git-pull-request-closed.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/git-pull-request-draft.svg b/resources/icons/codicons/git-pull-request-draft.svg new file mode 100644 index 0000000000..0afee6e0e3 --- /dev/null +++ b/resources/icons/codicons/git-pull-request-draft.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/git-pull-request.svg b/resources/icons/codicons/git-pull-request.svg new file mode 100644 index 0000000000..47a216d753 --- /dev/null +++ b/resources/icons/codicons/git-pull-request.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/github-project.svg b/resources/icons/codicons/github-project.svg new file mode 100644 index 0000000000..d240cf2cf6 --- /dev/null +++ b/resources/icons/codicons/github-project.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/issues.svg b/resources/icons/codicons/issues.svg new file mode 100644 index 0000000000..7de219baea --- /dev/null +++ b/resources/icons/codicons/issues.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/loading.svg b/resources/icons/codicons/loading.svg new file mode 100644 index 0000000000..57a717a150 --- /dev/null +++ b/resources/icons/codicons/loading.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/milestone.svg b/resources/icons/codicons/milestone.svg new file mode 100644 index 0000000000..3d2f9db353 --- /dev/null +++ b/resources/icons/codicons/milestone.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/pass.svg b/resources/icons/codicons/pass.svg new file mode 100644 index 0000000000..0abbfb2a92 --- /dev/null +++ b/resources/icons/codicons/pass.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/quote.svg b/resources/icons/codicons/quote.svg new file mode 100644 index 0000000000..6903c03730 --- /dev/null +++ b/resources/icons/codicons/quote.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/request-changes.svg b/resources/icons/codicons/request-changes.svg new file mode 100644 index 0000000000..749801beeb --- /dev/null +++ b/resources/icons/codicons/request-changes.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/settings-gear.svg b/resources/icons/codicons/settings-gear.svg new file mode 100644 index 0000000000..cdc25f1e9d --- /dev/null +++ b/resources/icons/codicons/settings-gear.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/sparkle.svg b/resources/icons/codicons/sparkle.svg new file mode 100644 index 0000000000..cacf8d2a88 --- /dev/null +++ b/resources/icons/codicons/sparkle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/stop-circle.svg b/resources/icons/codicons/stop-circle.svg new file mode 100644 index 0000000000..9970ad8c97 --- /dev/null +++ b/resources/icons/codicons/stop-circle.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/sync.svg b/resources/icons/codicons/sync.svg new file mode 100644 index 0000000000..1767194bbf --- /dev/null +++ b/resources/icons/codicons/sync.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/tag.svg b/resources/icons/codicons/tag.svg new file mode 100644 index 0000000000..788540b4ef --- /dev/null +++ b/resources/icons/codicons/tag.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/tasklist.svg b/resources/icons/codicons/tasklist.svg new file mode 100644 index 0000000000..c9b951ff1c --- /dev/null +++ b/resources/icons/codicons/tasklist.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/three-bars.svg b/resources/icons/codicons/three-bars.svg new file mode 100644 index 0000000000..b31880865e --- /dev/null +++ b/resources/icons/codicons/three-bars.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/trash.svg b/resources/icons/codicons/trash.svg new file mode 100644 index 0000000000..cb5ac5d1f3 --- /dev/null +++ b/resources/icons/codicons/trash.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/warning.svg b/resources/icons/codicons/warning.svg new file mode 100644 index 0000000000..9400358a2d --- /dev/null +++ b/resources/icons/codicons/warning.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/comment.svg b/resources/icons/comment.svg deleted file mode 100644 index 672b889b3b..0000000000 --- a/resources/icons/comment.svg +++ /dev/null @@ -1,4 +0,0 @@ - \ No newline at end of file diff --git a/resources/icons/commit_icon.svg b/resources/icons/commit_icon.svg deleted file mode 100644 index dc1d10c63f..0000000000 --- a/resources/icons/commit_icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/copilot.svg b/resources/icons/copilot.svg deleted file mode 100644 index 2e5f639e35..0000000000 --- a/resources/icons/copilot.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/resources/icons/copy.svg b/resources/icons/copy.svg deleted file mode 100644 index 3a932546b3..0000000000 --- a/resources/icons/copy.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/resources/icons/delete.svg b/resources/icons/delete.svg deleted file mode 100644 index 4bebdd27c4..0000000000 --- a/resources/icons/delete.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/dot.svg b/resources/icons/dot.svg deleted file mode 100644 index 0394588aec..0000000000 --- a/resources/icons/dot.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/edit.svg b/resources/icons/edit.svg deleted file mode 100644 index b02c84f152..0000000000 --- a/resources/icons/edit.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/resources/icons/error.svg b/resources/icons/error.svg deleted file mode 100644 index 4cfb5bbfab..0000000000 --- a/resources/icons/error.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/resources/icons/gear.svg b/resources/icons/gear.svg deleted file mode 100644 index d4b13f9797..0000000000 --- a/resources/icons/gear.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/pr_webview.svg b/resources/icons/git-pull-request_webview.svg similarity index 100% rename from resources/icons/pr_webview.svg rename to resources/icons/git-pull-request_webview.svg diff --git a/resources/icons/github-project.svg b/resources/icons/github-project.svg deleted file mode 100644 index cf8b1ddf03..0000000000 --- a/resources/icons/github-project.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/resources/icons/issue.svg b/resources/icons/issue.svg deleted file mode 100644 index 666a3baac7..0000000000 --- a/resources/icons/issue.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/resources/icons/issue_closed.svg b/resources/icons/issue_closed.svg deleted file mode 100644 index 6a6c314255..0000000000 --- a/resources/icons/issue_closed.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/resources/icons/label.svg b/resources/icons/label.svg deleted file mode 100644 index 06c7fb55ea..0000000000 --- a/resources/icons/label.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/loading.svg b/resources/icons/loading.svg deleted file mode 100644 index 4e3e640e03..0000000000 --- a/resources/icons/loading.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/resources/icons/merge_icon.svg b/resources/icons/merge_icon.svg deleted file mode 100644 index 6d3f716696..0000000000 --- a/resources/icons/merge_icon.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/merge_method.svg b/resources/icons/merge_method.svg deleted file mode 100644 index b8596218f1..0000000000 --- a/resources/icons/merge_method.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/milestone.svg b/resources/icons/milestone.svg deleted file mode 100644 index 58ac83a825..0000000000 --- a/resources/icons/milestone.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/output.svg b/resources/icons/output.svg deleted file mode 100644 index 41f7853c24..0000000000 --- a/resources/icons/output.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/resources/icons/plus.svg b/resources/icons/plus.svg deleted file mode 100644 index 4d9389336b..0000000000 --- a/resources/icons/plus.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/pr.svg b/resources/icons/pr.svg deleted file mode 100644 index 6d59036b31..0000000000 --- a/resources/icons/pr.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/pr_base.svg b/resources/icons/pr_base.svg deleted file mode 100644 index 862a547917..0000000000 --- a/resources/icons/pr_base.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/pr_closed.svg b/resources/icons/pr_closed.svg deleted file mode 100644 index 4fb8a73d1f..0000000000 --- a/resources/icons/pr_closed.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/resources/icons/pr_draft.svg b/resources/icons/pr_draft.svg deleted file mode 100644 index d0cf9b3008..0000000000 --- a/resources/icons/pr_draft.svg +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/resources/icons/quote.svg b/resources/icons/quote.svg deleted file mode 100644 index 1c71b75363..0000000000 --- a/resources/icons/quote.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/resources/icons/request_changes.svg b/resources/icons/request_changes.svg deleted file mode 100644 index c412bb8546..0000000000 --- a/resources/icons/request_changes.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - \ No newline at end of file diff --git a/resources/icons/reviewer.svg b/resources/icons/reviewer.svg deleted file mode 100644 index e83e580fd1..0000000000 --- a/resources/icons/reviewer.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/settings.svg b/resources/icons/settings.svg deleted file mode 100644 index 4e7e022bcf..0000000000 --- a/resources/icons/settings.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/sparkle.svg b/resources/icons/sparkle.svg deleted file mode 100644 index 442e6cc389..0000000000 --- a/resources/icons/sparkle.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/resources/icons/stop-circle.svg b/resources/icons/stop-circle.svg deleted file mode 100644 index 4f39984fa2..0000000000 --- a/resources/icons/stop-circle.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/resources/icons/sync.svg b/resources/icons/sync.svg deleted file mode 100644 index 63c0090a6c..0000000000 --- a/resources/icons/sync.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/resources/icons/tasklist.svg b/resources/icons/tasklist.svg deleted file mode 100644 index efc81525d0..0000000000 --- a/resources/icons/tasklist.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/resources/icons/three-bars.svg b/resources/icons/three-bars.svg deleted file mode 100644 index f49db2a735..0000000000 --- a/resources/icons/three-bars.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts index c0b38dffc4..f05e4fe691 100644 --- a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts @@ -152,12 +152,19 @@ declare module 'vscode' { */ title: string; + /** + * Whether the multi diff editor should be read-only. + * When true, users cannot open individual files or interact with file navigation. + */ + readOnly?: boolean; + /** * Create a new ChatResponseMultiDiffPart. * @param value Array of file diff entries. * @param title The title for the multi diff editor. + * @param readOnly Optional flag to make the multi diff editor read-only. */ - constructor(value: ChatResponseDiffEntry[], title: string); + constructor(value: ChatResponseDiffEntry[], title: string, readOnly?: boolean); } export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseNotebookEditPart | ChatResponseConfirmationPart | ChatResponseCodeCitationPart | ChatResponseReferencePart2 | ChatResponseMovePart | ChatResponseExtensionsPart | ChatResponsePullRequestPart | ChatPrepareToolInvocationPart | ChatToolInvocationPart | ChatResponseMultiDiffPart | ChatResponseThinkingProgressPart; diff --git a/src/github/pullRequestOverview.ts b/src/github/pullRequestOverview.ts index 47dbe5ce23..8b85298d74 100644 --- a/src/github/pullRequestOverview.ts +++ b/src/github/pullRequestOverview.ts @@ -134,8 +134,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel deleteComment({ id, pullRequestReviewId })} > - {deleteIcon} + {trashIcon} ) : null} diff --git a/webviews/components/dropdown.tsx b/webviews/components/dropdown.tsx index a271e8780a..1c4d9b9f13 100644 --- a/webviews/components/dropdown.tsx +++ b/webviews/components/dropdown.tsx @@ -5,7 +5,7 @@ import React, { useState } from 'react'; import { v4 as uuid } from 'uuid'; -import { chevronIcon } from './icon'; +import { chevronDownIcon } from './icon'; const enum KEYCODES { esc = 27, @@ -98,7 +98,7 @@ export const Dropdown = ({ options, defaultOption, disabled, submitAction, chang />
diff --git a/webviews/components/header.tsx b/webviews/components/header.tsx index a4254279f8..9418515e19 100644 --- a/webviews/components/header.tsx +++ b/webviews/components/header.tsx @@ -5,7 +5,7 @@ import React, { useContext, useState } from 'react'; import { ContextDropdown } from './contextDropdown'; -import { copilotErrorIcon, copilotInProgressIcon, copilotSuccessIcon, copyIcon, editIcon, issueClosedIcon, issueIcon, loadingIcon, mergeIcon, prClosedIcon, prDraftIcon, prOpenIcon } from './icon'; +import { copilotErrorIcon, copilotInProgressIcon, copilotSuccessIcon, copyIcon, editIcon, gitMergeIcon, gitPullRequestClosedIcon, gitPullRequestDraftIcon, gitPullRequestIcon, issuescon, loadingIcon, passIcon } from './icon'; import { AuthorLink, Avatar } from './user'; import { copilotEventToStatus, CopilotPRStatus, mostRecentCopilotEvent } from '../../src/common/copilot'; import { CopilotStartedEvent, TimelineEvent } from '../../src/common/timelineEvent'; @@ -366,13 +366,13 @@ const CheckoutButton: React.FC = ({ isCurrentlyCheckedOut, }; export function getStatus(state: GithubItemStateEnum, isDraft: boolean, isIssue: boolean, stateReason?: StateReason) { - const closed = isIssue ? issueClosedIcon : prClosedIcon; - const open = isIssue ? issueIcon : prOpenIcon; + const closed = isIssue ? passIcon : gitPullRequestClosedIcon; + const open = isIssue ? issuescon : gitPullRequestIcon; if (state === GithubItemStateEnum.Merged) { - return { text: 'Merged', color: 'merged', icon: mergeIcon }; + return { text: 'Merged', color: 'merged', icon: gitMergeIcon }; } else if (state === GithubItemStateEnum.Open) { - return isDraft ? { text: 'Draft', color: 'draft', icon: prDraftIcon } : { text: 'Open', color: 'open', icon: open }; + return isDraft ? { text: 'Draft', color: 'draft', icon: gitPullRequestDraftIcon } : { text: 'Open', color: 'open', icon: open }; } else { let closedColor: string = 'closed'; if (isIssue) { diff --git a/webviews/components/icon.tsx b/webviews/components/icon.tsx index 6e18b19348..6f0cddcf5c 100644 --- a/webviews/components/icon.tsx +++ b/webviews/components/icon.tsx @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ - + import * as React from 'react'; export const Icon = ({ className = '', src, title }: { className?: string; title?: string; src: string }) => ( @@ -11,46 +11,45 @@ export const Icon = ({ className = '', src, title }: { className?: string; title ); export default Icon; +// Codicons +export const accountIcon = ; +export const addIcon = ; +export const checkIcon = ; +export const chevronDownIcon = ; +export const circleFilledIcon = ; +export const closeIcon = ; +export const commentIcon = ; +export const copilotIcon = ; +export const copyIcon = ; +export const editIcon = ; +export const errorIcon = ; +export const feedbackIcon = ; +export const gitCommitIcon = ; +export const gitCompareIcon = ; +export const gitMergeIcon = ; +export const gitPullRequestClosedIcon = ; +export const gitPullRequestDraftIcon = ; +export const gitPullRequestIcon = ; +export const issuescon = ; +export const loadingIcon = ; +export const milestoneIcon = ; +export const passIcon = ; +export const projectIcon = ; +export const quoteIcon = ; +export const requestChangesIcon = ; +export const settingsIcon = ; +export const sparkleIcon = ; +export const stopCircleIcon = ; +export const syncIcon = ; +export const tagIcon = ; +export const tasklistIcon = ; +export const threeBars = ; +export const trashIcon = ; +export const warningIcon = ; -export const alertIcon = ; -export const checkIcon = ; -export const skipIcon = ; -export const chevronIcon = ; -export const chevronDownIcon = ; -export const commentIcon = ; -export const quoteIcon = ; -export const commitIcon = ; -export const copyIcon = ; -export const deleteIcon = ; -export const mergeIcon = ; -export const mergeMethodIcon = ; -export const prClosedIcon = ; -export const prOpenIcon = ; -export const prDraftIcon = ; -export const editIcon = ; -export const plusIcon = ; -export const pendingIcon = ; -export const requestChanges = ; -export const settingsIcon = ; -export const closeIcon = ; -export const syncIcon = ; -export const prBaseIcon = ; -export const prMergeIcon = ; -export const gearIcon = ; -export const assigneeIcon = ; -export const reviewerIcon = ; -export const labelIcon = ; -export const milestoneIcon = ; -export const projectIcon = ; -export const sparkleIcon = ; -export const stopCircleIcon = ; -export const issueIcon = ; -export const issueClosedIcon = ; -export const copilotIcon = ; -export const threeBars = ; -export const tasklistIcon = ; -export const errorIcon = ; -export const loadingIcon = ; -export const copilotSuccessIcon = ; +// Other icons export const copilotErrorIcon = ; export const copilotInProgressIcon = ; +export const copilotSuccessIcon = ; +export const prMergeIcon = ; +export const skipIcon = ; diff --git a/webviews/components/merge.tsx b/webviews/components/merge.tsx index 236b9366f3..7a6aee3d7b 100644 --- a/webviews/components/merge.tsx +++ b/webviews/components/merge.tsx @@ -14,7 +14,7 @@ import React, { } from 'react'; import { AutoMerge, QueuedToMerge } from './automergeSelect'; import { Dropdown } from './dropdown'; -import { alertIcon, checkIcon, closeIcon, mergeIcon, pendingIcon, requestChanges, skipIcon } from './icon'; +import { checkIcon, circleFilledIcon, closeIcon, gitMergeIcon, requestChangesIcon, skipIcon, warningIcon } from './icon'; import { nbsp } from './space'; import { Avatar } from './user'; import { EventType, ReviewEvent } from '../../src/common/timelineEvent'; @@ -36,7 +36,7 @@ import { Reviewer } from '../components/reviewer'; const PRStatusMessage = ({ pr, isSimple }: { pr: PullRequest; isSimple: boolean }) => { return pr.state === GithubItemStateEnum.Merged ? (
-
{isSimple ? mergeIcon : null}
{' '} +
{isSimple ? gitMergeIcon : null}
{' '} {'Pull request successfully merged.'}
) : pr.state === GithubItemStateEnum.Closed ? ( @@ -218,7 +218,7 @@ export const MergeStatus = ({ mergeable, isSimple, canUpdateBranch }: { mergeabl updateBranch().finally(() => setBusy(false)); }; - let icon: JSX.Element | null = pendingIcon; + let icon: JSX.Element | null = circleFilledIcon; let summary: string = 'Checking if this branch can be merged...'; let action: string | null = null; if (mergeable === PullRequestMergeability.Mergeable) { @@ -273,7 +273,7 @@ export const OfferToUpdate = ({ mergeable, isSimple, isCurrentlyCheckedOut, canU } return (
- {alertIcon} + {warningIcon}

This branch is out-of-date with the base branch.

@@ -298,7 +298,7 @@ export const ReadyForReview = ({ isSimple }: { isSimple: boolean }) => { return (
-
{isSimple ? null : alertIcon}
+
{isSimple ? null : warningIcon}
This pull request is still a work in progress.
Draft pull requests cannot be merged.
@@ -592,13 +592,13 @@ function StateIcon({ state }: { state: CheckState }) { case CheckState.Failure: return closeIcon; } - return pendingIcon; + return circleFilledIcon; } function RequiredReviewStateIcon({ state }: { state: CheckState }) { switch (state) { case CheckState.Pending: - return requestChanges; + return requestChangesIcon; case CheckState.Failure: return closeIcon; } diff --git a/webviews/components/reviewer.tsx b/webviews/components/reviewer.tsx index 35ed0d5eb3..be8c02be01 100644 --- a/webviews/components/reviewer.tsx +++ b/webviews/components/reviewer.tsx @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ import React, { cloneElement, useContext } from 'react'; -import { checkIcon, commentIcon, pendingIcon, requestChanges, syncIcon } from './icon'; +import { checkIcon, circleFilledIcon, commentIcon, requestChangesIcon, syncIcon } from './icon'; import { AuthorLink, Avatar } from './user'; import { ReviewEvent } from '../../src/common/timelineEvent'; import { AccountType, isITeam, ReviewState } from '../../src/github/interface'; @@ -37,8 +37,8 @@ export function Reviewer(reviewInfo: { reviewState: ReviewState, event?: ReviewE } const REVIEW_STATE: { [state: string]: React.ReactElement } = { - REQUESTED: cloneElement(pendingIcon, { className: 'section-icon requested', title: 'Awaiting requested review' }), + REQUESTED: cloneElement(circleFilledIcon, { className: 'section-icon requested', title: 'Awaiting requested review' }), COMMENTED: cloneElement(commentIcon, { className: 'section-icon commented', Root: 'div', title: 'Left review comments' }), APPROVED: cloneElement(checkIcon, { className: 'section-icon approved', title: 'Approved these changes' }), - CHANGES_REQUESTED: cloneElement(requestChanges, { className: 'section-icon changes', title: 'Requested changes' }), + CHANGES_REQUESTED: cloneElement(requestChangesIcon, { className: 'section-icon changes', title: 'Requested changes' }), }; diff --git a/webviews/components/timeline.tsx b/webviews/components/timeline.tsx index 5e617a5cf7..193828756f 100644 --- a/webviews/components/timeline.tsx +++ b/webviews/components/timeline.tsx @@ -6,7 +6,7 @@ import React, { useContext, useRef, useState } from 'react'; import { CommentView } from './comment'; import Diff from './diff'; -import { commitIcon, errorIcon, loadingIcon, mergeIcon, plusIcon, tasklistIcon, threeBars } from './icon'; +import { addIcon, errorIcon, gitCommitIcon, gitMergeIcon, loadingIcon, tasklistIcon, threeBars } from './icon'; import { nbsp } from './space'; import { Timestamp } from './timestamp'; import { AuthorLink, Avatar } from './user'; @@ -122,7 +122,7 @@ const CommitEventView = (event: CommitEvent) => { return (
- {commitIcon} + {gitCommitIcon} {nbsp}
@@ -169,7 +169,7 @@ const NewCommitsSinceReviewEventView = () => { return (
- {plusIcon} + {addIcon} {nbsp} New changes since your last Review
@@ -368,7 +368,7 @@ const MergedEventView = (event: MergedEvent) => { return (
- {mergeIcon} + {gitMergeIcon} {nbsp}
diff --git a/webviews/createPullRequestViewNew/app.tsx b/webviews/createPullRequestViewNew/app.tsx index 6f1a0fed5c..d762585360 100644 --- a/webviews/createPullRequestViewNew/app.tsx +++ b/webviews/createPullRequestViewNew/app.tsx @@ -12,7 +12,7 @@ import PullRequestContextNew from '../common/createContextNew'; import { ErrorBoundary } from '../common/errorBoundary'; import { LabelCreate } from '../common/label'; import { ContextDropdown } from '../components/contextDropdown'; -import { assigneeIcon, labelIcon, milestoneIcon, prBaseIcon, prMergeIcon, projectIcon, reviewerIcon, sparkleIcon, stopCircleIcon } from '../components/icon'; +import { accountIcon, feedbackIcon, gitCompareIcon, milestoneIcon, prMergeIcon, projectIcon, sparkleIcon, stopCircleIcon, tagIcon } from '../components/icon'; import { Avatar } from '../components/user'; type CreateMethod = 'create-draft' | 'create' | 'create-automerge-squash' | 'create-automerge-rebase' | 'create-automerge-merge'; @@ -192,7 +192,7 @@ export function main() {
- +
{params.assignees && (params.assignees.length > 0) ?
- +
    activateCommand(e.nativeEvent, 'pr.changeAssignees')} onKeyPress={(e) => activateCommand(e.nativeEvent, 'pr.changeAssignees')} @@ -267,7 +267,7 @@ export function main() { {params.reviewers && (params.reviewers.length > 0) ?
    - +
      activateCommand(e.nativeEvent, 'pr.changeReviewers')} onKeyPress={(e) => activateCommand(e.nativeEvent, 'pr.changeReviewers')} @@ -285,7 +285,7 @@ export function main() { {params.labels && (params.labels.length > 0) ?
      - +
        activateCommand(e.nativeEvent, 'pr.changeLabels')} onKeyPress={(e) => activateCommand(e.nativeEvent, 'pr.changeLabels')} diff --git a/yarn.lock b/yarn.lock index 4a5f8597e8..a95d623eb0 100644 --- a/yarn.lock +++ b/yarn.lock @@ -102,6 +102,13 @@ resolved "https://registry.yarnpkg.com/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz#75a2e8b51cb758a7553d6804a5932d7aace75c39" integrity sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw== +"@cspotcode/source-map-support@^0.8.0": + version "0.8.1" + resolved "https://registry.yarnpkg.com/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz#00629c35a688e05a88b1cda684fb9d5e73f000a1" + integrity sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw== + dependencies: + "@jridgewell/trace-mapping" "0.3.9" + "@emnapi/core@^1.4.3": version "1.5.0" resolved "https://registry.yarnpkg.com/@emnapi/core/-/core-1.5.0.tgz#85cd84537ec989cebb2343606a1ee663ce4edaf0" @@ -423,6 +430,14 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== +"@jridgewell/trace-mapping@0.3.9": + version "0.3.9" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz#6534fd5933a53ba7cbf3a17615e273a0d1273ff9" + integrity sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ== + dependencies: + "@jridgewell/resolve-uri" "^3.0.3" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping@^0.3.12": version "0.3.31" resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz#db15d6781c931f3a251a3dac39501c98a6082fd0" @@ -788,6 +803,26 @@ resolved "https://registry.yarnpkg.com/@tootallnate/once/-/once-2.0.0.tgz#f544a148d3ab35801c1f633a7441fd87c2e484bf" integrity sha512-XCuKFP5PS55gnMVu3dty8KPatLqUoy/ZYzDzAGCQ8JNFCkLXzmI7vNHCR+XpbZaMWQK/vQubr7PkYq8g470J/A== +"@tsconfig/node10@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node10/-/node10-1.0.11.tgz#6ee46400685f130e278128c7b38b7e031ff5b2f2" + integrity sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw== + +"@tsconfig/node12@^1.0.7": + version "1.0.11" + resolved "https://registry.yarnpkg.com/@tsconfig/node12/-/node12-1.0.11.tgz#ee3def1f27d9ed66dac6e46a295cffb0152e058d" + integrity sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag== + +"@tsconfig/node14@^1.0.0": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@tsconfig/node14/-/node14-1.0.3.tgz#e4386316284f00b98435bf40f72f75a09dabf6c1" + integrity sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow== + +"@tsconfig/node16@^1.0.2": + version "1.0.4" + resolved "https://registry.yarnpkg.com/@tsconfig/node16/-/node16-1.0.4.tgz#0b92dcc0cc1c81f6f306a381f28e31b1a56536e9" + integrity sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA== + "@tybys/wasm-util@^0.10.0": version "0.10.1" resolved "https://registry.yarnpkg.com/@tybys/wasm-util/-/wasm-util-0.10.1.tgz#ecddd3205cf1e2d5274649ff0eedd2991ed7f414" @@ -1459,12 +1494,19 @@ acorn-walk@^7.1.1: resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-7.2.0.tgz#0de889a601203909b0fbe07b8938dc21d2e967bc" integrity sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA== +acorn-walk@^8.1.1: + version "8.3.4" + resolved "https://registry.yarnpkg.com/acorn-walk/-/acorn-walk-8.3.4.tgz#794dd169c3977edf4ba4ea47583587c5866236b7" + integrity sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g== + dependencies: + acorn "^8.11.0" + acorn@^7.1.1: version "7.4.1" resolved "https://registry.yarnpkg.com/acorn/-/acorn-7.4.1.tgz#feaed255973d2e77555b83dbc08851a6c63520fa" integrity sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A== -acorn@^8.15.0: +acorn@^8.11.0, acorn@^8.15.0, acorn@^8.4.1: version "8.15.0" resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.15.0.tgz#a360898bc415edaac46c8241f6383975b930b816" integrity sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg== @@ -1692,6 +1734,11 @@ applicationinsights@2.4.1: diagnostic-channel "1.1.0" diagnostic-channel-publishers "1.0.5" +arg@^4.1.0: + version "4.1.3" + resolved "https://registry.yarnpkg.com/arg/-/arg-4.1.3.tgz#269fc7ad5b8e42cb63c896d5666017261c144089" + integrity sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA== + argparse@^2.0.1: version "2.0.1" resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" @@ -2486,6 +2533,11 @@ create-hmac@^1.1.0, create-hmac@^1.1.7: safe-buffer "^5.0.1" sha.js "^2.4.8" +create-require@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" + integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== + cross-fetch@3.1.5: version "3.1.5" resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.5.tgz#e1389f44d9e7ba767907f7af8454787952ab534f" @@ -2770,7 +2822,7 @@ diff@5.0.0: resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== -diff@^4.0.2: +diff@^4.0.1, diff@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== @@ -5048,6 +5100,11 @@ make-dir@^4.0.0: dependencies: semver "^7.5.3" +make-error@^1.1.1: + version "1.3.6" + resolved "https://registry.yarnpkg.com/make-error/-/make-error-1.3.6.tgz#2eb2e37ea9b67c4891f684a1394799af484cf7a2" + integrity sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw== + map-stream@0.0.7: version "0.0.7" resolved "https://registry.yarnpkg.com/map-stream/-/map-stream-0.0.7.tgz#8a1f07896d82b10926bd3744a2420009f88974a8" @@ -7239,6 +7296,25 @@ ts-loader@9.5.2: semver "^7.3.4" source-map "^0.7.4" +ts-node@^10.9.2: + version "10.9.2" + resolved "https://registry.yarnpkg.com/ts-node/-/ts-node-10.9.2.tgz#70f021c9e185bccdca820e26dc413805c101c71f" + integrity sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ== + dependencies: + "@cspotcode/source-map-support" "^0.8.0" + "@tsconfig/node10" "^1.0.7" + "@tsconfig/node12" "^1.0.7" + "@tsconfig/node14" "^1.0.0" + "@tsconfig/node16" "^1.0.2" + acorn "^8.4.1" + acorn-walk "^8.1.1" + arg "^4.1.0" + create-require "^1.1.0" + diff "^4.0.1" + make-error "^1.1.1" + v8-compile-cache-lib "^3.0.1" + yn "3.1.1" + tsconfig-paths@^3.15.0: version "3.15.0" resolved "https://registry.yarnpkg.com/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz#5299ec605e55b1abb23ec939ef15edaf483070d4" @@ -7553,6 +7629,11 @@ uuid@8.3.2, uuid@^8.3.0: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== +v8-compile-cache-lib@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" + integrity sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg== + v8-compile-cache@^2.2.0: version "2.3.0" resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz#2de19618c66dc247dcfb6f99338035d8245a2cee" @@ -8034,6 +8115,11 @@ yargs@^17.7.2: y18n "^5.0.5" yargs-parser "^21.1.1" +yn@3.1.1: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yn/-/yn-3.1.1.tgz#1e87401a09d767c1d5eab26a6e4c185182d2eb50" + integrity sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q== + yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" From 2971ea4735db04fa3925208a55180ce1ebe52cb6 Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Fri, 24 Oct 2025 14:51:22 +0200 Subject: [PATCH 012/402] Pass PR template to TitleAndDescriptionProvider (#8025) --- src/api/api.d.ts | 2 +- src/github/createPRViewProvider.ts | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/api/api.d.ts b/src/api/api.d.ts index 67d5a856d4..2dd11908e6 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 }[], template?: string }, token: CancellationToken): Promise<{ title: string, description?: string } | undefined>; } export interface ReviewerComments { diff --git a/src/github/createPRViewProvider.ts b/src/github/createPRViewProvider.ts index bb29a5b147..2c8cb242b5 100644 --- a/src/github/createPRViewProvider.ts +++ b/src/github/createPRViewProvider.ts @@ -1028,11 +1028,13 @@ export class CreatePullRequestViewProvider extends BaseCreatePullRequestViewProv private async getTitleAndDescriptionFromProvider(token: vscode.CancellationToken, searchTerm?: string) { return CreatePullRequestViewProvider.withProgress(async () => { try { + const templatePromise = this.getPullRequestTemplate(); // Fetch in parallel const { commitMessages, patches } = await this.getCommitsAndPatches(); const issues = await this.findIssueContext(commitMessages); + const template = await templatePromise; 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, template }, token); if (provider) { this.lastGeneratedTitleAndDescription = { ...result, providerTitle: provider.title }; From 6db883c715b177cdfabd17586499e4903eb11eab Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Fri, 24 Oct 2025 09:09:10 -0700 Subject: [PATCH 013/402] do not exclude markdown when detecting comment for Delegate codelens (#8044) --- src/issues/issueTodoProvider.ts | 2 +- src/issues/util.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/issues/issueTodoProvider.ts b/src/issues/issueTodoProvider.ts index 383e6007aa..fcdf556073 100644 --- a/src/issues/issueTodoProvider.ts +++ b/src/issues/issueTodoProvider.ts @@ -120,7 +120,7 @@ export class IssueTodoProvider implements vscode.CodeActionProvider, vscode.Code if (!todoInfo) { continue; } - if (!(await isComment(document, new vscode.Position(lineNumber, firstNonWhitespaceCharacterIndex)))) { + if (!(await isComment(document, new vscode.Position(lineNumber, firstNonWhitespaceCharacterIndex), []))) { continue; } const { match, search, insertIndex } = todoInfo; diff --git a/src/issues/util.ts b/src/issues/util.ts index e36207fa6d..955e8951ce 100644 --- a/src/issues/util.ts +++ b/src/issues/util.ts @@ -516,8 +516,8 @@ export async function pushAndCreatePR( } } -export async function isComment(document: vscode.TextDocument, position: vscode.Position): Promise { - if (document.languageId !== 'markdown' && document.languageId !== 'plaintext') { +export async function isComment(document: vscode.TextDocument, position: vscode.Position, excludedLanguageIds = ['markdown', 'plaintext']): Promise { + if (!excludedLanguageIds.includes(document.languageId)) { const tokenInfo = await vscode.languages.getTokenInformationAtPosition(document, position); if (tokenInfo.type !== vscode.StandardTokenType.Comment) { return false; From e4fa3030bbba9283668cc26f8b2f40eb8ef0cfa2 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Fri, 24 Oct 2025 09:59:58 -0700 Subject: [PATCH 014/402] fix multiDiff context buttons (#8045) --- package.json | 6 ++---- src/commands.ts | 22 +++++++++++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/package.json b/package.json index 42135dabe6..6aa3345a86 100644 --- a/package.json +++ b/package.json @@ -3550,12 +3550,10 @@ ], "chat/multiDiff/context": [ { - "command": "pr.checkoutFromDescription", - "when": "resourceScheme == copilot-swe-agent || resourceScheme == 'copilot-cloud-agent'" + "command": "pr.checkoutFromDescription" }, { - "command": "pr.applyChangesFromDescription", - "when": "resourceScheme == copilot-swe-agent || resourceScheme == 'copilot-cloud-agent'" + "command": "pr.applyChangesFromDescription" } ] }, diff --git a/src/commands.ts b/src/commands.ts index 9ee47145c6..ac8f75ffa4 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -624,6 +624,18 @@ export function registerCommands( return !!contextAsPath.path; } + function prNumberFromUriPath(path: string): number | undefined { + const trimPath = path.startsWith('/') ? path.substring(1) : path; + if (!Number.isNaN(Number(trimPath))) { + return Number(trimPath); + } + // This is a base64 encoded PR number like: /MTIz + const decoded = Number(Buffer.from(trimPath, 'base64').toString('utf8')); + if (!Number.isNaN(decoded)) { + return decoded; + } + } + context.subscriptions.push(vscode.commands.registerCommand('pr.checkoutFromDescription', async (ctx: OverviewContext | { path: string } | undefined) => { if (!ctx) { return vscode.window.showErrorMessage(vscode.l10n.t('No pull request context provided for checkout.')); @@ -631,9 +643,9 @@ export function registerCommands( if (contextHasPath(ctx)) { const { path } = ctx; - const prNumber = Number(Buffer.from(path.substring(1), 'base64').toString('utf8')); - if (Number.isNaN(prNumber)) { - return vscode.window.showErrorMessage(vscode.l10n.t('Unable to parse pull request number.')); + const prNumber = prNumberFromUriPath(path); + if (!prNumber) { + return vscode.window.showErrorMessage(vscode.l10n.t('No pull request number found in context path.')); } const folderManager = reposManager.folderManagers[0]; const pullRequest = await folderManager.fetchById(folderManager.gitHubRepositories[0], Number(prNumber)); @@ -659,8 +671,8 @@ export function registerCommands( if (contextHasPath(ctx)) { const { path } = ctx; - const prNumber = Number(Buffer.from(path.substring(1), 'base64').toString('utf8')); - if (Number.isNaN(prNumber)) { + const prNumber = prNumberFromUriPath(path); + if (!prNumber) { return vscode.window.showErrorMessage(vscode.l10n.t('Unable to parse pull request number.')); } From 1e1a8db2a4c4009bdb14329bf879e669f2e9f4d3 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Fri, 24 Oct 2025 10:08:50 -0700 Subject: [PATCH 015/402] Contribute open changes button to copilot-cloud-agent (#8039) * contribute open changes button to copilot-cloud-agent * formatting * use shared helper --- package.json | 3 +++ src/commands.ts | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 6aa3345a86..9a1abd217b 100644 --- a/package.json +++ b/package.json @@ -3554,6 +3554,9 @@ }, { "command": "pr.applyChangesFromDescription" + }, + { + "command": "pr.openChanges" } ] }, diff --git a/src/commands.ts b/src/commands.ts index ac8f75ffa4..6d6c46583f 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -716,7 +716,7 @@ export function registerCommands( })); context.subscriptions.push( - vscode.commands.registerCommand('pr.openChanges', async (pr: PRNode | RepositoryChangesNode | PullRequestModel | OverviewContext | ChatSessionWithPR | undefined) => { + vscode.commands.registerCommand('pr.openChanges', async (pr: PRNode | RepositoryChangesNode | PullRequestModel | OverviewContext | ChatSessionWithPR | { path: string } | undefined) => { if (pr === undefined) { // This is unexpected, but has happened a few times. Logger.error('Unexpectedly received undefined when picking a PR.', logId); @@ -739,7 +739,21 @@ export function registerCommands( preventDefaultContextMenuItems: true, }); pullRequestModel = resolved?.pr; - } else { + } + else if (contextHasPath(pr)) { + const { path } = pr; + const prNumber = prNumberFromUriPath(path); + if (!prNumber) { + return vscode.window.showErrorMessage(vscode.l10n.t('No pull request number found in context path.')); + } + const folderManager = reposManager.folderManagers[0]; + const pullRequest = await folderManager.fetchById(folderManager.gitHubRepositories[0], Number(prNumber)); + if (!pullRequest) { + return vscode.window.showErrorMessage(vscode.l10n.t('Unable to find pull request #{0}', prNumber.toString())); + } + pullRequestModel = pullRequest; + } + else { const resolved = await resolvePr(pr as OverviewContext); pullRequestModel = resolved?.pr; } From ca34c001cec3ad42033ee9116d331b13b274dcc0 Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Sun, 26 Oct 2025 14:52:55 -0700 Subject: [PATCH 016/402] remove typo after resource migration (#8047) --- src/github/pullRequestOverview.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/github/pullRequestOverview.ts b/src/github/pullRequestOverview.ts index 8b85298d74..1b05c200c6 100644 --- a/src/github/pullRequestOverview.ts +++ b/src/github/pullRequestOverview.ts @@ -509,7 +509,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel): Promise { try { - return vscode.commands.executeCommand('vscode.open', COPILOT_SWE_AGENT, SessionIdForPr.getResource(this._item.number, message.args.link.sessionIndex)); + const resource = SessionIdForPr.getResource(this._item.number, message.args.link.sessionIndex); + return vscode.commands.executeCommand('vscode.open', resource); } catch (e) { Logger.error(`Open session log view failed: ${formatError(e)}`, PullRequestOverviewPanel.ID); } From 75baa7d7a814d7b25a9db31522cec96f3cf3df5a Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:42:10 +0100 Subject: [PATCH 017/402] Move markdown/plaintext check into completion providers (#8049) --- src/issues/issueCompletionProvider.ts | 4 +- src/issues/issueLinkProvider.ts | 89 --------------------------- src/issues/issueTodoProvider.ts | 2 +- src/issues/userCompletionProvider.ts | 4 +- src/issues/util.ts | 11 ++-- 5 files changed, 12 insertions(+), 98 deletions(-) delete mode 100644 src/issues/issueLinkProvider.ts diff --git a/src/issues/issueCompletionProvider.ts b/src/issues/issueCompletionProvider.ts index db2506f354..63bd4a527d 100644 --- a/src/issues/issueCompletionProvider.ts +++ b/src/issues/issueCompletionProvider.ts @@ -100,7 +100,9 @@ export class IssueCompletionProvider implements vscode.CompletionItemProvider { return []; } - if ((document.languageId !== 'scminput') && (document.languageId !== 'git-commit') && !(await isComment(document, position))) { + const isPositionComment = document.languageId === 'plaintext' || document.languageId === 'markdown' || await isComment(document, position); + + if ((document.languageId !== 'scminput') && (document.languageId !== 'git-commit') && !isPositionComment) { return []; } diff --git a/src/issues/issueLinkProvider.ts b/src/issues/issueLinkProvider.ts deleted file mode 100644 index fafc7b7f6b..0000000000 --- a/src/issues/issueLinkProvider.ts +++ /dev/null @@ -1,89 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { StateManager } from './stateManager'; -import { - getIssue, - isComment, - MAX_LINE_LENGTH, -} from './util'; -import { EDITOR, WORD_WRAP } from '../common/settingKeys'; -import { ReposManagerState } from '../github/folderRepositoryManager'; -import { RepositoriesManager } from '../github/repositoriesManager'; -import { ISSUE_EXPRESSION, ParsedIssue, parseIssueExpressionOutput } from '../github/utils'; - -const MAX_LINE_COUNT = 2000; - -class IssueDocumentLink extends vscode.DocumentLink { - constructor( - range: vscode.Range, - public readonly mappedLink: { readonly value: string; readonly parsed: ParsedIssue }, - public readonly uri: vscode.Uri, - ) { - super(range); - } -} - -export class IssueLinkProvider implements vscode.DocumentLinkProvider { - constructor(private manager: RepositoriesManager, private stateManager: StateManager) { } - - async provideDocumentLinks( - document: vscode.TextDocument, - _token: vscode.CancellationToken, - ): Promise { - const links: vscode.DocumentLink[] = []; - const wraps: boolean = vscode.workspace.getConfiguration(EDITOR, document).get(WORD_WRAP, 'off') !== 'off'; - for (let i = 0; i < Math.min(document.lineCount, MAX_LINE_COUNT); i++) { - let searchResult = -1; - let lineOffset = 0; - const line = document.lineAt(i).text; - const lineLength = wraps ? line.length : Math.min(line.length, MAX_LINE_LENGTH); - let lineSubstring = line.substring(0, lineLength); - while ((searchResult = lineSubstring.search(ISSUE_EXPRESSION)) >= 0) { - const match = lineSubstring.match(ISSUE_EXPRESSION); - const parsed = parseIssueExpressionOutput(match); - if (match && parsed) { - const startPosition = new vscode.Position(i, searchResult + lineOffset); - if (await isComment(document, startPosition)) { - const link = new IssueDocumentLink( - new vscode.Range( - startPosition, - new vscode.Position(i, searchResult + lineOffset + match[0].length - 1), - ), - { value: match[0], parsed }, - document.uri, - ); - links.push(link); - } - } - lineOffset += searchResult + (match ? match[0].length : 0); - lineSubstring = line.substring(lineOffset, line.length); - } - } - return links; - } - - async resolveDocumentLink( - link: IssueDocumentLink, - _token: vscode.CancellationToken, - ): Promise { - if (this.manager.state === ReposManagerState.RepositoriesLoaded) { - const folderManager = this.manager.getManagerForFile(link.uri); - if (!folderManager) { - return; - } - const issue = await getIssue( - this.stateManager, - folderManager, - link.mappedLink.value, - link.mappedLink.parsed, - ); - if (issue) { - link.target = await vscode.env.asExternalUri(vscode.Uri.parse(issue.html_url)); - } - return link; - } - } -} diff --git a/src/issues/issueTodoProvider.ts b/src/issues/issueTodoProvider.ts index fcdf556073..383e6007aa 100644 --- a/src/issues/issueTodoProvider.ts +++ b/src/issues/issueTodoProvider.ts @@ -120,7 +120,7 @@ export class IssueTodoProvider implements vscode.CodeActionProvider, vscode.Code if (!todoInfo) { continue; } - if (!(await isComment(document, new vscode.Position(lineNumber, firstNonWhitespaceCharacterIndex), []))) { + if (!(await isComment(document, new vscode.Position(lineNumber, firstNonWhitespaceCharacterIndex)))) { continue; } const { match, search, insertIndex } = todoInfo; diff --git a/src/issues/userCompletionProvider.ts b/src/issues/userCompletionProvider.ts index 6ed98c11bf..fbd5413cfd 100644 --- a/src/issues/userCompletionProvider.ts +++ b/src/issues/userCompletionProvider.ts @@ -77,7 +77,9 @@ export class UserCompletionProvider implements vscode.CompletionItemProvider { return []; } - if (!this.isCodeownersFiles(document.uri) && (document.languageId !== 'scminput') && (document.languageId !== 'git-commit') && !(await isComment(document, position))) { + const isPositionComment = document.languageId === 'plaintext' || document.languageId === 'markdown' || await isComment(document, position); + + if (!this.isCodeownersFiles(document.uri) && (document.languageId !== 'scminput') && (document.languageId !== 'git-commit') && !isPositionComment) { return []; } diff --git a/src/issues/util.ts b/src/issues/util.ts index 955e8951ce..87a4e7e382 100644 --- a/src/issues/util.ts +++ b/src/issues/util.ts @@ -516,13 +516,12 @@ export async function pushAndCreatePR( } } -export async function isComment(document: vscode.TextDocument, position: vscode.Position, excludedLanguageIds = ['markdown', 'plaintext']): Promise { - if (!excludedLanguageIds.includes(document.languageId)) { - const tokenInfo = await vscode.languages.getTokenInformationAtPosition(document, position); - if (tokenInfo.type !== vscode.StandardTokenType.Comment) { - return false; - } +export async function isComment(document: vscode.TextDocument, position: vscode.Position): Promise { + const tokenInfo = await vscode.languages.getTokenInformationAtPosition(document, position); + if (tokenInfo.type !== vscode.StandardTokenType.Comment) { + return false; } + return true; } From 01aea5f980095c91b63b584a37f1da16a45b4127 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Tue, 28 Oct 2025 16:04:56 +0100 Subject: [PATCH 018/402] PR editor tab icon not theme responsive (#8052) Fixes #8051 --- .../icons/dark/{pr_webview.svg => git-pull-request_webview.svg} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename resources/icons/dark/{pr_webview.svg => git-pull-request_webview.svg} (100%) diff --git a/resources/icons/dark/pr_webview.svg b/resources/icons/dark/git-pull-request_webview.svg similarity index 100% rename from resources/icons/dark/pr_webview.svg rename to resources/icons/dark/git-pull-request_webview.svg From c07ecc23914dbc443383d2c9c2870d63f430142e Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Wed, 29 Oct 2025 12:08:18 +0100 Subject: [PATCH 019/402] Switch icons (#8053) * merge and skip to codicons * skip to codicons * Switch icons * Revert "Switch icons" This reverts commit ae9d496f1815c6835a40aba50b7f7fbed651ba13. * Switch icons --- resources/icons/codicons/merge.svg | 1 + resources/icons/codicons/skip.svg | 1 + resources/icons/pr_merge.svg | 3 --- resources/icons/skip.svg | 1 - webviews/components/icon.tsx | 6 +++--- 5 files changed, 5 insertions(+), 7 deletions(-) create mode 100644 resources/icons/codicons/merge.svg create mode 100644 resources/icons/codicons/skip.svg delete mode 100644 resources/icons/pr_merge.svg delete mode 100644 resources/icons/skip.svg diff --git a/resources/icons/codicons/merge.svg b/resources/icons/codicons/merge.svg new file mode 100644 index 0000000000..2692deecee --- /dev/null +++ b/resources/icons/codicons/merge.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/codicons/skip.svg b/resources/icons/codicons/skip.svg new file mode 100644 index 0000000000..f9dcd2df81 --- /dev/null +++ b/resources/icons/codicons/skip.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/resources/icons/pr_merge.svg b/resources/icons/pr_merge.svg deleted file mode 100644 index cf26950f84..0000000000 --- a/resources/icons/pr_merge.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/resources/icons/skip.svg b/resources/icons/skip.svg deleted file mode 100644 index b7368b71f2..0000000000 --- a/resources/icons/skip.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/webviews/components/icon.tsx b/webviews/components/icon.tsx index 6f0cddcf5c..45f2bb938e 100644 --- a/webviews/components/icon.tsx +++ b/webviews/components/icon.tsx @@ -46,10 +46,10 @@ export const tasklistIcon = ; export const trashIcon = ; export const warningIcon = ; +export const prMergeIcon = ; +export const skipIcon = ; // Other icons export const copilotErrorIcon = ; export const copilotInProgressIcon = ; -export const copilotSuccessIcon = ; -export const prMergeIcon = ; -export const skipIcon = ; +export const copilotSuccessIcon = ; \ No newline at end of file From dd498989454308ccf160c58f228cdb379d76bc18 Mon Sep 17 00:00:00 2001 From: Lee Murray Date: Wed, 29 Oct 2025 12:31:10 +0000 Subject: [PATCH 020/402] Include ul in comment-body spacing rule (#8055) --- webviews/editorWebview/index.css | 1 + 1 file changed, 1 insertion(+) diff --git a/webviews/editorWebview/index.css b/webviews/editorWebview/index.css index 37da24989a..98042167f4 100644 --- a/webviews/editorWebview/index.css +++ b/webviews/editorWebview/index.css @@ -437,6 +437,7 @@ body .comment-container .review-comment-container .review-comment-body { body .comment-container .comment-body>p, body .comment-container .comment-body>div>p, +body .comment-container .comment-body>div>ul, .comment-container .review-body>p { margin-top: 0; line-height: 1.5em; From 34c4d49e9947c32e207e7e1be8c21cdbe7bbb228 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Wed, 29 Oct 2025 16:49:15 +0100 Subject: [PATCH 021/402] Fix create view not updating merge when view is hidden (#8056) --- src/github/createPRViewProvider.ts | 36 +++++++++++++----------------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/src/github/createPRViewProvider.ts b/src/github/createPRViewProvider.ts index 2c8cb242b5..ce45d6d987 100644 --- a/src/github/createPRViewProvider.ts +++ b/src/github/createPRViewProvider.ts @@ -634,28 +634,22 @@ export class CreatePullRequestViewProvider extends BaseCreatePullRequestViewProv } public async setDefaultCompareBranch(compareBranch: Branch | undefined) { - const branchChanged = compareBranch && (compareBranch.name !== this.model.compareBranch); - const currentCompareRemote = this._folderRepositoryManager.gitHubRepositories.find(repo => repo.remote.owner === this.model.compareOwner)?.remote.remoteName; - const branchRemoteChanged = compareBranch && (compareBranch.upstream?.remote !== currentCompareRemote); - if (branchChanged || branchRemoteChanged) { - this._defaultCompareBranch = compareBranch!.name!; - this.model.setCompareBranch(compareBranch!.name); - this.changeBranch(compareBranch!.name!, false).then(async titleAndDescription => { - const params: Partial = { - defaultTitle: titleAndDescription.title, - defaultDescription: titleAndDescription.description, - compareBranch: compareBranch?.name, - defaultCompareBranch: compareBranch?.name, - warning: await this.existingPRMessage(), - }; - if (!branchRemoteChanged) { - return this._postMessage({ - command: 'pr.initialize', - params, - }); - } + this._defaultCompareBranch = compareBranch!.name!; + this.model.setCompareBranch(compareBranch!.name); + this.changeBranch(compareBranch!.name!, false).then(async titleAndDescription => { + const params: Partial = { + defaultTitle: titleAndDescription.title, + defaultDescription: titleAndDescription.description, + compareBranch: compareBranch?.name, + defaultCompareBranch: compareBranch?.name, + warning: await this.existingPRMessage(), + }; + return this._postMessage({ + command: 'pr.initialize', + params, }); - } + }); + } public override show(compareBranch?: Branch): void { From f7f1a72e71796f91ca243da8988154d511f5bdc4 Mon Sep 17 00:00:00 2001 From: Matt Bierner <12821956+mjbvz@users.noreply.github.com> Date: Thu, 30 Oct 2025 03:33:55 -0700 Subject: [PATCH 022/402] Adopt using uuids for generating webview nonces (#8057) --- src/common/uuid.ts | 58 +++++++++++++++++++++++++++ src/common/webview.ts | 9 ----- src/github/activityBarViewProvider.ts | 5 ++- src/github/createPRViewProvider.ts | 5 ++- src/github/issueOverview.ts | 5 ++- 5 files changed, 67 insertions(+), 15 deletions(-) create mode 100644 src/common/uuid.ts diff --git a/src/common/uuid.ts b/src/common/uuid.ts new file mode 100644 index 0000000000..bdccc3f5db --- /dev/null +++ b/src/common/uuid.ts @@ -0,0 +1,58 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/** + * Copied from vscode/src/vs/base/common/uuid.ts + */ +export function generateUuid(): string { + // use `randomUUID` if possible + if (typeof crypto.randomUUID === 'function') { + // see https://developer.mozilla.org/en-US/docs/Web/API/Window/crypto + // > Although crypto is available on all windows, the returned Crypto object only has one + // > usable feature in insecure contexts: the getRandomValues() method. + // > In general, you should use this API only in secure contexts. + + return crypto.randomUUID.bind(crypto)(); + } + + // prep-work + const _data = new Uint8Array(16); + const _hex: string[] = []; + for (let i = 0; i < 256; i++) { + _hex.push(i.toString(16).padStart(2, '0')); + } + + // get data + crypto.getRandomValues(_data); + + // set version bits + _data[6] = (_data[6] & 0x0f) | 0x40; + _data[8] = (_data[8] & 0x3f) | 0x80; + + // print as string + let i = 0; + let result = ''; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += '-'; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += '-'; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += '-'; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += '-'; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + result += _hex[_data[i++]]; + return result; +} \ No newline at end of file diff --git a/src/common/webview.ts b/src/common/webview.ts index 02b59a029a..f887fd349b 100644 --- a/src/common/webview.ts +++ b/src/common/webview.ts @@ -22,15 +22,6 @@ export interface IReplyMessage { res?: any; } -export function getNonce() { - let text = ''; - const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'; - for (let i = 0; i < 32; i++) { - text += possible.charAt(Math.floor(Math.random() * possible.length)); - } - return text; -} - export class WebviewBase extends Disposable { protected _webview?: vscode.Webview; diff --git a/src/github/activityBarViewProvider.ts b/src/github/activityBarViewProvider.ts index 881e3a49e1..b9243558b7 100644 --- a/src/github/activityBarViewProvider.ts +++ b/src/github/activityBarViewProvider.ts @@ -17,7 +17,8 @@ import { emojify, ensureEmojis } from '../common/emoji'; import { disposeAll } from '../common/lifecycle'; import { ReviewEvent } from '../common/timelineEvent'; import { formatError } from '../common/utils'; -import { getNonce, IRequestMessage, WebviewViewBase } from '../common/webview'; +import { generateUuid } from '../common/uuid'; +import { IRequestMessage, WebviewViewBase } from '../common/webview'; import { ReviewManager } from '../view/reviewManager'; export class PullRequestViewProvider extends WebviewViewBase implements vscode.WebviewViewProvider { @@ -489,7 +490,7 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W } private _getHtmlForWebview() { - const nonce = getNonce(); + const nonce = generateUuid(); const uri = vscode.Uri.joinPath(this._extensionUri, 'dist', 'webview-open-pr-view.js'); diff --git a/src/github/createPRViewProvider.ts b/src/github/createPRViewProvider.ts index ce45d6d987..40763443bb 100644 --- a/src/github/createPRViewProvider.ts +++ b/src/github/createPRViewProvider.ts @@ -38,7 +38,8 @@ import { } from '../common/settingKeys'; import { ITelemetry } from '../common/telemetry'; import { asPromise, compareIgnoreCase, formatError, promiseWithTimeout } from '../common/utils'; -import { getNonce, IRequestMessage, WebviewViewBase } from '../common/webview'; +import { generateUuid } from '../common/uuid'; +import { IRequestMessage, WebviewViewBase } from '../common/webview'; import { PREVIOUS_CREATE_METHOD } from '../extensionState'; import { CreatePullRequestDataModel } from '../view/createPullRequestDataModel'; @@ -549,7 +550,7 @@ export abstract class BaseCreatePullRequestViewProvider extends WebviewBase { public static ID: string = 'IssueOverviewPanel'; @@ -705,7 +706,7 @@ export class IssueOverviewPanel extends W } protected getHtmlForWebview() { - const nonce = getNonce(); + const nonce = generateUuid(); const uri = vscode.Uri.joinPath(this._extensionUri, 'dist', 'webview-pr-description.js'); From 161802de2ca0400908b7ad5ba7f0c5f1c2f458cf Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 30 Oct 2025 10:38:23 +0000 Subject: [PATCH 023/402] Fix reviewer icons horizontal misalignment in pull request view (#8013) * Initial plan * Initial investigation of reviewer icon misalignment issue Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Fix reviewer icons misalignment by adding align-items: center Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Add justify-content: flex-end to align reviewer icons horizontally Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Hide aria alert div to prevent layout space consumption Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * revert * Remove extra css --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- webviews/common/common.css | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/webviews/common/common.css b/webviews/common/common.css index a198813a65..24e4d5a07d 100644 --- a/webviews/common/common.css +++ b/webviews/common/common.css @@ -238,6 +238,13 @@ body img.avatar { gap: 4px; } +.reviewer-icons [role='alert'] { + position: absolute; + width: 0; + height: 0; + overflow: hidden; +} + .push-right { margin-left: auto; } From 508c1b334552dc253c208075f0d74c074b62e8c5 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Thu, 30 Oct 2025 17:14:26 +0100 Subject: [PATCH 024/402] 0.120.2 changelog (#8064) --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 17c6227717..3eb84103c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.120.2 + +### Fixes + +- Unable to open PR webview within VSCode. https://github.com/microsoft/vscode-pull-request-github/issues/8028 + ## 0.120.1 ### Fixes From 670ad964a4377b72cc8e7a4abb602c0a57834df3 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Thu, 30 Oct 2025 17:14:56 +0100 Subject: [PATCH 025/402] Github Pull Request tab won't open if branch names are reused (#8063) Fixes #8007 --- src/github/pullRequestGitHelper.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/github/pullRequestGitHelper.ts b/src/github/pullRequestGitHelper.ts index 7ad603dfac..599a3eda6d 100644 --- a/src/github/pullRequestGitHelper.ts +++ b/src/github/pullRequestGitHelper.ts @@ -329,8 +329,9 @@ export class PullRequestGitHelper { ): Promise { try { const configKey = this.getMetadataKeyForBranch(branchName); - const configValue = await repository.getConfig(configKey); - return PullRequestGitHelper.parsePullRequestMetadata(configValue); + const allConfigs = await repository.getConfigs(); + const matchingConfigs = allConfigs.filter(config => config.key === configKey).sort((a, b) => b.value < a.value ? 1 : -1); + return PullRequestGitHelper.parsePullRequestMetadata(matchingConfigs[0].value); } catch (_) { return; } From fd96ed07ba5edb885fbdd3a48708a8db3489afe9 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Fri, 31 Oct 2025 14:29:38 +0100 Subject: [PATCH 026/402] Fix load more and duplicate issues (#8066) --- src/github/folderRepositoryManager.ts | 6 ++++++ src/view/prsTreeModel.ts | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index 9ac0cd2832..cf73691d48 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -1082,7 +1082,13 @@ export class FolderRepositoryManager extends Disposable { } }; + const activeGitHubRemotes = await this.getActiveGitHubRemotes(this._allGitHubRemotes); + const githubRepositories = this._githubRepositories.filter(repo => { + if (!activeGitHubRemotes.find(r => r.equals(repo.remote))) { + return false; + } + const info = this._repositoryPageInformation.get(repo.remote.url.toString() + queryId); // If we are in case 1 or 3, don't filter out repos that are out of pages, as we will be querying from the start. return info && (options.fetchNextPage === false || info.hasMorePages !== false); diff --git a/src/view/prsTreeModel.ts b/src/view/prsTreeModel.ts index 491ab5cacc..2ceec9d84b 100644 --- a/src/view/prsTreeModel.ts +++ b/src/view/prsTreeModel.ts @@ -355,9 +355,9 @@ export class PrsTreeModel extends Disposable { try { let maxKnownPR: number | undefined; const cache = this.getFolderCache(folderRepoManager); + const cachedPRs = cache.get(query)!; if (!fetchNextPage && cache.has(query)) { const shouldRefresh = await this._testIfRefreshNeeded(cache.get(query)!, query, folderRepoManager); - const cachedPRs = cache.get(query)!; maxKnownPR = cachedPRs.maxKnownPR; if (!shouldRefresh) { cachedPRs.clearRequested = false; @@ -377,6 +377,9 @@ export class PrsTreeModel extends Disposable { { fetchNextPage, fetchOnePagePerRepo }, query, ); + if (fetchNextPage) { + prs.items = cachedPRs?.items.items.concat(prs.items) ?? prs.items; + } cache.set(query, { clearRequested: false, items: prs, maxKnownPR }); prs.items.forEach(pr => this._allCachedPRs.add(pr)); @@ -404,6 +407,9 @@ export class PrsTreeModel extends Disposable { PRType.All, { fetchNextPage } ); + if (fetchNextPage) { + prs.items = allCache?.items.items.concat(prs.items) ?? prs.items; + } cache.set(PRType.All, { clearRequested: false, items: prs, maxKnownPR: undefined }); prs.items.forEach(pr => this._allCachedPRs.add(pr)); From f4b2b245af78126cca76dc9ad605133200a7c016 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Fri, 31 Oct 2025 14:31:49 +0100 Subject: [PATCH 027/402] Topmost comment info bottom border missing (#8067) Fixes #8031 --- webviews/editorWebview/index.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webviews/editorWebview/index.css b/webviews/editorWebview/index.css index 98042167f4..f73c8c39c3 100644 --- a/webviews/editorWebview/index.css +++ b/webviews/editorWebview/index.css @@ -155,7 +155,7 @@ body .comment-container.review { position: relative; } -body #main>.comment-container>.review-comment-container>.review-comment-header:not(:nth-last-child(2)) { +body #main .comment-container>.review-comment-container>.review-comment-header:not(:nth-last-child(2)) { border-bottom: 1px solid var(--vscode-editorHoverWidget-border); } From b10dac6e0bc65f3a2e8b8c0693fe0182955d1e6c Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Fri, 31 Oct 2025 08:36:59 -0700 Subject: [PATCH 028/402] Enable markdown alert syntax in PR comments (#8068) --- package.json | 1 + .../vscode.proposed.markdownAlertSyntax.d.ts | 29 +++++++++++++++++++ src/github/prComment.ts | 12 ++++++-- 3 files changed, 39 insertions(+), 3 deletions(-) create mode 100644 src/@types/vscode.proposed.markdownAlertSyntax.d.ts diff --git a/package.json b/package.json index 9a1abd217b..8bdb9bd283 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "contribShareMenu", "diffCommand", "languageModelToolResultAudience", + "markdownAlertSyntax", "quickDiffProvider", "remoteCodingAgents", "shareProvider", diff --git a/src/@types/vscode.proposed.markdownAlertSyntax.d.ts b/src/@types/vscode.proposed.markdownAlertSyntax.d.ts new file mode 100644 index 0000000000..bb02da446f --- /dev/null +++ b/src/@types/vscode.proposed.markdownAlertSyntax.d.ts @@ -0,0 +1,29 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // @kycutler https://github.com/microsoft/vscode/issues/209652 + + export interface MarkdownString { + + /** + * Indicates that this markdown string can contain alert syntax. Defaults to `false`. + * + * When `supportAlertSyntax` is true, the markdown renderer will parse GitHub-style alert syntax: + * + * ```markdown + * > [!NOTE] + * > This is a note alert + * + * > [!WARNING] + * > This is a warning alert + * ``` + * + * Supported alert types: `NOTE`, `TIP`, `IMPORTANT`, `WARNING`, `CAUTION`. + */ + supportAlertSyntax?: boolean; + } +} diff --git a/src/github/prComment.ts b/src/github/prComment.ts index f35cebc8b6..4547387758 100644 --- a/src/github/prComment.ts +++ b/src/github/prComment.ts @@ -190,7 +190,9 @@ export class TemporaryComment extends CommentBase { } get body(): string | vscode.MarkdownString { - return new vscode.MarkdownString(this.input); + const s = new vscode.MarkdownString(this.input); + s.supportAlertSyntax = true; + return s; } get author(): vscode.CommentAuthorInformation { @@ -477,11 +479,15 @@ ${lineContents} if (this.mode === vscode.CommentMode.Editing) { return this._rawBody; } - return new vscode.MarkdownString(this.replacedBody); + const s = new vscode.MarkdownString(this.replacedBody); + s.supportAlertSyntax = true; + return s; } protected getCancelEditBody() { - return new vscode.MarkdownString(this.rawComment.body); + const s = new vscode.MarkdownString(this.rawComment.body); + s.supportAlertSyntax = true; + return s; } } From b0b51eb46df34b378365490d5f14c71db79d4b39 Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Fri, 31 Oct 2025 09:03:43 -0700 Subject: [PATCH 029/402] Use markdown in PR tree labels (#8069) * Use markdown in PR tree labels * Make escape syntax more specific * Fix test --- package.json | 1 + ...vscode.proposed.treeItemMarkdownLabel.d.ts | 23 ++++++++++ src/test/view/prsTree.test.ts | 8 +++- src/view/treeNodes/pullRequestNode.ts | 45 ++++++++++++------- 4 files changed, 60 insertions(+), 17 deletions(-) create mode 100644 src/@types/vscode.proposed.treeItemMarkdownLabel.d.ts diff --git a/package.json b/package.json index 8bdb9bd283..ea564de1b5 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "remoteCodingAgents", "shareProvider", "tokenInformation", + "treeItemMarkdownLabel", "treeViewMarkdownMessage" ], "version": "0.120.0", diff --git a/src/@types/vscode.proposed.treeItemMarkdownLabel.d.ts b/src/@types/vscode.proposed.treeItemMarkdownLabel.d.ts new file mode 100644 index 0000000000..6150fa0667 --- /dev/null +++ b/src/@types/vscode.proposed.treeItemMarkdownLabel.d.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // @kycutler https://github.com/microsoft/vscode/issues/271523 + + export interface TreeItemLabel2 { + highlights?: [number, number][]; + + /** + * A human-readable string or MarkdownString describing the {@link TreeItem Tree item}. + * + * When using MarkdownString, only the following Markdown syntax is supported: + * - Icons (e.g., `$(icon-name)`, when the `supportIcons` flag is also set) + * - Bold, italics, and strikethrough formatting, but only when the syntax wraps the entire string + * (e.g., `**bold**`, `_italic_`, `~~strikethrough~~`) + */ + label: string | MarkdownString; + } +} diff --git a/src/test/view/prsTree.test.ts b/src/test/view/prsTree.test.ts index cf082dd4b9..fc98daf733 100644 --- a/src/test/view/prsTree.test.ts +++ b/src/test/view/prsTree.test.ts @@ -256,14 +256,18 @@ describe('GitHub Pull Requests view', function () { assert.strictEqual(localChildren.length, 2); const [localItem0, localItem1] = await Promise.all(localChildren.map(node => node.getTreeItem())); - assert.strictEqual(localItem0.label, 'zero'); + const label0 = (localItem0.label as vscode.TreeItemLabel2).label; + assert.ok(label0 instanceof vscode.MarkdownString); + assert.equal(label0.value, 'zero'); assert.strictEqual(localItem0.tooltip, undefined); assert.strictEqual(localItem0.description, 'by @me'); assert.strictEqual(localItem0.collapsibleState, vscode.TreeItemCollapsibleState.Collapsed); assert.strictEqual(localItem0.contextValue, 'pullrequest:local:nonactive:hasHeadRef'); assert.deepStrictEqual(localItem0.iconPath!.toString(), 'https://avatars.com/me.jpg'); - assert.strictEqual(localItem1.label, '✓ one'); + const label1 = (localItem1.label as vscode.TreeItemLabel2).label; + assert.ok(label1 instanceof vscode.MarkdownString); + assert.equal(label1.value, '$(check) one'); assert.strictEqual(localItem1.tooltip, undefined); assert.strictEqual(localItem1.description, 'by @you'); assert.strictEqual(localItem1.collapsibleState, vscode.TreeItemCollapsibleState.Collapsed); diff --git a/src/view/treeNodes/pullRequestNode.ts b/src/view/treeNodes/pullRequestNode.ts index 2ee41b518c..6ebae209e0 100644 --- a/src/view/treeNodes/pullRequestNode.ts +++ b/src/view/treeNodes/pullRequestNode.ts @@ -297,30 +297,45 @@ export class PRNode extends TreeNode implements vscode.CommentingRangeProvider2 } } - async getTreeItem(): Promise { + private _getLabel(): string { const currentBranchIsForThisPR = this.pullRequestModel.equals(this._folderReposManager.activePullRequest); + const { title, number, author, isDraft } = this.pullRequestModel; + let label = ''; - const { title, number, author, isDraft, html_url } = this.pullRequestModel; - let labelTitle = this.pullRequestModel.title.length > 50 ? `${this.pullRequestModel.title.substring(0, 50)}...` : this.pullRequestModel.title; - if (COPILOT_ACCOUNTS[this.pullRequestModel.author.login]) { - labelTitle = labelTitle.replace('[WIP]', ''); + if (currentBranchIsForThisPR) { + label += '$(check) '; } - const login = author.specialDisplayName ?? author.login; - - const hasNotification = this._notificationProvider.hasNotification(this.pullRequestModel) || this._prsTreeModel.hasCopilotNotification(this.pullRequestModel.remote.owner, this.pullRequestModel.remote.repositoryName, this.pullRequestModel.number); - - const formattedPRNumber = number.toString(); - let labelPrefix = currentBranchIsForThisPR ? '✓ ' : ''; if ( vscode.workspace .getConfiguration(PR_SETTINGS_NAMESPACE) .get(SHOW_PULL_REQUEST_NUMBER_IN_TREE, false) ) { - labelPrefix += `#${formattedPRNumber}: `; + label += `#${number}: `; + } + + let labelTitle = title.length > 50 ? `${title.substring(0, 50)}...` : title; + if (COPILOT_ACCOUNTS[author.login]) { + labelTitle = labelTitle.replace('[WIP]', ''); } + // Escape any $(...) syntax to avoid rendering PR titles as icons. + label += labelTitle.replace(/\$\([a-zA-Z0-9~-]+\)/g, '\\$&'); - const label = `${labelPrefix}${isDraft ? '\u270E ' : ''}${labelTitle}`; + if (isDraft) { + label = `_${label}_`; + } + + return label; + } + + async getTreeItem(): Promise { + const currentBranchIsForThisPR = this.pullRequestModel.equals(this._folderReposManager.activePullRequest); + const { title, number, author, isDraft, html_url } = this.pullRequestModel; + const login = author.specialDisplayName ?? author.login; + const hasNotification = this._notificationProvider.hasNotification(this.pullRequestModel) || this._prsTreeModel.hasCopilotNotification(this.pullRequestModel.remote.owner, this.pullRequestModel.remote.repositoryName, this.pullRequestModel.number); + const label: vscode.TreeItemLabel2 = { + label: new vscode.MarkdownString(this._getLabel(), true) + }; const description = `by @${login}`; const command = { title: vscode.l10n.t('View Pull Request Description'), @@ -329,7 +344,7 @@ export class PRNode extends TreeNode implements vscode.CommentingRangeProvider2 }; return { - label, + label: label as vscode.TreeItemLabel, id: `${this.parent instanceof TreeNode ? (this.parent.id ?? this.parent.label) : ''}${html_url}${this._isLocal ? this.pullRequestModel.localBranchName : ''}`, // unique id stable across checkout status description, collapsibleState: 1, @@ -341,7 +356,7 @@ export class PRNode extends TreeNode implements vscode.CommentingRangeProvider2 (((this.pullRequestModel.item.isRemoteHeadDeleted && !this._isLocal) || !this._folderReposManager.isPullRequestAssociatedWithOpenRepository(this.pullRequestModel)) ? '' : ':hasHeadRef'), iconPath: await this._getIcon(), accessibilityInformation: { - label: `${isDraft ? 'Draft ' : ''}Pull request number ${formattedPRNumber}: ${title} by ${login}` + label: `${isDraft ? 'Draft ' : ''}Pull request number ${number}: ${title} by ${login}` }, resourceUri: createPRNodeUri(this.pullRequestModel, this.parent instanceof CategoryTreeNode && this.parent.isCopilot ? true : undefined), command From 2124448af445edbbdb38c70c5f22f39fbd0f014c Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Fri, 31 Oct 2025 17:38:31 +0100 Subject: [PATCH 030/402] Add url handling for checking out a PR (#8070) * Add url handling for checking out a PR * Copilot review --- src/@types/git.d.ts | 118 ++++++++++++++++++++++++++++++--- src/api/api.d.ts | 3 +- src/api/api1.ts | 10 +++ src/common/uri.ts | 5 +- src/extension.ts | 7 +- src/gitProviders/builtinGit.ts | 3 + src/gitProviders/vslsguest.ts | 4 +- src/uriHandler.ts | 93 ++++++++++++++++++++++++-- 8 files changed, 225 insertions(+), 18 deletions(-) diff --git a/src/@types/git.d.ts b/src/@types/git.d.ts index 702e382fee..718d8f4b6b 100644 --- a/src/@types/git.d.ts +++ b/src/@types/git.d.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Uri, Event, Disposable, ProviderResult, Command, CancellationToken } from 'vscode'; +import { Uri, Event, Disposable, ProviderResult, Command, CancellationToken, SourceControlHistoryItem } from 'vscode'; export { ProviderResult } from 'vscode'; export interface Git { @@ -16,7 +16,8 @@ export interface InputBox { export const enum ForcePushMode { Force, - ForceWithLease + ForceWithLease, + ForceWithLeaseIfIncludes, } export const enum RefType { @@ -29,12 +30,14 @@ export interface Ref { readonly type: RefType; readonly name?: string; readonly commit?: string; + readonly commitDetails?: Commit; readonly remote?: string; } export interface UpstreamRef { readonly remote: string; readonly name: string; + readonly commit?: string; } export interface Branch extends Ref { @@ -43,6 +46,12 @@ export interface Branch extends Ref { readonly behind?: number; } +export interface CommitShortStat { + readonly files: number; + readonly insertions: number; + readonly deletions: number; +} + export interface Commit { readonly hash: string; readonly message: string; @@ -51,6 +60,7 @@ export interface Commit { readonly authorName?: string; readonly authorEmail?: string; readonly commitDate?: Date; + readonly shortStat?: CommitShortStat; } export interface Submodule { @@ -78,6 +88,8 @@ export const enum Status { UNTRACKED, IGNORED, INTENT_TO_ADD, + INTENT_TO_RENAME, + TYPE_CHANGED, ADDED_BY_US, ADDED_BY_THEM, @@ -103,6 +115,7 @@ export interface Change { export interface RepositoryState { readonly HEAD: Branch | undefined; + readonly refs: Ref[]; readonly remotes: Remote[]; readonly submodules: Submodule[]; readonly rebaseCommit: Commit | undefined; @@ -110,6 +123,7 @@ export interface RepositoryState { readonly mergeChanges: Change[]; readonly indexChanges: Change[]; readonly workingTreeChanges: Change[]; + readonly untrackedChanges: Change[]; readonly onDidChange: Event; } @@ -126,6 +140,16 @@ export interface LogOptions { /** Max number of log entries to retrieve. If not specified, the default is 32. */ readonly maxEntries?: number; readonly path?: string; + /** A commit range, such as "0a47c67f0fb52dd11562af48658bc1dff1d75a38..0bb4bdea78e1db44d728fd6894720071e303304f" */ + readonly range?: string; + readonly reverse?: boolean; + readonly sortByAuthorDate?: boolean; + readonly shortStats?: boolean; + readonly author?: string; + readonly grep?: string; + readonly refNames?: string[]; + readonly maxParents?: number; + readonly skip?: number; } export interface CommitOptions { @@ -155,10 +179,27 @@ export interface FetchOptions { depth?: number; } +export interface InitOptions { + defaultBranch?: string; +} + +export interface CloneOptions { + parentPath?: Uri; + /** + * ref is only used if the repository cache is missed. + */ + ref?: string; + recursive?: boolean; + /** + * If no postCloneAction is provided, then the users setting for git.openAfterClone is used. + */ + postCloneAction?: 'none'; +} + export interface RefQuery { readonly contains?: string; readonly count?: number; - readonly pattern?: string; + readonly pattern?: string | string[]; readonly sort?: 'alphabetically' | 'committerdate'; } @@ -173,9 +214,13 @@ export interface Repository { readonly state: RepositoryState; readonly ui: RepositoryUIState; + readonly onDidCommit: Event; + readonly onDidCheckout: Event; + getConfigs(): Promise<{ key: string; value: string; }[]>; getConfig(key: string): Promise; setConfig(key: string, value: string): Promise; + unsetConfig(key: string): Promise; getGlobalConfig(key: string): Promise; getObjectDetails(treeish: string, path: string): Promise<{ mode: string, object: string, size: number }>; @@ -211,9 +256,11 @@ export interface Repository { getBranchBase(name: string): Promise; setBranchUpstream(name: string, upstream: string): Promise; + checkIgnore(paths: string[]): Promise>; + getRefs(query: RefQuery, cancellationToken?: CancellationToken): Promise; - getMergeBase(ref1: string, ref2: string): Promise; + getMergeBase(ref1: string, ref2: string): Promise; tag(name: string, upstream: string): Promise; deleteTag(name: string): Promise; @@ -236,6 +283,10 @@ export interface Repository { commit(message: string, opts?: CommitOptions): Promise; merge(ref: string): Promise; mergeAbort(): Promise; + + applyStash(index?: number): Promise; + popStash(index?: number): Promise; + dropStash(index?: number): Promise; } export interface RemoteSource { @@ -276,6 +327,38 @@ export interface PushErrorHandler { handlePushError(repository: Repository, remote: Remote, refspec: string, error: Error & { gitErrorCode: GitErrorCodes }): Promise; } +export interface BranchProtection { + readonly remote: string; + readonly rules: BranchProtectionRule[]; +} + +export interface BranchProtectionRule { + readonly include?: string[]; + readonly exclude?: string[]; +} + +export interface BranchProtectionProvider { + onDidChangeBranchProtection: Event; + provideBranchProtection(): BranchProtection[]; +} + +export interface AvatarQueryCommit { + readonly hash: string; + readonly authorName?: string; + readonly authorEmail?: string; +} + +export interface AvatarQuery { + readonly commits: AvatarQueryCommit[]; + readonly size: number; +} + +export interface SourceControlHistoryItemDetailsProvider { + provideAvatar(repository: Repository, query: AvatarQuery): ProviderResult>; + provideHoverCommands(repository: Repository): ProviderResult; + provideMessageLinks(repository: Repository, message: string): ProviderResult; +} + export type APIState = 'uninitialized' | 'initialized'; export interface PublishEvent { @@ -294,14 +377,24 @@ export interface GitAPI { toGitUri(uri: Uri, ref: string): Uri; getRepository(uri: Uri): Repository | null; - init(root: Uri): Promise; - openRepository(root: Uri): Promise + getRepositoryRoot(uri: Uri): Promise; + getRepositoryWorkspace(uri: Uri): Promise; + init(root: Uri, options?: InitOptions): Promise; + /** + * Checks the cache of known cloned repositories, and clones if the repository is not found. + * Make sure to pass `postCloneAction` 'none' if you want to have the uri where you can find the repository returned. + * @returns The URI of a folder or workspace file which, when opened, will open the cloned repository. + */ + clone(uri: Uri, options?: CloneOptions): Promise; + openRepository(root: Uri): Promise; registerRemoteSourcePublisher(publisher: RemoteSourcePublisher): Disposable; registerRemoteSourceProvider(provider: RemoteSourceProvider): Disposable; registerCredentialsProvider(provider: CredentialsProvider): Disposable; registerPostCommitCommandsProvider(provider: PostCommitCommandsProvider): Disposable; registerPushErrorHandler(handler: PushErrorHandler): Disposable; + registerBranchProtectionProvider(root: Uri, provider: BranchProtectionProvider): Disposable; + registerSourceControlHistoryItemDetailsProvider(provider: SourceControlHistoryItemDetailsProvider): Disposable; } export interface GitExtension { @@ -319,21 +412,25 @@ export interface GitExtension { * @param version Version number. * @returns API instance */ - getAPI(version: 1): GitAPI; + getAPI(version: 1): API; } export const enum GitErrorCodes { BadConfigFile = 'BadConfigFile', + BadRevision = 'BadRevision', AuthenticationFailed = 'AuthenticationFailed', NoUserNameConfigured = 'NoUserNameConfigured', NoUserEmailConfigured = 'NoUserEmailConfigured', NoRemoteRepositorySpecified = 'NoRemoteRepositorySpecified', NotAGitRepository = 'NotAGitRepository', + NotASafeGitRepository = 'NotASafeGitRepository', NotAtRepositoryRoot = 'NotAtRepositoryRoot', Conflict = 'Conflict', StashConflict = 'StashConflict', UnmergedChanges = 'UnmergedChanges', PushRejected = 'PushRejected', + ForcePushWithLeaseRejected = 'ForcePushWithLeaseRejected', + ForcePushWithLeaseIfIncludesRejected = 'ForcePushWithLeaseIfIncludesRejected', RemoteConnectionError = 'RemoteConnectionError', DirtyWorkTree = 'DirtyWorkTree', CantOpenResource = 'CantOpenResource', @@ -361,5 +458,10 @@ export const enum GitErrorCodes { EmptyCommitMessage = 'EmptyCommitMessage', BranchFastForwardRejected = 'BranchFastForwardRejected', BranchNotYetBorn = 'BranchNotYetBorn', - TagConflict = 'TagConflict' + TagConflict = 'TagConflict', + CherryPickEmpty = 'CherryPickEmpty', + CherryPickConflict = 'CherryPickConflict', + WorktreeContainsChanges = 'WorktreeContainsChanges', + WorktreeAlreadyExists = 'WorktreeAlreadyExists', + WorktreeBranchAlreadyUsed = 'WorktreeBranchAlreadyUsed' } diff --git a/src/api/api.d.ts b/src/api/api.d.ts index 2dd11908e6..c168e554d2 100644 --- a/src/api/api.d.ts +++ b/src/api/api.d.ts @@ -188,7 +188,7 @@ export interface Repository { setBranchUpstream(name: string, upstream: string): Promise; getRefs?(query: RefQuery, cancellationToken?: CancellationToken): Promise; // Optional, because Remote Hub doesn't support this - getMergeBase(ref1: string, ref2: string): Promise; + getMergeBase(ref1: string, ref2: string): Promise; status(): Promise; checkout(treeish: string): Promise; @@ -239,6 +239,7 @@ export interface IGit { readonly onDidPublish?: Event; registerPostCommitCommandsProvider?(provider: PostCommitCommandsProvider): Disposable; + getRepositoryWorkspace?(uri: Uri): Promise; } export interface TitleAndDescriptionProvider { diff --git a/src/api/api1.ts b/src/api/api1.ts index 1527e0622d..777b226def 100644 --- a/src/api/api1.ts +++ b/src/api/api1.ts @@ -84,6 +84,16 @@ export class GitApiImpl extends Disposable implements API, IGit { super(); } + async getRepositoryWorkspace(uri: vscode.Uri): Promise { + for (const [, provider] of this._providers) { + if (provider.getRepositoryWorkspace) { + return provider.getRepositoryWorkspace(uri); + } + } + return null; + } + + public get repositories(): Repository[] { const ret: Repository[] = []; diff --git a/src/common/uri.ts b/src/common/uri.ts index 9aecf5f3e0..ea92664ad5 100644 --- a/src/common/uri.ts +++ b/src/common/uri.ts @@ -636,6 +636,7 @@ function validateOpenWebviewParams(owner?: string, repo?: string, number?: strin export enum UriHandlerPaths { OpenIssueWebview = '/open-issue-webview', OpenPullRequestWebview = '/open-pull-request-webview', + CheckoutPullRequest = '/checkout-pull-request' } export interface OpenIssueWebviewUriParams { @@ -676,11 +677,11 @@ export async function toOpenPullRequestWebviewUri(params: OpenPullRequestWebview return vscode.env.asExternalUri(vscode.Uri.from({ scheme: vscode.env.uriScheme, authority: EXTENSION_ID, path: UriHandlerPaths.OpenPullRequestWebview, query })); } -export function fromOpenPullRequestWebviewUri(uri: vscode.Uri): OpenPullRequestWebviewUriParams | undefined { +export function fromOpenOrCheckoutPullRequestWebviewUri(uri: vscode.Uri): OpenPullRequestWebviewUriParams | undefined { if (compareIgnoreCase(uri.authority, EXTENSION_ID) !== 0) { return; } - if (uri.path !== UriHandlerPaths.OpenPullRequestWebview) { + if (uri.path !== UriHandlerPaths.OpenPullRequestWebview && uri.path !== UriHandlerPaths.CheckoutPullRequest) { return; } try { diff --git a/src/extension.ts b/src/extension.ts index 31e8013536..fff41dbcf9 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -39,7 +39,7 @@ import { NotificationsFeatureRegister } from './notifications/notificationsFeatu import { NotificationsManager } from './notifications/notificationsManager'; import { NotificationsProvider } from './notifications/notificationsProvider'; import { ThemeWatcher } from './themeWatcher'; -import { UriHandler } from './uriHandler'; +import { resumePendingCheckout, UriHandler } from './uriHandler'; import { CommentDecorationProvider } from './view/commentDecorationProvider'; import { CompareChanges } from './view/compareChangesTreeDataProvider'; import { CreatePullRequestHelper } from './view/createPullRequestHelper'; @@ -267,8 +267,11 @@ async function init( registerPostCommitCommandsProvider(reposManager, git); + // Resume any pending checkout request stored before workspace reopened. + await resumePendingCheckout(context, reposManager); + initChat(context, credentialStore, reposManager, copilotRemoteAgentManager, telemetry, prsTreeModel); - context.subscriptions.push(vscode.window.registerUriHandler(new UriHandler(reposManager, telemetry, context))); + context.subscriptions.push(vscode.window.registerUriHandler(new UriHandler(reposManager, telemetry, context, git))); // Make sure any compare changes tabs, which come from the create flow, are closed. CompareChanges.closeTabs(); diff --git a/src/gitProviders/builtinGit.ts b/src/gitProviders/builtinGit.ts index 4c569a13ae..6eb0c6f895 100644 --- a/src/gitProviders/builtinGit.ts +++ b/src/gitProviders/builtinGit.ts @@ -46,6 +46,9 @@ export class BuiltinGitProvider extends Disposable implements IGit { this._register(this._gitAPI.onDidChangeState(e => this._onDidChangeState.fire(e))); this._register(this._gitAPI.onDidPublish(e => this._onDidPublish.fire(e))); } + getRepositoryWorkspace(uri: vscode.Uri): Promise { + return this._gitAPI.getRepositoryWorkspace(uri); + } static async createProvider(): Promise { const extension = vscode.extensions.getExtension('vscode.git'); diff --git a/src/gitProviders/vslsguest.ts b/src/gitProviders/vslsguest.ts index 5385745f91..fd823781af 100644 --- a/src/gitProviders/vslsguest.ts +++ b/src/gitProviders/vslsguest.ts @@ -6,7 +6,7 @@ import * as vscode from 'vscode'; import { LiveShare, SharedServiceProxy } from 'vsls/vscode.js'; -import { Branch, Change, Commit, Remote, RepositoryState, Submodule } from '../@types/git'; +import { Branch, Change, Commit, Ref, Remote, RepositoryState, Submodule } from '../@types/git'; import { IGit, Repository } from '../api/api'; import { Disposable } from '../common/lifecycle'; import { @@ -149,6 +149,8 @@ class LiveShareRepositoryState implements RepositoryState { this.HEAD = state.HEAD; this.remotes = state.remotes; } + refs: Ref[] = []; + untrackedChanges: Change[] = []; public update(state: RepositoryState) { this.HEAD = state.HEAD; diff --git a/src/uriHandler.ts b/src/uriHandler.ts index b1b4a8839b..20ae6e4512 100644 --- a/src/uriHandler.ts +++ b/src/uriHandler.ts @@ -4,16 +4,73 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { GitApiImpl } from './api/api1'; +import Logger from './common/logger'; import { ITelemetry } from './common/telemetry'; -import { fromOpenIssueWebviewUri, fromOpenPullRequestWebviewUri, UriHandlerPaths } from './common/uri'; +import { fromOpenIssueWebviewUri, fromOpenOrCheckoutPullRequestWebviewUri, UriHandlerPaths } from './common/uri'; +import { FolderRepositoryManager } from './github/folderRepositoryManager'; import { IssueOverviewPanel } from './github/issueOverview'; import { PullRequestOverviewPanel } from './github/pullRequestOverview'; import { RepositoriesManager } from './github/repositoriesManager'; +export const PENDING_CHECKOUT_PULL_REQUEST_KEY = 'pendingCheckoutPullRequest'; + +interface PendingCheckoutPayload { + owner: string; + repo: string; + pullRequestNumber: number; + timestamp: number; // epoch millis when the pending checkout was stored +} + +async function performPullRequestCheckout(folderManager: FolderRepositoryManager, owner: string, repo: string, prNumber: number): Promise { + try { + const pullRequest = await folderManager.resolvePullRequest(owner, repo, prNumber); + if (!pullRequest) { + Logger.warn(`Pull request #${prNumber} not found for checkout.`, UriHandler.ID); + return; + } + await vscode.commands.executeCommand('pr.pick', pullRequest); + } catch (e) { + Logger.error(`Error during pull request checkout: ${e instanceof Error ? e.message : String(e)}`, UriHandler.ID); + } +} + +export async function resumePendingCheckout(context: vscode.ExtensionContext, reposManager: RepositoriesManager): Promise { + const pending = context.globalState.get(PENDING_CHECKOUT_PULL_REQUEST_KEY); + if (!pending) { + return; + } + // Validate freshness (5 minutes) + const maxAgeMs = 5 * 60 * 1000; + if (!pending.timestamp || Date.now() - pending.timestamp > maxAgeMs) { + await context.globalState.update(PENDING_CHECKOUT_PULL_REQUEST_KEY, undefined); + Logger.debug('Stale pending checkout entry cleared (older than 5 minutes).', UriHandler.ID); + return; + } + const attempt = async () => { + const fm = reposManager.getManagerForRepository(pending.owner, pending.repo); + if (!fm) { + return false; + } + await performPullRequestCheckout(fm, pending.owner, pending.repo, pending.pullRequestNumber); + await context.globalState.update(PENDING_CHECKOUT_PULL_REQUEST_KEY, undefined); + return true; + }; + if (!(await attempt())) { + const disposable = reposManager.onDidLoadAnyRepositories(async () => { + if (await attempt()) { + disposable.dispose(); + } + }); + } +} + export class UriHandler implements vscode.UriHandler { + public static readonly ID = 'UriHandler'; constructor(private readonly _reposManagers: RepositoriesManager, private readonly _telemetry: ITelemetry, - private readonly _context: vscode.ExtensionContext + private readonly _context: vscode.ExtensionContext, + private readonly _git: GitApiImpl ) { } async handleUri(uri: vscode.Uri): Promise { @@ -22,6 +79,9 @@ export class UriHandler implements vscode.UriHandler { return this._openIssueWebview(uri); case UriHandlerPaths.OpenPullRequestWebview: return this._openPullRequestWebview(uri); + case UriHandlerPaths.CheckoutPullRequest: + // example vscode-insiders://github.vscode-pull-request-github/checkout-pull-request?%7B%22owner%22%3A%22alexr00%22%2C%22repo%22%3A%22playground%22%2C%22pullRequestNumber%22%3A714%7D + return this._checkoutPullRequest(uri); } } @@ -39,7 +99,7 @@ export class UriHandler implements vscode.UriHandler { } private async _openPullRequestWebview(uri: vscode.Uri): Promise { - const params = fromOpenPullRequestWebviewUri(uri); + const params = fromOpenOrCheckoutPullRequestWebviewUri(uri); if (!params) { return; } @@ -51,4 +111,29 @@ export class UriHandler implements vscode.UriHandler { return PullRequestOverviewPanel.createOrShow(this._telemetry, this._context.extensionUri, folderManager, pullRequest); } -} \ No newline at end of file + private async _checkoutPullRequest(uri: vscode.Uri): Promise { + const params = fromOpenOrCheckoutPullRequestWebviewUri(uri); + if (!params) { + return; + } + const folderManager = this._reposManagers.getManagerForRepository(params.owner, params.repo); + if (folderManager) { + return performPullRequestCheckout(folderManager, params.owner, params.repo, params.pullRequestNumber); + } + // Folder not found; request workspace open then resume later. + try { + const remoteUri = vscode.Uri.parse(`https://github.com/${params.owner}/${params.repo}`); + const workspaces = await this._git.getRepositoryWorkspace(remoteUri); + if (workspaces && workspaces.length) { + const payload: PendingCheckoutPayload = { ...params, timestamp: Date.now() }; + await this._context.globalState.update(PENDING_CHECKOUT_PULL_REQUEST_KEY, payload); + await vscode.commands.executeCommand('vscode.openFolder', workspaces[0]); + } else { + Logger.warn(`No repository workspace found for ${remoteUri.toString()}`, UriHandler.ID); + } + } catch (e) { + Logger.error(`Failed attempting workspace open for checkout PR: ${e instanceof Error ? e.message : String(e)}`, UriHandler.ID); + } + } + +} From 53299ec92c2491e18b973799798512f8896f2a5a Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Fri, 31 Oct 2025 17:40:03 +0100 Subject: [PATCH 031/402] Don't call remove reviewer API when there's nothing to remove (#8071) --- src/github/pullRequestModel.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/github/pullRequestModel.ts b/src/github/pullRequestModel.ts index 459e9a3f31..abc657bd93 100644 --- a/src/github/pullRequestModel.ts +++ b/src/github/pullRequestModel.ts @@ -1264,6 +1264,9 @@ export class PullRequestModel extends IssueModel implements IPullRe * @param reviewer A GitHub Login */ async deleteReviewRequest(reviewers: IAccount[], teamReviewers: ITeam[]): Promise { + if (reviewers.length === 0 && teamReviewers.length === 0) { + return; + } const { octokit, remote } = await this.githubRepository.ensure(); await octokit.call(octokit.api.pulls.removeRequestedReviewers, { owner: remote.owner, From 82915e7de46d381bfc11c961faba7492f1e4bf28 Mon Sep 17 00:00:00 2001 From: Osvaldo Ortega <48293249+osortega@users.noreply.github.com> Date: Fri, 31 Oct 2025 11:18:44 -0700 Subject: [PATCH 032/402] Fix for when clauses chatSessionType (#8048) --- package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/package.json b/package.json index ea564de1b5..4ba9fae8f8 100644 --- a/package.json +++ b/package.json @@ -3531,22 +3531,22 @@ "chat/chatSessions": [ { "command": "pr.openChanges", - "when": "resourceScheme == copilot-swe-agent || resourceScheme == copilot-cloud-agent", + "when": "chatSessionType == copilot-swe-agent || chatSessionType == copilot-cloud-agent", "group": "inline" }, { "command": "pr.checkoutChatSessionPullRequest", - "when": "resourceScheme == copilot-swe-agent || resourceScheme == copilot-cloud-agent", + "when": "chatSessionType == copilot-swe-agent || chatSessionType == copilot-cloud-agent", "group": "context" }, { "command": "pr.closeChatSessionPullRequest", - "when": "resourceScheme == copilot-swe-agent", + "when": "chatSessionType == copilot-swe-agent", "group": "context" }, { "command": "pr.cancelCodingAgent", - "when": "resourceScheme == copilot-swe-agent", + "when": "chatSessionType == copilot-swe-agent", "group": "context" } ], From 4d39482014ef99e91d0350d90e62a6d2041b53ee Mon Sep 17 00:00:00 2001 From: Josh Spicer <23246594+joshspicer@users.noreply.github.com> Date: Mon, 3 Nov 2025 15:20:53 -0800 Subject: [PATCH 033/402] do not contribute open changes (#8080) --- package.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/package.json b/package.json index 4ba9fae8f8..9bc55df813 100644 --- a/package.json +++ b/package.json @@ -3556,9 +3556,6 @@ }, { "command": "pr.applyChangesFromDescription" - }, - { - "command": "pr.openChanges" } ] }, From f20121678a5353c803109f0563367eb28d4be996 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Wed, 5 Nov 2025 11:58:55 +0100 Subject: [PATCH 034/402] Command cleanup (#8084) --- package.json | 13 ++++++++----- package.nls.json | 1 - 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/package.json b/package.json index 9bc55df813..b4b768559d 100644 --- a/package.json +++ b/package.json @@ -1794,11 +1794,6 @@ "category": "%command.notifications.category%", "icon": "$(gear)" }, - { - "command": "codingAgent.openSessionLog", - "title": "%command.codingAgent.openSessionLog.title%", - "category": "%command.pull.request.category%" - }, { "command": "pr.refreshChatSessions", "title": "%command.pr.refreshChatSessions.title%", @@ -2623,6 +2618,14 @@ { "command": "pr.preferredCodingAgentGitHubRemote", "when": "false" + }, + { + "command": "pr.closeChatSessionPullRequest", + "when": "false" + }, + { + "command": "pr.cancelCodingAgent", + "when": "false" } ], "view/title": [ diff --git a/package.nls.json b/package.nls.json index fab85ddf89..17aeb3a2b5 100644 --- a/package.nls.json +++ b/package.nls.json @@ -332,7 +332,6 @@ "command.notifications.markPullRequestsAsDone.title": "Mark Pull Requests as Done", "command.notifications.configureNotificationsViewlet.title": "Configure...", "command.notification.chatSummarizeNotification.title": "Summarize With Copilot", - "command.codingAgent.openSessionLog.title": "Open Coding Agent Session Log", "command.pr.checkoutChatSessionPullRequest.title": "Checkout Pull Request", "command.pr.closeChatSessionPullRequest.title": "Close Pull Request", "command.pr.preferredCodingAgentGitHubRemote.title": "Set Preferred GitHub Remote", From 39c6f5e08a9a81c29d14fb1f8e2a07bee7efb4b2 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Wed, 5 Nov 2025 11:59:30 +0100 Subject: [PATCH 035/402] Better handling for cases where the pr can't be checked out (#8092) * Better handling for cases where the pr can't be checked out Fixes https://github.com/microsoft/vscode-pull-request-github/issues/8086 Fixes https://github.com/microsoft/vscode/issues/275358 Fixes https://github.com/microsoft/vscode/issues/275156 Fixes https://github.com/microsoft/vscode/issues/275355 * Reduce duplication --- src/api/api.d.ts | 1 + src/api/api1.ts | 11 ++++- src/gitProviders/builtinGit.ts | 6 ++- src/uriHandler.ts | 90 ++++++++++++++++++++++++++++------ 4 files changed, 92 insertions(+), 16 deletions(-) diff --git a/src/api/api.d.ts b/src/api/api.d.ts index c168e554d2..011cbd9cfa 100644 --- a/src/api/api.d.ts +++ b/src/api/api.d.ts @@ -240,6 +240,7 @@ export interface IGit { registerPostCommitCommandsProvider?(provider: PostCommitCommandsProvider): Disposable; getRepositoryWorkspace?(uri: Uri): Promise; + clone?(uri: Uri, options?: CloneOptions): Promise; } export interface TitleAndDescriptionProvider { diff --git a/src/api/api1.ts b/src/api/api1.ts index 777b226def..4eabe95f06 100644 --- a/src/api/api1.ts +++ b/src/api/api1.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import { API, IGit, PostCommitCommandsProvider, Repository, ReviewerCommentsProvider, TitleAndDescriptionProvider } from './api'; -import { APIState, PublishEvent } from '../@types/git'; +import { APIState, CloneOptions, PublishEvent } from '../@types/git'; import { Disposable } from '../common/lifecycle'; import Logger from '../common/logger'; import { TernarySearchTree } from '../common/utils'; @@ -93,6 +93,15 @@ export class GitApiImpl extends Disposable implements API, IGit { return null; } + async clone(uri: vscode.Uri, options?: CloneOptions): Promise { + for (const [, provider] of this._providers) { + if (provider.clone) { + return provider.clone(uri, options); + } + } + return null; + } + public get repositories(): Repository[] { const ret: Repository[] = []; diff --git a/src/gitProviders/builtinGit.ts b/src/gitProviders/builtinGit.ts index 6eb0c6f895..bb6df80374 100644 --- a/src/gitProviders/builtinGit.ts +++ b/src/gitProviders/builtinGit.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { APIState, GitAPI, GitExtension, PublishEvent } from '../@types/git'; +import { APIState, CloneOptions, GitAPI, GitExtension, PublishEvent } from '../@types/git'; import { IGit, Repository } from '../api/api'; import { commands } from '../common/executeCommands'; import { Disposable } from '../common/lifecycle'; @@ -50,6 +50,10 @@ export class BuiltinGitProvider extends Disposable implements IGit { return this._gitAPI.getRepositoryWorkspace(uri); } + clone(uri: vscode.Uri, options?: CloneOptions): Promise { + return this._gitAPI.clone(uri, options); + } + static async createProvider(): Promise { const extension = vscode.extensions.getExtension('vscode.git'); if (extension) { diff --git a/src/uriHandler.ts b/src/uriHandler.ts index 20ae6e4512..3fe4bfc746 100644 --- a/src/uriHandler.ts +++ b/src/uriHandler.ts @@ -22,13 +22,33 @@ interface PendingCheckoutPayload { timestamp: number; // epoch millis when the pending checkout was stored } +function withCheckoutProgress(owner: string, repo: string, prNumber: number, task: (progress: vscode.Progress<{ message?: string; increment?: number }>, token: vscode.CancellationToken) => Promise): Promise { + return vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: vscode.l10n.t('Checking out pull request #{0} from {1}/{2}...', prNumber, owner, repo), + cancellable: true + }, async (progress, token) => { + if (token.isCancellationRequested) { + return Promise.resolve(undefined as unknown as T); + } + return task(progress, token); + }) as Promise; +} + async function performPullRequestCheckout(folderManager: FolderRepositoryManager, owner: string, repo: string, prNumber: number): Promise { try { const pullRequest = await folderManager.resolvePullRequest(owner, repo, prNumber); if (!pullRequest) { + vscode.window.showErrorMessage(vscode.l10n.t('Pull request #{0} not found in {1}/{2}.', prNumber, owner, repo)); Logger.warn(`Pull request #${prNumber} not found for checkout.`, UriHandler.ID); return; } + + const proceed = await showCheckoutPrompt(owner, repo, prNumber); + if (!proceed) { + return; + } + await vscode.commands.executeCommand('pr.pick', pullRequest); } catch (e) { Logger.error(`Error during pull request checkout: ${e instanceof Error ? e.message : String(e)}`, UriHandler.ID); @@ -48,11 +68,11 @@ export async function resumePendingCheckout(context: vscode.ExtensionContext, re return; } const attempt = async () => { - const fm = reposManager.getManagerForRepository(pending.owner, pending.repo); - if (!fm) { + const folderManager = reposManager.getManagerForRepository(pending.owner, pending.repo); + if (!folderManager) { return false; } - await performPullRequestCheckout(fm, pending.owner, pending.repo, pending.pullRequestNumber); + await performPullRequestCheckout(folderManager, pending.owner, pending.repo, pending.pullRequestNumber); await context.globalState.update(PENDING_CHECKOUT_PULL_REQUEST_KEY, undefined); return true; }; @@ -65,6 +85,13 @@ export async function resumePendingCheckout(context: vscode.ExtensionContext, re } } +export async function showCheckoutPrompt(owner: string, repo: string, prNumber: number): Promise { + const message = vscode.l10n.t('Checkout pull request #{0} from {1}/{2}?', prNumber, owner, repo); + const confirm = vscode.l10n.t('Checkout'); + const selection = await vscode.window.showInformationMessage(message, { modal: true }, confirm); + return selection === confirm; +} + export class UriHandler implements vscode.UriHandler { public static readonly ID = 'UriHandler'; constructor(private readonly _reposManagers: RepositoriesManager, @@ -111,6 +138,12 @@ export class UriHandler implements vscode.UriHandler { return PullRequestOverviewPanel.createOrShow(this._telemetry, this._context.extensionUri, folderManager, pullRequest); } + private async _savePendingCheckoutAndOpenFolder(params: { owner: string; repo: string; pullRequestNumber: number }, folderUri: vscode.Uri): Promise { + const payload: PendingCheckoutPayload = { ...params, timestamp: Date.now() }; + await this._context.globalState.update(PENDING_CHECKOUT_PULL_REQUEST_KEY, payload); + await vscode.commands.executeCommand('vscode.openFolder', folderUri); + } + private async _checkoutPullRequest(uri: vscode.Uri): Promise { const params = fromOpenOrCheckoutPullRequestWebviewUri(uri); if (!params) { @@ -121,18 +154,47 @@ export class UriHandler implements vscode.UriHandler { return performPullRequestCheckout(folderManager, params.owner, params.repo, params.pullRequestNumber); } // Folder not found; request workspace open then resume later. - try { - const remoteUri = vscode.Uri.parse(`https://github.com/${params.owner}/${params.repo}`); - const workspaces = await this._git.getRepositoryWorkspace(remoteUri); - if (workspaces && workspaces.length) { - const payload: PendingCheckoutPayload = { ...params, timestamp: Date.now() }; - await this._context.globalState.update(PENDING_CHECKOUT_PULL_REQUEST_KEY, payload); - await vscode.commands.executeCommand('vscode.openFolder', workspaces[0]); - } else { - Logger.warn(`No repository workspace found for ${remoteUri.toString()}`, UriHandler.ID); + await withCheckoutProgress(params.owner, params.repo, params.pullRequestNumber, async (progress, token) => { + if (token.isCancellationRequested) { + return; + } + try { + progress.report({ message: vscode.l10n.t('Locating workspace...') }); + const remoteUri = vscode.Uri.parse(`https://github.com/${params.owner}/${params.repo}`); + const workspaces = await this._git.getRepositoryWorkspace(remoteUri); + if (token.isCancellationRequested) { + return; + } + if (workspaces && workspaces.length) { + progress.report({ message: vscode.l10n.t('Opening workspace...') }); + await this._savePendingCheckoutAndOpenFolder(params, workspaces[0]); + } else { + this._showCloneOffer(remoteUri, params); + } + } catch (e) { + Logger.error(`Failed attempting workspace open for checkout PR: ${e instanceof Error ? e.message : String(e)}`, UriHandler.ID); + } + }); + } + + private async _showCloneOffer(remoteUri: vscode.Uri, params: { owner: string; repo: string; pullRequestNumber: number }): Promise { + const cloneLabel = vscode.l10n.t('Clone Repository'); + const choice = await vscode.window.showErrorMessage( + vscode.l10n.t('Could not find a folder for repository {0}/{1}. Please clone or open the repository manually.', params.owner, params.repo), + cloneLabel + ); + Logger.warn(`No repository workspace found for ${remoteUri.toString()}`, UriHandler.ID); + if (choice === cloneLabel) { + try { + const clonedWorkspaceUri = await this._git.clone(remoteUri, { postCloneAction: 'none' }); + if (clonedWorkspaceUri) { + await this._savePendingCheckoutAndOpenFolder(params, clonedWorkspaceUri); + } else { + Logger.warn(`Clone API returned null for ${remoteUri.toString()}`, UriHandler.ID); + } + } catch (err) { + Logger.error(`Failed to clone repository via API: ${err instanceof Error ? err.message : String(err)}`, UriHandler.ID); } - } catch (e) { - Logger.error(`Failed attempting workspace open for checkout PR: ${e instanceof Error ? e.message : String(e)}`, UriHandler.ID); } } From 389363e1e1b5d1ee6d0cdd98f8440011bb1b0832 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Wed, 5 Nov 2025 13:01:06 +0100 Subject: [PATCH 036/402] Pull Request explorer pane is not shown when it is hidden and opening PR from a URL (#8095) * Pull Request explorer pane is not shown when it is hidden and opening PR from a URL Fixes #8089 * Fix new window behavior --- src/commands.ts | 117 ++-------------------------------- src/common/executeCommands.ts | 4 ++ src/extension.ts | 4 +- src/uriHandler.ts | 29 ++++++--- src/view/reviewManager.ts | 16 +++-- src/view/reviewsManager.ts | 115 ++++++++++++++++++++++++++++++++- 6 files changed, 153 insertions(+), 132 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index 6d6c46583f..4844af4cfa 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -7,11 +7,10 @@ import * as pathLib from 'path'; import * as vscode from 'vscode'; import { Repository } from './api/api'; -import { GitErrorCodes, Status } from './api/api1'; +import { GitErrorCodes } from './api/api1'; import { CommentReply, findActiveHandler, resolveCommentHandler } from './commentHandlerResolver'; import { commands } from './common/executeCommands'; import Logger from './common/logger'; -import * as PersistentState from './common/persistentState'; import { FILE_LIST_LAYOUT, HIDE_VIEWED_FILES, PR_SETTINGS_NAMESPACE } from './common/settingKeys'; import { editQuery } from './common/settingsUtils'; import { ITelemetry } from './common/telemetry'; @@ -51,81 +50,6 @@ import { import { PRNode } from './view/treeNodes/pullRequestNode'; import { RepositoryChangesNode } from './view/treeNodes/repositoryChangesNode'; -// Modal dialog options for handling uncommitted changes during PR checkout -const STASH_CHANGES = vscode.l10n.t('Stash changes'); -const DISCARD_CHANGES = vscode.l10n.t('Discard changes'); -const DONT_SHOW_AGAIN = vscode.l10n.t('Try to checkout anyway and don\'t show again'); - -// Constants for persistent state storage -const UNCOMMITTED_CHANGES_SCOPE = vscode.l10n.t('uncommitted changes warning'); -const UNCOMMITTED_CHANGES_STORAGE_KEY = 'showWarning'; - -/** - * Shows a modal dialog when there are uncommitted changes during PR checkout - * @param repository The git repository with uncommitted changes - * @returns Promise true if user chose to proceed (after staging/discarding), false if cancelled - */ -async function handleUncommittedChanges(repository: Repository): Promise { - // Check if user has disabled the warning using persistent state - if (PersistentState.fetch(UNCOMMITTED_CHANGES_SCOPE, UNCOMMITTED_CHANGES_STORAGE_KEY) === false) { - return true; // User has disabled warnings, proceed without showing dialog - } - - // Filter out untracked files as they typically don't conflict with PR checkout - const trackedWorkingTreeChanges = repository.state.workingTreeChanges.filter(change => change.status !== Status.UNTRACKED); - const hasTrackedWorkingTreeChanges = trackedWorkingTreeChanges.length > 0; - const hasIndexChanges = repository.state.indexChanges.length > 0; - - if (!hasTrackedWorkingTreeChanges && !hasIndexChanges) { - return true; // No tracked uncommitted changes, proceed - } - - const modalResult = await vscode.window.showInformationMessage( - vscode.l10n.t('You have uncommitted changes that might be overwritten by checking out this pull request.'), - { - modal: true, - detail: vscode.l10n.t('Choose how to handle your uncommitted changes before checking out the pull request.'), - }, - STASH_CHANGES, - DISCARD_CHANGES, - DONT_SHOW_AGAIN, - ); - - if (!modalResult) { - return false; // User cancelled - } - - if (modalResult === DONT_SHOW_AGAIN) { - // Store preference to never show this dialog again using persistent state - PersistentState.store(UNCOMMITTED_CHANGES_SCOPE, UNCOMMITTED_CHANGES_STORAGE_KEY, false); - return true; // Proceed with checkout - } - - try { - if (modalResult === STASH_CHANGES) { - // Stash all changes (working tree changes + any unstaged changes) - const allChangedFiles = [ - ...trackedWorkingTreeChanges.map(change => change.uri.fsPath), - ...repository.state.indexChanges.map(change => change.uri.fsPath), - ]; - if (allChangedFiles.length > 0) { - await repository.add(allChangedFiles); - await vscode.commands.executeCommand('git.stash', repository); - } - } else if (modalResult === DISCARD_CHANGES) { - // Discard all tracked working tree changes - const trackedWorkingTreeFiles = trackedWorkingTreeChanges.map(change => change.uri.fsPath); - if (trackedWorkingTreeFiles.length > 0) { - await repository.clean(trackedWorkingTreeFiles); - } - } - return true; // Successfully handled changes, proceed with checkout - } catch (error) { - vscode.window.showErrorMessage(vscode.l10n.t('Failed to handle uncommitted changes: {0}', formatError(error))); - return false; - } -} - function ensurePR(folderRepoManager: FolderRepositoryManager, pr?: PRNode): PullRequestModel; function ensurePR>(folderRepoManager: FolderRepositoryManager, pr?: TIssueModel): TIssueModel; function ensurePR>(folderRepoManager: FolderRepositoryManager, pr?: PRNode | TIssueModel): TIssueModel { @@ -493,37 +417,6 @@ export function registerCommands( ), ); - const switchToPr = async (folderManager: FolderRepositoryManager, pullRequestModel: PullRequestModel, repository: Repository | undefined, isFromDescription: boolean) => { - // If we don't have a repository from the node, use the one from the folder manager - const repositoryToCheck = repository || folderManager.repository; - - // Check for uncommitted changes before proceeding with checkout - const shouldProceed = await handleUncommittedChanges(repositoryToCheck); - if (!shouldProceed) { - return; // User cancelled or there was an error handling changes - } - - /* __GDPR__ - "pr.checkout" : { - "fromDescriptionPage" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } - } - */ - telemetry.sendTelemetryEvent('pr.checkout', { fromDescription: isFromDescription.toString() }); - - return vscode.window.withProgress( - { - location: vscode.ProgressLocation.SourceControl, - title: vscode.l10n.t('Switching to Pull Request #{0}', pullRequestModel.number), - }, - async () => { - await ReviewManager.getReviewManagerForRepository( - reviewsManager.reviewManagers, - pullRequestModel.githubRepository, - repository - )?.switch(pullRequestModel); - }); - }; - context.subscriptions.push( vscode.commands.registerCommand('pr.pick', async (pr: PRNode | RepositoryChangesNode | PullRequestModel) => { if (pr === undefined) { @@ -549,7 +442,7 @@ export function registerCommands( } const fromDescriptionPage = pr instanceof PullRequestModel; - return switchToPr(folderManager, pullRequestModel, repository, fromDescriptionPage); + return reviewsManager.switchToPr(folderManager, pullRequestModel, repository, fromDescriptionPage); })); @@ -653,14 +546,14 @@ export function registerCommands( return vscode.window.showErrorMessage(vscode.l10n.t('Unable to find pull request #{0}', prNumber.toString())); } - return switchToPr(folderManager, pullRequest, folderManager.repository, true); + return reviewsManager.switchToPr(folderManager, pullRequest, folderManager.repository, true); } const resolved = await resolvePr(ctx); if (!resolved) { return vscode.window.showErrorMessage(vscode.l10n.t('Unable to resolve pull request for checkout.')); } - return switchToPr(resolved.folderManager, resolved.pr, resolved.folderManager.repository, true); + return reviewsManager.switchToPr(resolved.folderManager, resolved.pr, resolved.folderManager.repository, true); })); @@ -1035,7 +928,7 @@ export function registerCommands( return vscode.window.showErrorMessage(vscode.l10n.t('Unable to find repository for pull request #{0}', pr.number.toString())); } - return switchToPr(folderManager, pr, folderManager.repository, false); + return reviewsManager.switchToPr(folderManager, pr, folderManager.repository, false); } async function closeChatSessionPullRequest(argument: ChatSessionWithPR | CrossChatSessionWithPR) { diff --git a/src/common/executeCommands.ts b/src/common/executeCommands.ts index bf550fd2ce..dee2f69f39 100644 --- a/src/common/executeCommands.ts +++ b/src/common/executeCommands.ts @@ -40,4 +40,8 @@ export namespace commands { export function setContext(context: string, value: any) { return executeCommand('setContext', context, value); } + + export function openFolder(ur: vscode.Uri, options: { forceNewWindow?: boolean, forceReuseWindow?: boolean }) { + return executeCommand('vscode.openFolder', ur, options); + } } \ No newline at end of file diff --git a/src/extension.ts b/src/extension.ts index fff41dbcf9..651497c04a 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -268,10 +268,10 @@ async function init( registerPostCommitCommandsProvider(reposManager, git); // Resume any pending checkout request stored before workspace reopened. - await resumePendingCheckout(context, reposManager); + await resumePendingCheckout(reviewsManager, context, reposManager); initChat(context, credentialStore, reposManager, copilotRemoteAgentManager, telemetry, prsTreeModel); - context.subscriptions.push(vscode.window.registerUriHandler(new UriHandler(reposManager, telemetry, context, git))); + context.subscriptions.push(vscode.window.registerUriHandler(new UriHandler(reposManager, reviewsManager, telemetry, context, git))); // Make sure any compare changes tabs, which come from the create flow, are closed. CompareChanges.closeTabs(); diff --git a/src/uriHandler.ts b/src/uriHandler.ts index 3fe4bfc746..a9c6ac4abc 100644 --- a/src/uriHandler.ts +++ b/src/uriHandler.ts @@ -5,13 +5,16 @@ import * as vscode from 'vscode'; import { GitApiImpl } from './api/api1'; +import { commands } from './common/executeCommands'; import Logger from './common/logger'; import { ITelemetry } from './common/telemetry'; import { fromOpenIssueWebviewUri, fromOpenOrCheckoutPullRequestWebviewUri, UriHandlerPaths } from './common/uri'; import { FolderRepositoryManager } from './github/folderRepositoryManager'; import { IssueOverviewPanel } from './github/issueOverview'; +import { PullRequestModel } from './github/pullRequestModel'; import { PullRequestOverviewPanel } from './github/pullRequestOverview'; import { RepositoriesManager } from './github/repositoriesManager'; +import { ReviewsManager } from './view/reviewsManager'; export const PENDING_CHECKOUT_PULL_REQUEST_KEY = 'pendingCheckoutPullRequest'; @@ -25,7 +28,7 @@ interface PendingCheckoutPayload { function withCheckoutProgress(owner: string, repo: string, prNumber: number, task: (progress: vscode.Progress<{ message?: string; increment?: number }>, token: vscode.CancellationToken) => Promise): Promise { return vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, - title: vscode.l10n.t('Checking out pull request #{0} from {1}/{2}...', prNumber, owner, repo), + title: vscode.l10n.t('Checking out pull request #{0} from {1}/{2}', prNumber, owner, repo), cancellable: true }, async (progress, token) => { if (token.isCancellationRequested) { @@ -35,9 +38,13 @@ function withCheckoutProgress(owner: string, repo: string, prNumber: number, }) as Promise; } -async function performPullRequestCheckout(folderManager: FolderRepositoryManager, owner: string, repo: string, prNumber: number): Promise { +async function performPullRequestCheckout(reviewsManager: ReviewsManager, folderManager: FolderRepositoryManager, owner: string, repo: string, prNumber: number): Promise { try { - const pullRequest = await folderManager.resolvePullRequest(owner, repo, prNumber); + let pullRequest: PullRequestModel | undefined; + await withCheckoutProgress(owner, repo, prNumber, async (progress, _token) => { + progress.report({ message: vscode.l10n.t('Resolving pull request') }); + pullRequest = await folderManager.resolvePullRequest(owner, repo, prNumber); + }); if (!pullRequest) { vscode.window.showErrorMessage(vscode.l10n.t('Pull request #{0} not found in {1}/{2}.', prNumber, owner, repo)); Logger.warn(`Pull request #${prNumber} not found for checkout.`, UriHandler.ID); @@ -49,13 +56,13 @@ async function performPullRequestCheckout(folderManager: FolderRepositoryManager return; } - await vscode.commands.executeCommand('pr.pick', pullRequest); + await reviewsManager.switchToPr(folderManager, pullRequest, undefined, false); } catch (e) { Logger.error(`Error during pull request checkout: ${e instanceof Error ? e.message : String(e)}`, UriHandler.ID); } } -export async function resumePendingCheckout(context: vscode.ExtensionContext, reposManager: RepositoriesManager): Promise { +export async function resumePendingCheckout(reviewsManager: ReviewsManager, context: vscode.ExtensionContext, reposManager: RepositoriesManager): Promise { const pending = context.globalState.get(PENDING_CHECKOUT_PULL_REQUEST_KEY); if (!pending) { return; @@ -72,7 +79,7 @@ export async function resumePendingCheckout(context: vscode.ExtensionContext, re if (!folderManager) { return false; } - await performPullRequestCheckout(folderManager, pending.owner, pending.repo, pending.pullRequestNumber); + await performPullRequestCheckout(reviewsManager, folderManager, pending.owner, pending.repo, pending.pullRequestNumber); await context.globalState.update(PENDING_CHECKOUT_PULL_REQUEST_KEY, undefined); return true; }; @@ -95,6 +102,7 @@ export async function showCheckoutPrompt(owner: string, repo: string, prNumber: export class UriHandler implements vscode.UriHandler { public static readonly ID = 'UriHandler'; constructor(private readonly _reposManagers: RepositoriesManager, + private readonly _reviewsManagers: ReviewsManager, private readonly _telemetry: ITelemetry, private readonly _context: vscode.ExtensionContext, private readonly _git: GitApiImpl @@ -141,7 +149,8 @@ export class UriHandler implements vscode.UriHandler { private async _savePendingCheckoutAndOpenFolder(params: { owner: string; repo: string; pullRequestNumber: number }, folderUri: vscode.Uri): Promise { const payload: PendingCheckoutPayload = { ...params, timestamp: Date.now() }; await this._context.globalState.update(PENDING_CHECKOUT_PULL_REQUEST_KEY, payload); - await vscode.commands.executeCommand('vscode.openFolder', folderUri); + const isEmpty = vscode.workspace.workspaceFolders === undefined || vscode.workspace.workspaceFolders.length === 0; + await commands.openFolder(folderUri, { forceNewWindow: !isEmpty, forceReuseWindow: isEmpty }); } private async _checkoutPullRequest(uri: vscode.Uri): Promise { @@ -151,7 +160,7 @@ export class UriHandler implements vscode.UriHandler { } const folderManager = this._reposManagers.getManagerForRepository(params.owner, params.repo); if (folderManager) { - return performPullRequestCheckout(folderManager, params.owner, params.repo, params.pullRequestNumber); + return performPullRequestCheckout(this._reviewsManagers, folderManager, params.owner, params.repo, params.pullRequestNumber); } // Folder not found; request workspace open then resume later. await withCheckoutProgress(params.owner, params.repo, params.pullRequestNumber, async (progress, token) => { @@ -159,14 +168,14 @@ export class UriHandler implements vscode.UriHandler { return; } try { - progress.report({ message: vscode.l10n.t('Locating workspace...') }); + progress.report({ message: vscode.l10n.t('Locating workspace') }); const remoteUri = vscode.Uri.parse(`https://github.com/${params.owner}/${params.repo}`); const workspaces = await this._git.getRepositoryWorkspace(remoteUri); if (token.isCancellationRequested) { return; } if (workspaces && workspaces.length) { - progress.report({ message: vscode.l10n.t('Opening workspace...') }); + progress.report({ message: vscode.l10n.t('Opening workspace') }); await this._savePendingCheckoutAndOpenFolder(params, workspaces[0]); } else { this._showCloneOffer(remoteUri, params); diff --git a/src/view/reviewManager.ts b/src/view/reviewManager.ts index eb785f1e2a..6260047828 100644 --- a/src/view/reviewManager.ts +++ b/src/view/reviewManager.ts @@ -381,13 +381,13 @@ export class ReviewManager extends Disposable { } } - private async resolvePullRequest(metadata: PullRequestMetadata): Promise<(PullRequestModel & IResolvedPullRequestModel) | undefined> { + private async resolvePullRequest(metadata: PullRequestMetadata, useCache: boolean): Promise<(PullRequestModel & IResolvedPullRequestModel) | undefined> { try { this._prNumber = metadata.prNumber; const { owner, repositoryName } = metadata; Logger.appendLine('Resolving pull request', this.id); - let pr = await this._folderRepoManager.resolvePullRequest(owner, repositoryName, metadata.prNumber); + let pr = await this._folderRepoManager.resolvePullRequest(owner, repositoryName, metadata.prNumber, useCache); if (!pr || !pr.isResolved() || !(await pr.githubRepository.hasBranch(pr.base.name))) { await this.clear(true); @@ -450,7 +450,9 @@ export class ReviewManager extends Disposable { `current branch ${this._repository.state.HEAD.name} is associated with pull request #${matchingPullRequestMetadata.prNumber}`, this.id ); const previousPrNumber = this._prNumber; - let pr = await this.resolvePullRequest(matchingPullRequestMetadata); + // Use the cache if we just checked out the same PR as a small performance optimization. + const justCheckedOutSamePr = this.justSwitchedToReviewMode && (previousPrNumber === matchingPullRequestMetadata.prNumber); + let pr = await this.resolvePullRequest(matchingPullRequestMetadata, justCheckedOutSamePr); if (!pr) { Logger.appendLine(`Unable to resolve PR #${matchingPullRequestMetadata.prNumber}`, this.id); return; @@ -461,7 +463,7 @@ export class ReviewManager extends Disposable { if (pr.state !== GithubItemStateEnum.Open) { const metadataFromGithub = await this.checkGitHubForPrBranch(branch); if (metadataFromGithub && metadataFromGithub?.prNumber !== pr.number) { - const prFromGitHub = await this.resolvePullRequest(metadataFromGithub); + const prFromGitHub = await this.resolvePullRequest(metadataFromGithub, false); if (prFromGitHub) { pr = prFromGitHub; } @@ -469,7 +471,7 @@ export class ReviewManager extends Disposable { } const hasPushedChanges = branch.commit !== oldLastCommitSha && branch.ahead === 0 && branch.behind === 0; - if (previousPrNumber === pr.number && !hasPushedChanges && (this._isShowingLastReviewChanges === pr.showChangesSinceReview)) { + if (!this.justSwitchedToReviewMode && (previousPrNumber === pr.number) && !hasPushedChanges && (this._isShowingLastReviewChanges === pr.showChangesSinceReview)) { return; } this._isShowingLastReviewChanges = pr.showChangesSinceReview; @@ -541,7 +543,9 @@ export class ReviewManager extends Disposable { }) ); Logger.appendLine(`Register in memory content provider`, this.id); - await this.registerGitHubInMemContentProvider(); + if (previousPrNumber !== pr.number) { + await this.registerGitHubInMemContentProvider(); + } this.statusBarItem.text = '$(git-pull-request) ' + vscode.l10n.t('Pull Request #{0}', pr.number); this.statusBarItem.command = { diff --git a/src/view/reviewsManager.ts b/src/view/reviewsManager.ts index 9ceb1a6e41..2995e0c8a8 100644 --- a/src/view/reviewsManager.ts +++ b/src/view/reviewsManager.ts @@ -10,13 +10,16 @@ import { PullRequestsTreeDataProvider } from './prsTreeDataProvider'; import { PrsTreeModel } from './prsTreeModel'; import { ReviewManager } from './reviewManager'; import { Repository } from '../api/api'; -import { GitApiImpl } from '../api/api1'; +import { GitApiImpl, Status } from '../api/api1'; import { Disposable } from '../common/lifecycle'; +import * as PersistentState from '../common/persistentState'; import { ITelemetry } from '../common/telemetry'; import { Schemes } from '../common/uri'; -import { isDescendant } from '../common/utils'; +import { formatError, isDescendant } from '../common/utils'; import { CopilotRemoteAgentManager } from '../github/copilotRemoteAgent'; import { CredentialStore } from '../github/credentials'; +import { FolderRepositoryManager } from '../github/folderRepositoryManager'; +import { PullRequestModel } from '../github/pullRequestModel'; import { RepositoriesManager } from '../github/repositoriesManager'; import { NotificationsManager } from '../notifications/notificationsManager'; @@ -104,4 +107,112 @@ export class ReviewsManager extends Disposable { manager.dispose(); } } + + async switchToPr(folderManager: FolderRepositoryManager, pullRequestModel: PullRequestModel, repository: Repository | undefined, isFromDescription: boolean) { + // If we don't have a repository from the node, use the one from the folder manager + const repositoryToCheck = repository || folderManager.repository; + + // Check for uncommitted changes before proceeding with checkout + const shouldProceed = await handleUncommittedChanges(repositoryToCheck); + if (!shouldProceed) { + return; // User cancelled or there was an error handling changes + } + + /* __GDPR__ + "pr.checkout" : { + "fromDescriptionPage" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this._telemetry.sendTelemetryEvent('pr.checkout', { fromDescription: isFromDescription.toString() }); + + return vscode.window.withProgress( + { + location: vscode.ProgressLocation.SourceControl, + title: vscode.l10n.t('Switching to Pull Request #{0}', pullRequestModel.number), + }, + async () => { + await ReviewManager.getReviewManagerForRepository( + this._reviewManagers, + pullRequestModel.githubRepository, + repository + )?.switch(pullRequestModel); + }); + }; +} + + +// Modal dialog options for handling uncommitted changes during PR checkout +const STASH_CHANGES = vscode.l10n.t('Stash changes'); +const DISCARD_CHANGES = vscode.l10n.t('Discard changes'); +const DONT_SHOW_AGAIN = vscode.l10n.t('Try to checkout anyway and don\'t show again'); + +// Constants for persistent state storage +const UNCOMMITTED_CHANGES_SCOPE = vscode.l10n.t('uncommitted changes warning'); +const UNCOMMITTED_CHANGES_STORAGE_KEY = 'showWarning'; + +/** + * Shows a modal dialog when there are uncommitted changes during PR checkout + * @param repository The git repository with uncommitted changes + * @returns Promise true if user chose to proceed (after staging/discarding), false if cancelled + */ +async function handleUncommittedChanges(repository: Repository): Promise { + // Check if user has disabled the warning using persistent state + if (PersistentState.fetch(UNCOMMITTED_CHANGES_SCOPE, UNCOMMITTED_CHANGES_STORAGE_KEY) === false) { + return true; // User has disabled warnings, proceed without showing dialog + } + + // Filter out untracked files as they typically don't conflict with PR checkout + const trackedWorkingTreeChanges = repository.state.workingTreeChanges.filter(change => change.status !== Status.UNTRACKED); + const hasTrackedWorkingTreeChanges = trackedWorkingTreeChanges.length > 0; + const hasIndexChanges = repository.state.indexChanges.length > 0; + + if (!hasTrackedWorkingTreeChanges && !hasIndexChanges) { + return true; // No tracked uncommitted changes, proceed + } + + const modalResult = await vscode.window.showInformationMessage( + vscode.l10n.t('You have uncommitted changes that might be overwritten by checking out this pull request.'), + { + modal: true, + detail: vscode.l10n.t('Choose how to handle your uncommitted changes before checking out the pull request.'), + }, + STASH_CHANGES, + DISCARD_CHANGES, + DONT_SHOW_AGAIN, + ); + + if (!modalResult) { + return false; // User cancelled + } + + if (modalResult === DONT_SHOW_AGAIN) { + // Store preference to never show this dialog again using persistent state + PersistentState.store(UNCOMMITTED_CHANGES_SCOPE, UNCOMMITTED_CHANGES_STORAGE_KEY, false); + return true; // Proceed with checkout + } + + try { + if (modalResult === STASH_CHANGES) { + // Stash all changes (working tree changes + any unstaged changes) + const allChangedFiles = [ + ...trackedWorkingTreeChanges.map(change => change.uri.fsPath), + ...repository.state.indexChanges.map(change => change.uri.fsPath), + ]; + if (allChangedFiles.length > 0) { + await repository.add(allChangedFiles); + await vscode.commands.executeCommand('git.stash', repository); + } + } else if (modalResult === DISCARD_CHANGES) { + // Discard all tracked working tree changes + const trackedWorkingTreeFiles = trackedWorkingTreeChanges.map(change => change.uri.fsPath); + if (trackedWorkingTreeFiles.length > 0) { + await repository.clean(trackedWorkingTreeFiles); + } + } + return true; // Successfully handled changes, proceed with checkout + } catch (error) { + vscode.window.showErrorMessage(vscode.l10n.t('Failed to handle uncommitted changes: {0}', formatError(error))); + return false; + } } + From d61254b80bdea73ef1bf3193eecf37418ce84486 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 5 Nov 2025 15:53:22 +0000 Subject: [PATCH 037/402] Support simplified URI format for checkout-pull-request command (#8094) * Initial plan * Initial planning for simplified checkout-pull-request URL format Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Add support for simplified URL format with uri parameter Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Update comment to document both URL formats Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Address code review: Add $ anchor to regex and test extra path segments Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Remove changed proposals --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/common/uri.ts | 21 +++++++ src/test/common/uri.test.ts | 113 ++++++++++++++++++++++++++++++++++++ src/uriHandler.ts | 3 +- 3 files changed, 136 insertions(+), 1 deletion(-) create mode 100644 src/test/common/uri.test.ts diff --git a/src/common/uri.ts b/src/common/uri.ts index ea92664ad5..5feba4fee2 100644 --- a/src/common/uri.ts +++ b/src/common/uri.ts @@ -685,6 +685,27 @@ export function fromOpenOrCheckoutPullRequestWebviewUri(uri: vscode.Uri): OpenPu return; } try { + // Check if the query uses the new simplified format: uri=https://github.com/owner/repo/pull/number + const queryParams = new URLSearchParams(uri.query); + const uriParam = queryParams.get('uri'); + if (uriParam) { + // Parse the GitHub PR URL - match only exact format ending with the PR number + const match = uriParam.match(/^https?:\/\/github\.com\/([^\/]+)\/([^\/]+)\/pull\/(\d+)$/); + if (match) { + const [, owner, repo, pullRequestNumber] = match; + const params = { + owner, + repo, + pullRequestNumber: parseInt(pullRequestNumber, 10) + }; + if (!validateOpenWebviewParams(params.owner, params.repo, params.pullRequestNumber.toString())) { + return; + } + return params; + } + } + + // Fall back to the old JSON format for backward compatibility const query = JSON.parse(uri.query.split('&')[0]); if (!validateOpenWebviewParams(query.owner, query.repo, query.pullRequestNumber)) { return; diff --git a/src/test/common/uri.test.ts b/src/test/common/uri.test.ts new file mode 100644 index 0000000000..ce9ab11d2f --- /dev/null +++ b/src/test/common/uri.test.ts @@ -0,0 +1,113 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { default as assert } from 'assert'; +import * as vscode from 'vscode'; +import { fromOpenOrCheckoutPullRequestWebviewUri } from '../../common/uri'; + +describe('uri', () => { + describe('fromOpenOrCheckoutPullRequestWebviewUri', () => { + it('should parse the new simplified format with uri parameter', () => { + const uri = vscode.Uri.parse('vscode://github.vscode-pull-request-github/checkout-pull-request?uri=https://github.com/microsoft/vscode-css-languageservice/pull/460'); + const result = fromOpenOrCheckoutPullRequestWebviewUri(uri); + + assert.strictEqual(result?.owner, 'microsoft'); + assert.strictEqual(result?.repo, 'vscode-css-languageservice'); + assert.strictEqual(result?.pullRequestNumber, 460); + }); + + it('should parse the new simplified format with http (not https)', () => { + const uri = vscode.Uri.parse('vscode://github.vscode-pull-request-github/checkout-pull-request?uri=http://github.com/owner/repo/pull/123'); + const result = fromOpenOrCheckoutPullRequestWebviewUri(uri); + + assert.strictEqual(result?.owner, 'owner'); + assert.strictEqual(result?.repo, 'repo'); + assert.strictEqual(result?.pullRequestNumber, 123); + }); + + it('should parse the old JSON format for backward compatibility', () => { + const uri = vscode.Uri.parse('vscode://github.vscode-pull-request-github/checkout-pull-request?%7B%22owner%22%3A%22microsoft%22%2C%22repo%22%3A%22vscode-css-languageservice%22%2C%22pullRequestNumber%22%3A460%7D'); + const result = fromOpenOrCheckoutPullRequestWebviewUri(uri); + + assert.strictEqual(result?.owner, 'microsoft'); + assert.strictEqual(result?.repo, 'vscode-css-languageservice'); + assert.strictEqual(result?.pullRequestNumber, 460); + }); + + it('should work for open-pull-request-webview path', () => { + const uri = vscode.Uri.parse('vscode://github.vscode-pull-request-github/open-pull-request-webview?uri=https://github.com/test/example/pull/789'); + const result = fromOpenOrCheckoutPullRequestWebviewUri(uri); + + assert.strictEqual(result?.owner, 'test'); + assert.strictEqual(result?.repo, 'example'); + assert.strictEqual(result?.pullRequestNumber, 789); + }); + + it('should return undefined for invalid authority', () => { + const uri = vscode.Uri.parse('vscode://invalid-authority/checkout-pull-request?uri=https://github.com/owner/repo/pull/1'); + const result = fromOpenOrCheckoutPullRequestWebviewUri(uri); + + assert.strictEqual(result, undefined); + }); + + it('should return undefined for invalid path', () => { + const uri = vscode.Uri.parse('vscode://github.vscode-pull-request-github/invalid-path?uri=https://github.com/owner/repo/pull/1'); + const result = fromOpenOrCheckoutPullRequestWebviewUri(uri); + + assert.strictEqual(result, undefined); + }); + + it('should return undefined for invalid GitHub URL format', () => { + const uri = vscode.Uri.parse('vscode://github.vscode-pull-request-github/checkout-pull-request?uri=https://example.com/owner/repo/pull/1'); + const result = fromOpenOrCheckoutPullRequestWebviewUri(uri); + + assert.strictEqual(result, undefined); + }); + + it('should return undefined for non-numeric pull request number', () => { + const uri = vscode.Uri.parse('vscode://github.vscode-pull-request-github/checkout-pull-request?uri=https://github.com/owner/repo/pull/abc'); + const result = fromOpenOrCheckoutPullRequestWebviewUri(uri); + + assert.strictEqual(result, undefined); + }); + + it('should handle repos with dots and dashes', () => { + const uri = vscode.Uri.parse('vscode://github.vscode-pull-request-github/checkout-pull-request?uri=https://github.com/my-org/my.awesome-repo/pull/42'); + const result = fromOpenOrCheckoutPullRequestWebviewUri(uri); + + assert.strictEqual(result?.owner, 'my-org'); + assert.strictEqual(result?.repo, 'my.awesome-repo'); + assert.strictEqual(result?.pullRequestNumber, 42); + }); + + it('should handle repos with underscores', () => { + const uri = vscode.Uri.parse('vscode://github.vscode-pull-request-github/checkout-pull-request?uri=https://github.com/owner/repo_name/pull/1'); + const result = fromOpenOrCheckoutPullRequestWebviewUri(uri); + + assert.strictEqual(result?.owner, 'owner'); + assert.strictEqual(result?.repo, 'repo_name'); + assert.strictEqual(result?.pullRequestNumber, 1); + }); + + it('should validate owner and repo names', () => { + // Invalid owner (empty) + const uri1 = vscode.Uri.parse('vscode://github.vscode-pull-request-github/checkout-pull-request?uri=https://github.com//repo/pull/1'); + const result1 = fromOpenOrCheckoutPullRequestWebviewUri(uri1); + assert.strictEqual(result1, undefined); + }); + + it('should reject URLs with extra path segments after PR number', () => { + // URL with /files suffix should be rejected + const uri1 = vscode.Uri.parse('vscode://github.vscode-pull-request-github/checkout-pull-request?uri=https://github.com/owner/repo/pull/123/files'); + const result1 = fromOpenOrCheckoutPullRequestWebviewUri(uri1); + assert.strictEqual(result1, undefined); + + // URL with /commits suffix should be rejected + const uri2 = vscode.Uri.parse('vscode://github.vscode-pull-request-github/checkout-pull-request?uri=https://github.com/owner/repo/pull/456/commits'); + const result2 = fromOpenOrCheckoutPullRequestWebviewUri(uri2); + assert.strictEqual(result2, undefined); + }); + }); +}); diff --git a/src/uriHandler.ts b/src/uriHandler.ts index a9c6ac4abc..73fdf128ab 100644 --- a/src/uriHandler.ts +++ b/src/uriHandler.ts @@ -115,7 +115,8 @@ export class UriHandler implements vscode.UriHandler { case UriHandlerPaths.OpenPullRequestWebview: return this._openPullRequestWebview(uri); case UriHandlerPaths.CheckoutPullRequest: - // example vscode-insiders://github.vscode-pull-request-github/checkout-pull-request?%7B%22owner%22%3A%22alexr00%22%2C%22repo%22%3A%22playground%22%2C%22pullRequestNumber%22%3A714%7D + // Simplified format example: vscode-insiders://github.vscode-pull-request-github/checkout-pull-request?uri=https://github.com/microsoft/vscode-css-languageservice/pull/460 + // Legacy format example: vscode-insiders://github.vscode-pull-request-github/checkout-pull-request?%7B%22owner%22%3A%22alexr00%22%2C%22repo%22%3A%22playground%22%2C%22pullRequestNumber%22%3A714%7D return this._checkoutPullRequest(uri); } } From 341bcf7bf8487950f9e74b2a0ae826f3288f67c8 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Wed, 5 Nov 2025 17:26:03 +0100 Subject: [PATCH 038/402] Use regex groups (#8097) --- .github/copilot-instructions.md | 1 + src/common/uri.ts | 8 +++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 7413725c41..0c48ce8f75 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -5,6 +5,7 @@ - **Strictness**: Some strictness is disabled in `tsconfig.base.json` (e.g., `strictNullChecks: false`), but new code should avoid unsafe patterns. - **Testing**: Place tests under `src/test`. Do not include test code in production files. - **Localization**: Use `%key%` syntax for strings that require localization. See `package.nls.json`. +- **Regular Expressions**: Use named capture groups for clarity when working with complex regex patterns. ## Extension-Specific Practices - **VS Code API**: Use the official VS Code API for all extension points. Register commands, views, and menus via `package.json`. diff --git a/src/common/uri.ts b/src/common/uri.ts index 5feba4fee2..7258baa2c3 100644 --- a/src/common/uri.ts +++ b/src/common/uri.ts @@ -690,9 +690,11 @@ export function fromOpenOrCheckoutPullRequestWebviewUri(uri: vscode.Uri): OpenPu const uriParam = queryParams.get('uri'); if (uriParam) { // Parse the GitHub PR URL - match only exact format ending with the PR number - const match = uriParam.match(/^https?:\/\/github\.com\/([^\/]+)\/([^\/]+)\/pull\/(\d+)$/); - if (match) { - const [, owner, repo, pullRequestNumber] = match; + // Use named regex groups for clarity + const prUrlRegex = /^https?:\/\/github\.com\/(?[^\/]+)\/(?[^\/]+)\/pull\/(?\d+)$/; + const match = prUrlRegex.exec(uriParam); + if (match && match.groups) { + const { owner, repo, pullRequestNumber } = match.groups; const params = { owner, repo, From 0023b4486a49f5285370ad0269eddf48e0762187 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Thu, 6 Nov 2025 11:53:59 +0100 Subject: [PATCH 039/402] Add logging for repository workspaces (#8104) Part of microsoft/vscode#275356 --- src/uriHandler.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/uriHandler.ts b/src/uriHandler.ts index 73fdf128ab..817f6e17ee 100644 --- a/src/uriHandler.ts +++ b/src/uriHandler.ts @@ -176,6 +176,7 @@ export class UriHandler implements vscode.UriHandler { return; } if (workspaces && workspaces.length) { + Logger.appendLine(`Found workspaces for ${remoteUri.toString()}: ${workspaces.map(w => w.toString()).join(', ')}`, UriHandler.ID); progress.report({ message: vscode.l10n.t('Opening workspace') }); await this._savePendingCheckoutAndOpenFolder(params, workspaces[0]); } else { From 7bc0db6d6612863d4460df0b643fae5a236e610a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 6 Nov 2025 11:32:48 +0000 Subject: [PATCH 040/402] Show empty commits with centered webview instead of notification (#8103) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Initial plan * Initial planning for empty commit display feature Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Add EmptyCommitFileSystemProvider for displaying empty commits Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Replace file system provider with webview for empty commits - Removed EmptyCommitFileSystemProvider and related URI scheme - Created showEmptyCommitWebview() function with centered message - Updated openCommitChanges() to accept extensionUri and show webview - Styled webview to display message centered similar to GitHub.com Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Update empty commit webview styling to match design - Added SVG document icon above the message - Split message into title and subtitle with different styling - Title: "No changes to show." in regular foreground color - Subtitle: "This commit has no content." in muted description color - Adjusted spacing and font sizes to match GitHub.com design Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * 💄 * clean up * Add icon path to empty commit webview panel - Set git-commit icon for the webview panel tab - Icon appears in both light and dark themes - Uses codicons/git-commit.svg from resources Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * more clean up --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- ...ode.proposed.chatParticipantAdditions.d.ts | 17 +++- ...scode.proposed.chatParticipantPrivate.d.ts | 2 + src/github/emptyCommitWebview.ts | 83 +++++++++++++++++++ src/github/pullRequestModel.ts | 6 +- src/github/pullRequestOverview.ts | 2 +- 5 files changed, 106 insertions(+), 4 deletions(-) create mode 100644 src/github/emptyCommitWebview.ts diff --git a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts index f05e4fe691..f4ce44cd9e 100644 --- a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts +++ b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts @@ -167,7 +167,14 @@ declare module 'vscode' { constructor(value: ChatResponseDiffEntry[], title: string, readOnly?: boolean); } - export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseNotebookEditPart | ChatResponseConfirmationPart | ChatResponseCodeCitationPart | ChatResponseReferencePart2 | ChatResponseMovePart | ChatResponseExtensionsPart | ChatResponsePullRequestPart | ChatPrepareToolInvocationPart | ChatToolInvocationPart | ChatResponseMultiDiffPart | ChatResponseThinkingProgressPart; + export class ChatResponseExternalEditPart { + uris: Uri[]; + callback: () => Thenable; + applied: Thenable; + constructor(uris: Uri[], callback: () => Thenable); + } + + export type ExtendedChatResponsePart = ChatResponsePart | ChatResponseTextEditPart | ChatResponseNotebookEditPart | ChatResponseConfirmationPart | ChatResponseCodeCitationPart | ChatResponseReferencePart2 | ChatResponseMovePart | ChatResponseExtensionsPart | ChatResponsePullRequestPart | ChatPrepareToolInvocationPart | ChatToolInvocationPart | ChatResponseMultiDiffPart | ChatResponseThinkingProgressPart | ChatResponseExternalEditPart; export class ChatResponseWarningPart { value: MarkdownString; constructor(value: string | MarkdownString); @@ -301,6 +308,14 @@ declare module 'vscode' { notebookEdit(target: Uri, isDone: true): void; + /** + * Makes an external edit to one or more resources. Changes to the + * resources made within the `callback` and before it resolves will be + * tracked as agent edits. This can be used to track edits made from + * external tools that don't generate simple {@link textEdit textEdits}. + */ + externalEdit(target: Uri | Uri[], callback: () => Thenable): Thenable; + markdownWithVulnerabilities(value: string | MarkdownString, vulnerabilities: ChatVulnerability[]): void; codeblockUri(uri: Uri, isEdit?: boolean): void; push(part: ChatResponsePart | ChatResponseTextEditPart | ChatResponseWarningPart | ChatResponseProgressPart2): void; diff --git a/src/@types/vscode.proposed.chatParticipantPrivate.d.ts b/src/@types/vscode.proposed.chatParticipantPrivate.d.ts index dca0f364d7..398dce07fd 100644 --- a/src/@types/vscode.proposed.chatParticipantPrivate.d.ts +++ b/src/@types/vscode.proposed.chatParticipantPrivate.d.ts @@ -87,6 +87,8 @@ declare module 'vscode' { * Events for edited files in this session collected since the last request. */ readonly editedFileEvents?: ChatRequestEditedFileEvent[]; + + readonly isSubagent?: boolean; } export enum ChatRequestEditedFileEventKind { diff --git a/src/github/emptyCommitWebview.ts b/src/github/emptyCommitWebview.ts new file mode 100644 index 0000000000..29a5f8127e --- /dev/null +++ b/src/github/emptyCommitWebview.ts @@ -0,0 +1,83 @@ +/*--------------------------------------------------------------------------------------------- + * 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'; + +/** + * Opens a webview panel to display a message for an empty commit. + * The message is centered and styled similar to GitHub.com. + */ +export function showEmptyCommitWebview(extensionUri: vscode.Uri, commitSha: string): void { + const panel = vscode.window.createWebviewPanel( + 'emptyCommit', + vscode.l10n.t('Commit {0}', commitSha.substring(0, 7)), + vscode.ViewColumn.Active, + { + enableScripts: false, + localResourceRoots: [] + } + ); + + panel.iconPath = { + light: vscode.Uri.joinPath(extensionUri, 'resources', 'icons', 'codicons', 'git-commit.svg'), + dark: vscode.Uri.joinPath(extensionUri, 'resources', 'icons', 'codicons', 'git-commit.svg') + }; + + panel.webview.html = getEmptyCommitHtml(); +} + +function getEmptyCommitHtml(): string { + return ` + + + + + Empty Commit + + + +
        +
        + +
        +
        No changes to show.
        +
        This commit has no content.
        +
        + +`; +} diff --git a/src/github/pullRequestModel.ts b/src/github/pullRequestModel.ts index abc657bd93..0859707f7f 100644 --- a/src/github/pullRequestModel.ts +++ b/src/github/pullRequestModel.ts @@ -11,6 +11,7 @@ import * as vscode from 'vscode'; import { OctokitCommon } from './common'; import { ConflictResolutionModel } from './conflictResolutionModel'; import { CredentialStore } from './credentials'; +import { showEmptyCommitWebview } from './emptyCommitWebview'; import { FolderRepositoryManager } from './folderRepositoryManager'; import { GitHubRepository, GraphQLError, GraphQLErrorType } from './githubRepository'; import { @@ -1465,7 +1466,7 @@ export class PullRequestModel extends IssueModel implements IPullRe return vscode.commands.executeCommand('vscode.changes', vscode.l10n.t('Changes in Pull Request #{0}', pullRequestModel.number), args); } - static async openCommitChanges(githubRepository: GitHubRepository, commitSha: string) { + static async openCommitChanges(extensionUri: vscode.Uri, githubRepository: GitHubRepository, commitSha: string) { try { const parentCommit = await githubRepository.getCommitParent(commitSha); if (!parentCommit) { @@ -1475,7 +1476,8 @@ export class PullRequestModel extends IssueModel implements IPullRe const changes = await githubRepository.compareCommits(parentCommit, commitSha); if (!changes?.files || changes.files.length === 0) { - vscode.window.showInformationMessage(vscode.l10n.t('No changes found in commit {0}', commitSha.substring(0, 7))); + // Show a webview with the empty commit message instead of a notification + showEmptyCommitWebview(extensionUri, commitSha); return; } diff --git a/src/github/pullRequestOverview.ts b/src/github/pullRequestOverview.ts index 1b05c200c6..de64cf068d 100644 --- a/src/github/pullRequestOverview.ts +++ b/src/github/pullRequestOverview.ts @@ -554,7 +554,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel): Promise { try { const { commitSha } = message.args; - await PullRequestModel.openCommitChanges(this._item.githubRepository, commitSha); + await PullRequestModel.openCommitChanges(this._extensionUri, this._item.githubRepository, commitSha); this._replyMessage(message, {}); } catch (error) { Logger.error(`Failed to open commit changes: ${formatError(error)}`, PullRequestOverviewPanel.ID); From dc8641a74219658672325b510a1a64a2364a7880 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Thu, 6 Nov 2025 12:53:20 +0100 Subject: [PATCH 041/402] git post commit logs to info (#8107) --- src/extension.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index 651497c04a..d2aeadcabd 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -355,7 +355,7 @@ function registerPostCommitCommandsProvider(reposManager: RepositoriesManager, g class Provider implements PostCommitCommandsProvider { getCommands(repository: Repository) { - Logger.debug(`Looking for remote. Comparing ${repository.state.remotes.length} local repo remotes with ${reposManager.folderManagers.reduce((prev, curr) => prev + curr.gitHubRepositories.length, 0)} GitHub repositories.`, componentId); + Logger.appendLine(`Looking for remote. Comparing ${repository.state.remotes.length} local repo remotes with ${reposManager.folderManagers.reduce((prev, curr) => prev + curr.gitHubRepositories.length, 0)} GitHub repositories.`, componentId); const repoRemotes = parseRepositoryRemotes(repository); const found = reposManager.folderManagers.find(folderManager => folderManager.findRepo(githubRepo => { @@ -363,7 +363,7 @@ function registerPostCommitCommandsProvider(reposManager: RepositoriesManager, g return remote.equals(githubRepo.remote); }); })); - Logger.debug(`Found ${found ? 'a repo' : 'no repos'} when getting post commit commands.`, componentId); + Logger.appendLine(`Found ${found ? 'a repo' : 'no repos'} when getting post commit commands.`, componentId); return found ? [{ command: 'pr.pushAndCreate', title: vscode.l10n.t('{0} Commit & Create Pull Request', '$(git-pull-request-create)'), @@ -376,9 +376,9 @@ function registerPostCommitCommandsProvider(reposManager: RepositoriesManager, g return reposManager.folderManagers.some(folderManager => folderManager.gitHubRepositories.length > 0); } function tryRegister(): boolean { - Logger.debug('Trying to register post commit commands.', 'GitPostCommitCommands'); + Logger.appendLine('Trying to register post commit commands.', 'GitPostCommitCommands'); if (hasGitHubRepos()) { - Logger.debug('GitHub remote(s) found, registering post commit commands.', componentId); + Logger.appendLine('GitHub remote(s) found, registering post commit commands.', componentId); git.registerPostCommitCommandsProvider(new Provider()); return true; } @@ -395,7 +395,7 @@ function registerPostCommitCommandsProvider(reposManager: RepositoriesManager, g } async function deferredActivateRegisterBuiltInGitProvider(context: vscode.ExtensionContext, apiImpl: GitApiImpl, credentialStore: CredentialStore) { - Logger.debug('Registering built in git provider.', 'Activation'); + Logger.appendLine('Registering built in git provider.', 'Activation'); if (!(await doRegisterBuiltinGitProvider(context, credentialStore, apiImpl))) { const extensionsChangedDisposable = vscode.extensions.onDidChange(async () => { if (await doRegisterBuiltinGitProvider(context, credentialStore, apiImpl)) { From aa98ae31c00d54b1a69a6a5298b7f8485a583b24 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Thu, 6 Nov 2025 13:46:24 +0100 Subject: [PATCH 042/402] Dispose of post commit command providers (#8109) --- src/extension.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/extension.ts b/src/extension.ts index d2aeadcabd..78f8936cb0 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -265,7 +265,7 @@ async function init( await vscode.commands.executeCommand('setContext', 'github:initialized', true); - registerPostCommitCommandsProvider(reposManager, git); + registerPostCommitCommandsProvider(context, reposManager, git); // Resume any pending checkout request stored before workspace reopened. await resumePendingCheckout(reviewsManager, context, reposManager); @@ -350,7 +350,7 @@ async function doRegisterBuiltinGitProvider(context: vscode.ExtensionContext, cr return false; } -function registerPostCommitCommandsProvider(reposManager: RepositoriesManager, git: GitApiImpl) { +function registerPostCommitCommandsProvider(context: vscode.ExtensionContext, reposManager: RepositoriesManager, git: GitApiImpl) { const componentId = 'GitPostCommitCommands'; class Provider implements PostCommitCommandsProvider { @@ -379,7 +379,7 @@ function registerPostCommitCommandsProvider(reposManager: RepositoriesManager, g Logger.appendLine('Trying to register post commit commands.', 'GitPostCommitCommands'); if (hasGitHubRepos()) { Logger.appendLine('GitHub remote(s) found, registering post commit commands.', componentId); - git.registerPostCommitCommandsProvider(new Provider()); + context.subscriptions.push(git.registerPostCommitCommandsProvider(new Provider())); return true; } return false; From 5c905bd16c2258ef1bcdab74bc578a0dbdd0fe7d Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Thu, 6 Nov 2025 15:44:50 +0100 Subject: [PATCH 043/402] update codicons (#8110) --- resources/icons/codicons/close.svg | 2 +- resources/icons/codicons/trash.svg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/resources/icons/codicons/close.svg b/resources/icons/codicons/close.svg index f6fc1bca88..033334911e 100644 --- a/resources/icons/codicons/close.svg +++ b/resources/icons/codicons/close.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/resources/icons/codicons/trash.svg b/resources/icons/codicons/trash.svg index cb5ac5d1f3..fd1c66aa77 100644 --- a/resources/icons/codicons/trash.svg +++ b/resources/icons/codicons/trash.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file From de4e3361c8d22671e93d49d9ee3743ccf235f309 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Thu, 6 Nov 2025 17:56:19 +0100 Subject: [PATCH 044/402] Fix elusive missing post commit action (#8111) --- src/github/folderRepositoryManager.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/github/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index cf73691d48..8448c00f95 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -489,8 +489,10 @@ export class FolderRepositoryManager extends Disposable { const activeRemotes = await this.getActiveRemotes(); const isAuthenticated = this.checkForAuthMatch(activeRemotes); if (this.credentialStore.isAnyAuthenticated() && (activeRemotes.length === 0)) { - const areAllNeverGitHub = (await this.computeAllUnknownRemotes()).every(remote => GitHubManager.isNeverGitHub(vscode.Uri.parse(remote.normalizedHost).authority)); - if (areAllNeverGitHub) { + const allUnknownRemotes = await this.computeAllUnknownRemotes(); + const areAllNeverGitHub = allUnknownRemotes.every(remote => GitHubManager.isNeverGitHub(vscode.Uri.parse(remote.normalizedHost).authority)); + if ((allUnknownRemotes.length > 0) && areAllNeverGitHub) { + Logger.appendLine('No GitHub remotes found and all remotes are marked as never GitHub.', this.id); this.state = ReposManagerState.RepositoriesLoaded; return true; } From 35df9c107ad284fdcb28ad83a51b808e0257cdaf Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:31:49 +0100 Subject: [PATCH 045/402] Get rid of an any (#8114) --- webviews/components/sidebar.tsx | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/webviews/components/sidebar.tsx b/webviews/components/sidebar.tsx index c82f3df03e..ac4eddd361 100644 --- a/webviews/components/sidebar.tsx +++ b/webviews/components/sidebar.tsx @@ -288,12 +288,14 @@ function CollapsedLabel(props: PullRequest) { ); - const PillContainer = ({ items, getKey, getColor, getText }: { - items: any[], - getKey: (item: any) => string, - getColor: (item: any) => { backgroundColor: string; textColor: string; borderColor: string }, - getText: (item: any) => string - }) => { + interface PillContainerProps { + items: T[]; + getKey: (item: T) => string; + getColor: (item: T) => { backgroundColor: string; textColor: string; borderColor: string }; + getText: (item: T) => string; + } + + const PillContainer = ({ items, getKey, getColor, getText }: PillContainerProps) => { const containerRef = useRef(null); const [visibleCount, setVisibleCount] = useState(items.length); From 8e1eb4a5df2fdc5aaedf29d238e610436a2ad44c Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 7 Nov 2025 12:11:15 +0000 Subject: [PATCH 046/402] Fix emoji rendering in collapsed sidebar labels (#8113) * Initial plan * Fix: Use displayName for label emojis in compact view Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- webviews/components/sidebar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/webviews/components/sidebar.tsx b/webviews/components/sidebar.tsx index ac4eddd361..84f0c6ab09 100644 --- a/webviews/components/sidebar.tsx +++ b/webviews/components/sidebar.tsx @@ -383,7 +383,7 @@ function CollapsedLabel(props: PullRequest) { items={labels} getKey={l => l.name} getColor={l => gitHubLabelColor(l.color, props?.isDarkTheme, false)} - getText={l => l.name} + getText={l => l.displayName} /> ), count: labels.length From 1b1de9f02d2599c2e682a2a19a3b09dc3ce8a91c Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Fri, 7 Nov 2025 15:47:57 +0100 Subject: [PATCH 047/402] Adopt scm/repository (#8105) * Adopt scm/repository * navigation -> inline * remove context menu --- package.json | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/package.json b/package.json index b4b768559d..442a0f740b 100644 --- a/package.json +++ b/package.json @@ -3146,6 +3146,13 @@ "group": "1_modification@5" } ], + "scm/repository": [ + { + "command": "pr.create", + "when": "scmProvider =~ /^git|^remoteHub:github/ && scmProviderRootUri in github:reposNotInReviewMode", + "group": "inline" + } + ], "comments/commentThread/context": [ { "command": "pr.createComment", From 9c9457f5955037fbfa14d502ddc00210c0b7c2cc Mon Sep 17 00:00:00 2001 From: Kyle Cutler <67761731+kycutler@users.noreply.github.com> Date: Mon, 10 Nov 2025 01:34:38 -0800 Subject: [PATCH 048/402] 0.122.0 changelog (#8117) * 0.122.0 changelog * add, reorder --- CHANGELOG.md | 23 ++++++++++++++++++ .../changelog/0.122.0/emoji-completions.gif | Bin 0 -> 46783 bytes .../changelog/0.122.0/markdown-alerts.png | Bin 0 -> 40366 bytes documentation/changelog/0.122.0/pr-labels.png | Bin 0 -> 19159 bytes 4 files changed, 23 insertions(+) create mode 100644 documentation/changelog/0.122.0/emoji-completions.gif create mode 100644 documentation/changelog/0.122.0/markdown-alerts.png create mode 100644 documentation/changelog/0.122.0/pr-labels.png diff --git a/CHANGELOG.md b/CHANGELOG.md index 3eb84103c3..a117a3d379 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ # Changelog +## 0.122.0 + +### Changes + +- Auto-generated PR descriptions (via `setting(githubPullRequests.pullRequestDescription:Copilot)`) will respect the repository PR template if there is one. +- Icons in the Pull Requests view now render with codicons instead of Unicode characters. +- Drafts in the Pull Requests view now render in italics instead of having a `[DRAFT]` prefix. + +![Pull Requests view showing codicon labels and italic draft PR titles](./documentation/changelog/0.122.0/pr-labels.png) + +- Emoji completions for `:smile:` style emojis are now available in review comments. + +![Emoji completions in review comments](./documentation/changelog/0.122.0/emoji-completions.gif) + +- [Markdown alert syntax](https://github.com/orgs/community/discussions/16925) is now rendered in review comments. + +![Markdown alerts in review comments](./documentation/changelog/0.122.0/markdown-alerts.png) + +### Fixes + +- Reactions to code comments are not showing up (Web). https://github.com/microsoft/vscode-pull-request-github/issues/2195 + + ## 0.120.2 ### Fixes diff --git a/documentation/changelog/0.122.0/emoji-completions.gif b/documentation/changelog/0.122.0/emoji-completions.gif new file mode 100644 index 0000000000000000000000000000000000000000..e38f3323e647a237b79cc406fb3ba70e6141562e GIT binary patch literal 46783 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 literal 0 HcmV?d00001 diff --git a/documentation/changelog/0.122.0/markdown-alerts.png b/documentation/changelog/0.122.0/markdown-alerts.png new file mode 100644 index 0000000000000000000000000000000000000000..c222af3ad2d22d3fe878490fb7a17766a8ee3439 GIT binary patch literal 40366 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 XULWn2S@GSw00000NkvXXu0mjf5u%jf literal 0 HcmV?d00001 From 9f93ba8b424b8c75089a0426abd32178ecc7d01d Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Mon, 10 Nov 2025 10:59:01 +0100 Subject: [PATCH 049/402] Fix build (#8122) --- src/@types/vscode.proposed.chatSessionsProvider.d.ts | 5 +++++ src/github/copilotRemoteAgent.ts | 4 +++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/@types/vscode.proposed.chatSessionsProvider.d.ts b/src/@types/vscode.proposed.chatSessionsProvider.d.ts index 3e7c795fbe..489e5f952c 100644 --- a/src/@types/vscode.proposed.chatSessionsProvider.d.ts +++ b/src/@types/vscode.proposed.chatSessionsProvider.d.ts @@ -123,6 +123,11 @@ declare module 'vscode' { * Statistics about the chat session. */ statistics?: { + /** + * Number of files edited during the session. + */ + files: number; + /** * Number of insertions made during the session. */ diff --git a/src/github/copilotRemoteAgent.ts b/src/github/copilotRemoteAgent.ts index ff5d2dfd17..e31b3b0c6b 100644 --- a/src/github/copilotRemoteAgent.ts +++ b/src/github/copilotRemoteAgent.ts @@ -1064,6 +1064,7 @@ export class CopilotRemoteAgentManager extends Disposable { const repo = pullRequest.remote.repositoryName; repoInfo = `${owner}/${repo} `; } + const fileCount = pullRequest.fileChanges.size === 0 ? (await pullRequest.getFileChangesInfo()).length : pullRequest.fileChanges.size; const description = new vscode.MarkdownString(`[${repoInfo}#${pullRequest.number}](${uri.toString()} "${prLinkTitle}")`); // pullRequest.base.ref === defaultBranch ? `PR #${pullRequest.number}`: `PR #${pullRequest.number} → ${pullRequest.base.ref}`; const chatSession: ChatSessionWithPR = { resource: vscode.Uri.from({ scheme: COPILOT_SWE_AGENT, path: '/' + pullRequest.number }), @@ -1078,7 +1079,8 @@ export class CopilotRemoteAgentManager extends Disposable { }, statistics: pullRequest.item.additions !== undefined && pullRequest.item.deletions !== undefined && (pullRequest.item.additions > 0 || pullRequest.item.deletions > 0) ? { insertions: pullRequest.item.additions, - deletions: pullRequest.item.deletions + deletions: pullRequest.item.deletions, + files: fileCount } : undefined }; return chatSession; From f2a335d7b7ce1867ff95202582ffc0c6e113a7a6 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Mon, 10 Nov 2025 11:40:35 +0100 Subject: [PATCH 050/402] 0.122.0 changelog (#8123) --- CHANGELOG.md | 18 +++++++++++++++++- .../changelog/0.122.0/cancel-review.png | Bin 0 -> 14698 bytes 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 documentation/changelog/0.122.0/cancel-review.png diff --git a/CHANGELOG.md b/CHANGELOG.md index a117a3d379..ba42edc6e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,7 +4,7 @@ ### Changes -- Auto-generated PR descriptions (via `setting(githubPullRequests.pullRequestDescription:Copilot)`) will respect the repository PR template if there is one. +- Auto-generated PR descriptions (via `githubPullRequests.pullRequestDescription`) will respect the repository PR template if there is one. - Icons in the Pull Requests view now render with codicons instead of Unicode characters. - Drafts in the Pull Requests view now render in italics instead of having a `[DRAFT]` prefix. @@ -18,10 +18,26 @@ ![Markdown alerts in review comments](./documentation/changelog/0.122.0/markdown-alerts.png) +- Opening an empty commit from a pull request webview shows an editor with a message instead of showing a notification. +- Pull requests can be opened from from a url, for example: `vscode-insiders://github.vscode-pull-request-github/checkout-pull-request?uri=https://github.com/microsoft/vscode-css-languageservice/pull/460` +- Icons are up-to-date with VS Code's latest icons. +- If you start a review and want to cancel it, there's now a "Cancel Review" button in the pull request webview. + +![Cancel review button](./documentation/changelog/0.122.0/cancel-review.png) + ### Fixes - Reactions to code comments are not showing up (Web). https://github.com/microsoft/vscode-pull-request-github/issues/2195 +- Editing a comment freezes VS Code. https://github.com/microsoft/vscode/issues/274455 +- Github Pull Request tab won't open if branch names are reused. https://github.com/microsoft/vscode-pull-request-github/issues/8007 +- Icons are misaligned. https://github.com/microsoft/vscode-pull-request-github/issues/7998 +- "Git is not installed or otherwise not available" even though it is. https://github.com/microsoft/vscode-pull-request-github/issues/5454 + +**_Thank You_** +* [@bendrucker (Ben Drucker)](https://github.com/bendrucker): Enable all LLM tools in prompts (agent mode) [PR #6956](https://github.com/microsoft/vscode-pull-request-github/pull/6956) +* [@gerardbalaoro (Gerard Balaoro)](https://github.com/gerardbalaoro): Make branch list timeout configurable (#2840) [PR #7927](https://github.com/microsoft/vscode-pull-request-github/pull/7927) +* [@wankun-tcj](https://github.com/wankun-tcj): Fix avatar display issue in Pull Request tree view [PR #7851](https://github.com/microsoft/vscode-pull-request-github/pull/7851) ## 0.120.2 diff --git a/documentation/changelog/0.122.0/cancel-review.png b/documentation/changelog/0.122.0/cancel-review.png new file mode 100644 index 0000000000000000000000000000000000000000..ac3473fe062ee6b7c107abfd013718227b438821 GIT binary patch literal 14698 zcmeHucT`i`_b1B34m=e^0l@+)@Brx$s`Mf~^rj%a_Ywl40xC`EJ#(7#{rHP3E$`}KvyGs2>%tF6{}0IeO`F4hsr^`f_t97Ph{E>Vk#A$)im?Q0>DHVA zD5P=B%Kc^_I~3J+?jZ~Hp5{67Hkg+B;mPHER_c`|<{YqV;LlXKjJk1}Pj?i>s8^b6 zml40``0!efo_haW<&_KdO7qv9fAbXIQpH^qlNtu6Q&O0Cg?X`-Klo*b&OG;we>Zb3 z@(*cD*JWPFJ0)?8yx=sHwxQum8KMWKp?n?ze^*}eqxm90l}X9{DzI+Ntl)L{8873D z7vF$=6f=L?o#B%aRb_yDu%UC&s2V>#|KTikU`yy_O7^>w5AW1>JOF|ZSpP(v%0}B3 z7%;KPssm34C)xOpy<`S%g!Fk9w5gB01$LXA0!+vwBid>Cqg*t>l2MoiVATwta@qRR zIa0Lq(7T5e{~-lgZ|7|BV~>WZP`j4(CP<{iW0>32%}=;n_jpxqf1ZwA9=ZMbeXJ?) z)6lqc7+EfJkNR#Gy(DiyV44tA z9`6unAqJ=HrHU`*43zI;Yj1YTRoR^C4`WRRX+DacN-6xPc|WEcyu9vz5k=c#?Y1j* z;ZjXj3`Dl61;tzbwzmV|fGqOAFj!7|@vD^oqS%PNs~gw~T4+6Pk=NdV!Dm?I{Ty~_ zC@)7Jt_ALr<&TxjCTtAct>6r6_`-3eFY`4LnOTIYU7;JK4nfYr-5b@tSu@eYY6+t& zCk-~sg=(1+MDv(5>&gx-lsF3+4G6!@G}-N^5uabXimbHxIv_nR$uE|dfNhJ!>OnI- zT8L+u*GUQnp(U>`S63Rs(6-ixOKN| zh^66~=!`5fp6<+|1ZlQX-q_Zl>Lc`VXAor<3GthzWhAtP2cY_; z!{6#sJgd%z9g0kb5%zmKmQ)m9g=DJTFm4B4*0j)po|Sp~6qdvMsw` zK#`G=%fmg_zI)WUetI5Z{$bOWnVEUT^JK$2g^9&^FVa`kX|rL$fu?P=YY5Gwih*FA zXTLPF$>z5edJ+$Hgqk7u%^vhi5#BI7ToPl zWfb%5eL{EbTDOrIKOJ3}+&%q?wlWC`evPV$wMN?^VR|h#fu{ESMm)%Ue7m;RbD{vy zkVdzmEAEp=wY47O74HW1tUfxyqb_MDaoL9lC<`KO3Z-_gIqfce3|UKIA#QbXE>i`$ zBxFbYf?pK%*}YEBCTAH*cepYwuQE~VW{iKW@8aY%cs1d~j3Ysfs$x&uqX`Y+M^<;$ ziuq*kDh*KPHoE)eY}wnRH`+=iB$_?sLX}8M?@6EcJ@nMQc4{WVTMlcF4}XO8!KAf% zQY{6VS`H7fy2ATQ@25{tT+4eFZKbbXNlL8-bbM7VFDoB_8>>t+7@tYATwDh%i*xW+!nGwxpI1>KGsiTcx%pP32AerY0Y@F?a2BtuZ?T@ zWq5cIFhB9vGzT7<506aL2n@)zz1&(?uTo;2Nmk=5WNM9j2SN1rprF zkgRKyS@5r4pHb#N%Ad%=7(Q$spTsXmqEw46w*do zui8;@Ny!=LaR-O55Dkq&!pfV!Fjw6VE~+AQl@xZR@ZP`b#n)sJIAO-`4i^(MGr=3) zgdBGZzttsgy68y0hYuCzhE?I=L7W&9;|CA)E?l~#m!!R7VdL(B9W3>iGf z=(;%rrstG0Fgq)UiJ41M*ap z8BP2i*H~gOyu2RPm*8a|6o~5~kO3I2J*XD50huOaF)_3zr>udAW!Rgk zsb(YUuzUmAnqFN4D`vtkzS7yL?AqR`P5M5$ubJpUMrA>*1ua4wK6-k20edobK155n zZnV;xhY<}lgslf)(iY#pztPmu86{4);3ad#2-d^JEP@Yn{TUe6%YPLq&zX)4M>*|& zn>9b2Yw|xjU?Fk&CU`W@i?5A0+2AbErnCl$#H6MT%(km%s(I@rC(86ZZmNR%5Bxgv zRn+ya2_v0SLoJjga=+GdXkepJsqLu43STWLKqJu?|X63_DUKPurt-ppR9vNV>9z^{NT|4_Ex>>zh`< z5qOi>&QQl11Eu2Tfo;FVa2@@n{`-EEFKqq;XNSM`poU{pWtG-~Njuts%lZ2~DZ;_= z$akW1j?w`)XKZ-FhjU2Fq-^LN(7Y47M z!<62B81nphYr$G}>q|h+Uln?WN=dA&tp4Lik(pAiC3gx6L|uNJvl8}_qobyk;a^%AFQ^#w;B(Id*;0%HTF9es*|9=9JynsXkh(kj8%LO z29ceed-nw;*`cGOV_ahX&Kj}|WH_RhYLt$Cc^Y{jzNyp&V}X=(0p z9Q%~auz*HG!y*2IceRg?kN=UfP8LpRsIL%_2sShfPK%Z|Eqvjkv>`{b~UaGPUbiQLf)R6AFAvb4FNKrsUrTJ+TUZft-q zyS#&&yx->YRfgyynG|b8PY)F`axeYhYAWbIL2^K52O7yo z=NibW-ynY+n&~?Ae81y9Zd0@}c&=ETHR)%vj>B?3zshj*z_R-Ni@tT0u#!_k!*3}3 z^CvqQ+kGkcAk<2@D=8Vf;KA~v#G4dTGAagl^6CR?28@Y^^ ztb&xX_p8%$gy)WyCa^yWXprS-`RHFA9~K7ApS^rR@0}$*F~oB}jM-!qUR7dUn|F(N zz6!4UTzkFbmTz^`I7>f<&ajj)mm|F!jbQ}bxYdGHk@3if!1V(?s$H9#UQr7xeEar7 zv14q~$c_B%ZJ#(+*^)^bZVl!XX247FfbL;~`lW=0H96|kxI0vU9f8%I%4`d=+Vzy5 z$ttFl_#znXp`@q?UDw(vt2=A~p0)B{kLRpg zvaH?KR7wUx-Z=Pqb8lWP7T`IyoRqq&dS1k1@U@_4w7>%cqc|p5z)J_N4yVq9k!0Jc z;+xn3Mu7juml3+{SjH)nt<^e^8*KYhV=% zsVRA!gDfg~W+my^+Nqshl%8&y6MoE;BN_G>)kdnK+6(A{o9|U1w%S=8VBixFupV*j zHSVlG&no1Ttg{fBpPen}_vA!w{Mks&4H2<}A7Je)IYWPc|87#vTx6Y`?7QNxUxVh% zmJfX$m*4gXco?qd=(L69n$+YvKo15Vb$tDbK96$AsLgqo{LD>@Gb!IQN4`P30dSU`^>0Aw zL^wRMmf@yM-q@IFae{LgRtow!>PjNMq$j@QMRa6j&$?GxO$|8FuRqThBUp84cGMot zz{|xontmoXIk_UA|Kd74XJ@(JJFCG(hcf?&TuT^ol-Jidk!sv|vjD@B7G-lzxUezd zK~_!WSH_E#|b@p{^VLDKukG@8$rg@t+f`+W}g+?+ud3j(IT z&%e3+ML7moZZ9b@5xv_zZ%#fE0Re6b(8u9OUzY0|^HvndToeu@(}^|5p~}kMb**6+ zwAm)uO-zhWJfJ@&0rccvbW1SZ_SDno3zP$SzaFLp>!wwckT1;*krRf6+07#4VkSUd zArG{O%529kfMi`jcdhVi>+n{hc}vJcT}FG(ZB}~a+7)X@Fkq86ZBNUT7xU>Cod!pjxb$!$^5vXTi^wAmE@g*(unwT!Pk=G-V?ZfvfqA|Rihdu7RJ)00-8yw`XMVn`ifF{*hi(%ke$B=ujzcvR_GH zpiyY=t;j#rGGib&O|&z%W8yPo0NkxV{U7A+7I^<%XQwI#1LnDji`9*8B_S&KW4&ieNox>`*R#AhX*8+gowxEsp_%eSYI9UaCap)Ri~l zM|2}?5OoD$8l{}7eR@L$1x`9V`N4xR!UaT?@2XUy7&{Cu$G}%X+n5uV@ac? zM_^8oHSRa6xyk(T`|MKT%-{!(KD+5Z9HD=spng`m&)~UE?Kat%k+$EYNZ$Mx;7>Vi ziK!!6e(Ml)Lw!%SPO0(K@l|b@P*3xDDkU?E{~2rjx&gNM4bs>kqBuP}lerp6!upz=)m|1Tv20&aCjf#KbF=HslH}XHu+L_jQS(b6rzw8=lh$ zS7S?*#0fETZ?#`^1=1e8aG%qeklI&i{u>598M$hPxR2jtd3>_|2uR+5u$p%#DIs=f z;iVZ22r4vFC!p&z#|v7cSK$HETwXEb{uF8KU1YMlXd&yJptzw0Aa>p>qjN(gEi4c0Pf8d zEW@FlBV@>8`Ope>Q>uvZgwhId)9JHHnhSQdAyW*3WMyOh<=sSVx7Zl`ad^@HbMi7M zbSdwcNa913mm9+5pnkJI=vYM@vgIb*OaO1aQ1@FM+6MpOPsQz=e#~4Nlj#O20EswzD17uE34tptWvCL~6ii4_jNAw!k!szgN<)sD=Vrpo+QYcv|O z9VBC5h$8$}p97aMHvs+vegTen^k|`S5^I>z&zX(+yXdfqA>XratPc2xRi!ci{tCnY z28%54W#_dQ6vIsb=yPW9Ar(WSp~+(@gCh-|fh!v7O>B$)FP2FDhYf&SmHd+$s5H@d zX+Gnzsjbn!8e#=Y1}Z3$qP>Wh@mR#o+P`*x*u#%tidzPXy5mWFC< z1noEgThF#Xefnju%B1mzclqz)$A6WIn{(kk(Ts}}D>IG!7fS8D9l%3KF_uFwP)?@L z=R=UsFOi#|(65dbii*wmVBxK+d$+{I#a;H-Wjo~zOD*$?ij*}Hy9`nL@&^k2n_h9( zg)C2wy`s0bx8o>W%adyx&P(sWaW|5X&!5v2#s{`x@vcoRl!d;W!lSp7~*5DUp6{{{4YnXG0*P{tFH2!crb+*xS!j*E8tx&W8KCZIGz!a=I_yra*h9 z=z!go5ca<40T@gJ@JI;2F}}NY6{Fj^!NG^D;%@2LI*UAH3^`Y@b+?Lv{hPs)C(p0^oGVJr zTk(?gJ9xd3CNQO7Wo%qR-7r-g7ACH{c2A$?d#}4-I6*HL$EJu{>Q1d50Zmqb-*m+y zO&k^kIyyUD0Bve%qR8Hq2~rm@i38}l`}=4-nYGH4aC6`(kzmwJbpfo#KK5gJ=d$ zA=1bZh>Pp%7;bLs`cnz{w!`2VEQ*$vmg&ls(C#o|YcvnM4tFd;5%H&ttRs8B8Lc=!gWs~-ug%bA2^()wD5MF1H00GVpVoH+CG1@GQH z;ZK)0=}8s9zBu${=HT#n_3oqHFxqX|mfxb$v^|PmY~|PDBHOIh zYR>XV3Bq$)PIbn)vaq(Ppm2X_wcebtl-+Vl7GWbF`RdiuPGQ&R;9yx<7=?+0<6c^i zy@zH~z=pn=S;n;WXgxsG-PC`Tj9yb~o7{tQAo@L|^1|$%E zmJy@|U?k@XU4o;~{uTsm+t`BCdOv>sZTwVW^_)v(p>KarPuj$qv90Zkl^_13qcI^3 z1K=}&z)r-5K&XmQY1U+IZ*LDeY4*&J2)A#XKurxz0ViF04Hd8Yvu_@-QDUN_J@bgI zaa5_lABDCb5#&k+gCGg<@#X0LezQbNfwZ46S1uEk?(u_3%czS4b*DewM7 zeSUd1D{#Ymat#N7P7Rg<^sJ)l0FgKM(tv;=H(W>kHDIylCX%&=^B%nfo0<7{hkPO5`XgbD;Z`VF^qD$Be?8|*Gi1P8ik+eG$6sySKbV-A z+3W#D)DH-#9U%8+Nq3dzn&-B!Rjl=AugK!1kyyYkOUpQ-M;XtZ6D?6(L^mTVDn!wv zZY#+bV*}1Ga#SP)tb>7b!oVq2FgU13)uyBYd7FlYh6RLdzh+wqxx58F?Xvva-hce~ z(Htv0IW@Oe19(DXyv4OOQXVZ@B(4#r1UnQ z|7Sq$+-Rxl-+5}yQ-)C)!%4Zndv-plK_W685ff^Ee|a5rVf67SPuTD7AfSPSkBEhxh`giz`e|qtp-jjFjKu9TuG5Xy=w6E#tUgnYbOoZikS`ETnHCxGI zLJZ)Sr%NGMsbqc{fBn}yp@RPJ=AmqX|A3~U|4BR^*5R>HIMaH&YTRu>d-JNcwl)Ip z|9uWF;<^19Ed%k=-OWbs**gUl6%_nr`t!Qe)F1MrV}n4-d)mk(HABK*OWQx?PN8jI zN6VGF0HxWtk3O%RxQS=IAV;OgOw5?)%GTcx(J>(>Doi&0@y8h;ukE)CHNFc-!YgX? zY~aRV3Sl$_y4g3BLYPc}4x+>E=9W}!FJ1)zN2XzIR#ZqSNg+lVLUQ6wOyh5pUeO+Ln7XidMV{#{3f1yA?OG9V0PXeju}rsfq43p!CIBukQw|R~ z;wfg}7@LI-XXU^lUwS5BY!dE-n(4OlI+!%tvy2if{@?xiQkPeX(|8^{P*3z*Jz#0v z>zwFqLT`=Qgq8kGG@vY%wuyRz_^BJer+vNrEXQ%7aNwGVCzi^Y&N#~U$yuPMCM003 zhcq$b8oEc|kGJQ1$nxI{{2)WBx@1MIZmNjiVj9;iZQ?wiQ@;DN$8)J!W3IO;tWr*X zFVZ{oBxs#ZTPplcVanQAMgwlCG%l;w`8Ye{?Q5^hz#Z2flz9;!iw$BC*JhjD6Q7fj zDH;|VY~nXG`hD7<|9fa0yBu&ht-L1R+M9OWdzKz-=@xrT8M>dyWujdH2frdz9BM!+!%o-G;>9$9+v-#a}AwMKx5ws-N zfmT|8u`5^$q@lZ2&XB4J27^hD2{mmGiXtG5pS?0vGCb=V zs@k}>DxKHZ*qAEfgcJ!LmXsyo!XFR@9aV$(hUTaw)$6h|M0-#B@8(k3rlj<;DuF|O zp29mK`zOaWM310V-hjM(Sa+-I#?&%W#9y~;-WMo&nY!bvtnNlJiuG&o<2SYoK($9Sa8FeBpu7oeZJj?5 z&yi%VS{hx%fF++y!FvQUP@Pe#R&}-+t$}la=%a2vCMKpLAft3$Uv(y}d9Dv7@7!OB zpYRw$_oVR(h^w+>%j@r>NN=`Cl<*DFJ$j(aW5W`yK2nlh3Pk69OdA$yh^6H1CNQnHhsVmT77ry{m)6vpwicIo_bqMr7NX?f)K+Y2hnlIWkA+?{0y zI|Q4d;){!ZkgvhR2SQcS?4U$wMZ0*;(|l0Coi4{`9zl4$oL)h&kr7%K0-Sv=4eC}X#0U-|FO+Wx96y-Tq zMCK&D?C#+U#k1v#gNyB%b9C0krvtq|4zW%PuR%AZXk+(70h8UAE;nA^)=O;OFMquM z39xWHQat*%^ZOoQLj@|y{gxXUi`Gt3nkVnY{{osV1b{=10!YjRC>_}IJ+p^9rac=M zmLcU{H6)lQ>bBH?-^5Crj|-D$-}YQ1?K-R2qm8E;G(FwJOsH0HaPZH$FsDfqG9&xw zr{rgL3$OO;b$V+s+p!sj<#(`-^dFWX6Nzr}I9TrN}g! zH1jX^=yFwk%AFKYP@arOoi%ihUt{=9wq*;6WYM`hd6RSFxJn1DL-?k|)1pxc2bcS)XX(RjrI(z5i zRjIcL35I~>B&mQ>fLNgBDV&#|&#>*AdNJ31UW84N&H_uoSfEYXc|f7V5{oe5VTvy2 z#{gyM+`*WgO;87*{hy-FictZpy`uf|7>Ox^r%yi)IjG&alkL8;F7Kr$q)!3s8}Zkb zS_8Z3!|1@vibgGe6&8|p60t42jRR7Rh&FcgYFX|f$GajH>#)hP3H@oQcEuE!O&%cY zwjA4y7S*W%q|_KRm5I7-8pGda+iOlfeHN!olSGyT0dGE%=<1Qk)l&U5CzsT- z(c9$b0(geb>R!*b9i%0a^=^HL3=!W*nNBR4gzS|!7Yr|=ON}0^A-(O?2s!kG-To;5 z#Ah>DoQ>lia=U00$0-6$Tr)d$@7 zxgWIU4F*ky*Z{dRRTZ*%|Ln~W!<=(o==fsJCy08e%2Pm6>0X0-N_YXSbjwpsp1750 zW9rq!z(6tWMcRIb%g(nVkn@Q2+iYNS_u@G3)|IwSsx-Y|4}dNrR62uozFn_OFAI}9 z1yx_U1wGI&(d_AFVqqx+^c_IGAS1197p5E$Hq#$8+D8na4yS64ZGTduly9%SI+WGE z&jpkh)%2F;tk+Zh9)wF&R5@{XS03lwx%=ImGKlbq19d7BMOggHh|p4Fz9z`PkTl-S z0lBz0TAr6Tw_LnNw6e5}ox&d><8!_ZQ6tW#jU$wfyp~P$=x17>Oa4ZLIAG5K743W+ zv*)*09Mv%0+ebX3k{9Pv zb9MWFpiv`os~W&Qrh!vC992w;4Ps;;oIdyC1lq;Od}9_2RIGvXsP|^e8;F54M@q9x zdu7QJR_7`UNx)$=@B_=oX>;<_O;j=lgcfXm(4*c(Pa?nk>}N@Ahf`la*t}axqJ{KV zz%xZ}x2Ja>lA^R1tU#msrk*V`>H9W;#&?f_kW@}KNZIgyc+UeuSs!qs0oy&gS(D#c zW(u*yh>_MdIQQXgqygRQM3G^&ZmUJ?`&BzcS%tA`&r+O3*+F5Qcw70G7q&2XYbA@2 z1=ZAh?^E#A^3C$}XI#P(l42ctNMGp|yqZT;_jhF9y9K*(6)+b*V_7D_#%H06lotybsXOuk zchqqFHZG%EXWK`jsHTE*G9+m$QbGCE$l{LOwW}CZyI?GEntjGzf{w0q{zzD$v?PFo zI6F4Owy;BvgEo}o@DrGfM}kn|#LXLaYTuvMl`rvKN&{Mw^_j0-bDhe*p=w4@>7|CE zQY<#bZL%$$*Hc=(7Qm5FQGSwDKlCZU+2RBA3;uFoXm&u(1n4dy5o5qBGrq}LHiI4t zYWu*u6M!7<1O0uH_3G$U33MH;l0~&{r79{Zi8^9O4QqpM5#F6C;UdRk(F-?aT`3hou7`(otTIk{ z>`-=ILDXPGg_7nnjH|9|a@n`-cm^=YxObmv#egh>;n~gY;Ha@4X-vTIt4niQ@0LgI z@p1j?xja>nXfTH_<+|_Exx0|JP8psB^Th@_@*Z?$K#a_d^RqlHXRlTOsSbWtY;QYC zYD3)_|6!Ct4PhY-`0C-R3$Q#X|Lr=aUrxtTsCZI;?{vuvNU2d)+wMV^ui!zb_xo%n z&+Lh8Gin3y+K!pC?A&Z(q;D+oQg1V5j1=b7avWiPJpCst>(8ABop8CA$1^~@wRH$x z`nY_T`SN&Z{M37s4Zn41k0uq`v%3;|Mm;S{H@U6D=9#OcSKUj%Fy{eeu*M>ToEomu zv;+#{00Z;|g!_VgadPtuED!ENi!`-*YP?6pcLZ{r&UXb+GoxXdjC#%-?fZy>HpHH54R>beG*BZNT47KY{2 zfvtH7YIs?w&1dM))5BL|=U76`JL~JybtZO?g*Ns^1Fye0+3Pij?Svj2H^ses@4xvv z(`|SbXsu7(`;i-B1SSatEF?7BWLkQUD>5@NE$6szkS!Offxk<`Ss>i#WFoaPG!2Mg zEb3ZNWA#pzVNNL?t=^FVXjW%ooYkZ-rim!J3fXA20)n>d)n@^JHmb7Y7#Or>a&mb7 z<~NXSEE4Gv9DfWJVU_FuQQkh@6&sRt@0nk45k8dhS9Y)1N?3M7U3;s=6v7R#ks%}) z0S{fBB@7NJzM6uJNyp@W_+SbI_@jU!r^X`GO^3?y0iCRkhn>uTN#&*2)mqzSr!EI0 zKLxalvb=j z`CE_f%>Vl3(o@S=^K+%6QwklX6FOabmYI#MvKa4cV2_{+DNniLw0 zUVFbQmOa&s`^p1SMmEBfra)WLe%kN({#UP{eY!C`I!Z|8m*VrE z2l?A`hWh%=hXmSl`c=Y!8h)?Jrtd$%O#iz~bCa~=ysx-YI9x*vE#T|T2CQ`!SZmqE zCN-Jq&*6~wol$2}l9|$D23-Cr;2&sy{HGlK4-of+`TxKha-)1o=spJ#mfvOAkT;$N zC4`iG(1ULtfA_EKUYRa>jN?=CpPte4y-dZ~^3o3O*@OOu#=cDDQ7i3Y7zAp+3=TKR z{SD4#i%|is7pp65?1)c!w{{k+%W+YEzp2;i(c<&ZrjDQg->9)4%L9(XYl&6m8kKep M6&+=y;*%Hu2`J^&Z~y=R literal 0 HcmV?d00001 From c3fd524bbccf5c1192867434c1ebd61750c324b4 Mon Sep 17 00:00:00 2001 From: Vignesh Date: Mon, 10 Nov 2025 04:04:02 -0800 Subject: [PATCH 051/402] fix: Message wrapping in narrow editor panes (#8121) --- webviews/components/merge.tsx | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/webviews/components/merge.tsx b/webviews/components/merge.tsx index 7a6aee3d7b..95851207f4 100644 --- a/webviews/components/merge.tsx +++ b/webviews/components/merge.tsx @@ -244,18 +244,20 @@ export const MergeStatus = ({ mergeable, isSimple, canUpdateBranch }: { mergeabl } } return ( -

        - {icon} -

        - {summary} -

        - {(action && canUpdateBranch) ? -
        - -
        - : null} +
        +
        + {icon} +

        + {summary} +

        + {(action && canUpdateBranch) ? +
        + +
        + : null} +
        ); }; From a745d1f50aec4dbc1faaacc82ebf053ec2e1c5b5 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Mon, 10 Nov 2025 17:02:12 +0100 Subject: [PATCH 052/402] Comments don't show when PR is on non-default repo (#8126) Fixes #8050 --- src/github/githubRepository.ts | 5 ++- .../pullRequestCommentControllerRegistry.ts | 32 +++++++++++++++---- 2 files changed, 28 insertions(+), 9 deletions(-) diff --git a/src/github/githubRepository.ts b/src/github/githubRepository.ts index fad9265b2b..a08bd61e93 100644 --- a/src/github/githubRepository.ts +++ b/src/github/githubRepository.ts @@ -227,13 +227,12 @@ export class GitHubRepository extends Disposable { public async ensureCommentsController(): Promise { try { + await this.ensure(); if (this.commentsController) { return; } - - await this.ensure(); this.commentsController = vscode.comments.createCommentController( - `${PullRequestCommentController.PREFIX}-${this.remote.gitProtocol.normalizeUri()?.authority}-${this.remote.owner}-${this.remote.repositoryName}`, + `${PullRequestCommentController.PREFIX}-${this.remote.gitProtocol.normalizeUri()?.authority}-${this.remote.remoteName}-${this.remote.owner}-${this.remote.repositoryName}`, `Pull Request (${this.remote.owner}/${this.remote.repositoryName})`, ); this.commentsHandler = new PRCommentControllerRegistry(this.commentsController, this.telemetry); diff --git a/src/view/pullRequestCommentControllerRegistry.ts b/src/view/pullRequestCommentControllerRegistry.ts index a8a9964362..b21c5f7145 100644 --- a/src/view/pullRequestCommentControllerRegistry.ts +++ b/src/view/pullRequestCommentControllerRegistry.ts @@ -20,9 +20,15 @@ interface PullRequestCommentHandlerInfo { dispose: () => void; } +interface PRCommentingRangeProviderInfo { + provider: vscode.CommentingRangeProvider2; + refCount: number; + dispose: () => void; +} + export class PRCommentControllerRegistry extends Disposable implements vscode.CommentingRangeProvider, CommentReactionHandler { private _prCommentHandlers: { [key: number]: PullRequestCommentHandlerInfo } = {}; - private _prCommentingRangeProviders: { [key: number]: vscode.CommentingRangeProvider2 } = {}; + private _prCommentingRangeProviders: { [key: number]: PRCommentingRangeProviderInfo } = {}; private readonly _activeChangeListeners: Map = new Map(); public readonly resourceHints = { schemes: [Schemes.Pr] }; @@ -40,8 +46,8 @@ export class PRCommentControllerRegistry extends Disposable implements vscode.Co return; } - const provideCommentingRanges = this._prCommentingRangeProviders[params.prNumber].provideCommentingRanges.bind( - this._prCommentingRangeProviders[params.prNumber], + const provideCommentingRanges = this._prCommentingRangeProviders[params.prNumber].provider.provideCommentingRanges.bind( + this._prCommentingRangeProviders[params.prNumber].provider, ); return provideCommentingRanges(document, token); @@ -108,13 +114,27 @@ export class PRCommentControllerRegistry extends Disposable implements vscode.Co } public registerCommentingRangeProvider(prNumber: number, provider: vscode.CommentingRangeProvider2): vscode.Disposable { - this._prCommentingRangeProviders[prNumber] = provider; + if (this._prCommentingRangeProviders[prNumber]) { + this._prCommentingRangeProviders[prNumber].refCount += 1; + return this._prCommentingRangeProviders[prNumber]; + } - return { + this._prCommentingRangeProviders[prNumber] = { + provider, + refCount: 1, dispose: () => { - delete this._prCommentingRangeProviders[prNumber]; + if (!this._prCommentingRangeProviders[prNumber]) { + return; + } + + this._prCommentingRangeProviders[prNumber].refCount -= 1; + if (this._prCommentingRangeProviders[prNumber].refCount === 0) { + delete this._prCommentingRangeProviders[prNumber]; + } } }; + + return this._prCommentingRangeProviders[prNumber]; } override dispose() { From 6e1f6cca78e8c3c0165e6315d724d64f12383556 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Tue, 11 Nov 2025 10:53:56 +0100 Subject: [PATCH 053/402] Extend disposable (#8127) * Extend disposable * clean up --- src/view/pullRequestCommentControllerRegistry.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/view/pullRequestCommentControllerRegistry.ts b/src/view/pullRequestCommentControllerRegistry.ts index b21c5f7145..e4eeb6ad12 100644 --- a/src/view/pullRequestCommentControllerRegistry.ts +++ b/src/view/pullRequestCommentControllerRegistry.ts @@ -14,16 +14,14 @@ import { GHPRComment } from '../github/prComment'; import { PullRequestModel } from '../github/pullRequestModel'; import { CommentReactionHandler } from '../github/utils'; -interface PullRequestCommentHandlerInfo { +interface PullRequestCommentHandlerInfo extends vscode.Disposable { handler: PullRequestCommentController & CommentReactionHandler; refCount: number; - dispose: () => void; } -interface PRCommentingRangeProviderInfo { +interface PRCommentingRangeProviderInfo extends vscode.Disposable { provider: vscode.CommentingRangeProvider2; refCount: number; - dispose: () => void; } export class PRCommentControllerRegistry extends Disposable implements vscode.CommentingRangeProvider, CommentReactionHandler { From 13ee5290bb53a23272db88aef6bc951266b3d04d Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:12:37 +0000 Subject: [PATCH 054/402] Fix: Enter review mode for explicitly switched PRs regardless of state (#8130) * Initial plan * Add _switchedToPullRequest to track explicitly switched PRs Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/view/reviewManager.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/view/reviewManager.ts b/src/view/reviewManager.ts index 6260047828..b9c31fac14 100644 --- a/src/view/reviewManager.ts +++ b/src/view/reviewManager.ts @@ -75,6 +75,11 @@ export class ReviewManager extends Disposable { * explicit user action from something like reloading on an existing PR branch. */ private justSwitchedToReviewMode: boolean = false; + /** + * The last pull request the user explicitly switched to via the switch method. + * Used to enter review mode for this PR regardless of its state (open/closed/merged). + */ + private _switchedToPullRequest?: PullRequestModel; public get switchingToReviewMode(): boolean { return this._switchingToReviewMode; @@ -481,13 +486,16 @@ export class ReviewManager extends Disposable { const useReviewConfiguration = getReviewMode(); - if (pr.isClosed && !useReviewConfiguration.closed) { + // If this is the PR the user explicitly switched to, always use review mode regardless of state + const isSwitchedToPullRequest = this._switchedToPullRequest?.number === pr.number; + + if (pr.isClosed && !useReviewConfiguration.closed && !isSwitchedToPullRequest) { Logger.appendLine('This PR is closed', this.id); await this.clear(true); return; } - if (pr.isMerged && !useReviewConfiguration.merged) { + if (pr.isMerged && !useReviewConfiguration.merged && !isSwitchedToPullRequest) { Logger.appendLine('This PR is merged', this.id); await this.clear(true); return; @@ -1090,6 +1098,7 @@ export class ReviewManager extends Disposable { this.statusBarItem.command = undefined; this.statusBarItem.show(); this.switchingToReviewMode = true; + this._switchedToPullRequest = pr; try { await vscode.window.withProgress({ location: vscode.ProgressLocation.Notification }, async (progress) => { @@ -1243,6 +1252,7 @@ export class ReviewManager extends Disposable { this._prNumber = undefined; this._folderRepoManager.activePullRequest = undefined; + this._switchedToPullRequest = undefined; if (this._statusBarItem) { this._statusBarItem.hide(); From eb6eec4e79b1a6226738598adc30ce6c7e477e0a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Tue, 11 Nov 2025 11:13:38 +0000 Subject: [PATCH 055/402] Add single-click merge button for Copilot draft PRs (#8108) * Initial plan * Add Ready & Merge button for Copilot PRs Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Improve error handling for Ready & Merge button Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * move some stuff * Add check all icon * Use icon button with checkAllIcon for Ready & Merge action Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Fix styling * Add loading spinner to Ready & Merge icon button Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Clean up --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- resources/icons/codicons/check-all.svg | 1 + src/github/pullRequestOverview.ts | 29 ++++++++++++++++++++ webviews/common/context.tsx | 2 ++ webviews/components/icon.tsx | 1 + webviews/components/merge.tsx | 38 ++++++++++++++++++++++---- webviews/editorWebview/index.css | 6 ++++ 6 files changed, 72 insertions(+), 5 deletions(-) create mode 100644 resources/icons/codicons/check-all.svg diff --git a/resources/icons/codicons/check-all.svg b/resources/icons/codicons/check-all.svg new file mode 100644 index 0000000000..3028a6c57e --- /dev/null +++ b/resources/icons/codicons/check-all.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/github/pullRequestOverview.ts b/src/github/pullRequestOverview.ts index de64cf068d..e7fb80cd68 100644 --- a/src/github/pullRequestOverview.ts +++ b/src/github/pullRequestOverview.ts @@ -364,6 +364,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel): Promise { + try { + const readyResult = await this._item.setReadyForReview(); + + try { + await this._item.approve(this._folderRepositoryManager.repository, ''); + } catch (e) { + vscode.window.showErrorMessage(`Pull request marked as ready for review, but failed to approve. ${formatError(e)}`); + this._replyMessage(message, readyResult); + return; + } + + try { + await this._item.enableAutoMerge(message.args.mergeMethod); + } catch (e) { + vscode.window.showErrorMessage(`Pull request marked as ready and approved, but failed to enable auto-merge. ${formatError(e)}`); + this._replyMessage(message, readyResult); + return; + } + + this._replyMessage(message, readyResult); + } catch (e) { + vscode.window.showErrorMessage(`Unable to mark pull request as ready for review. ${formatError(e)}`); + this._throwError(message, ''); + } + } + private async checkoutDefaultBranch(message: IRequestMessage): Promise { try { const prBranch = this._folderRepositoryManager.repository.state.HEAD?.name; diff --git a/webviews/common/context.tsx b/webviews/common/context.tsx index df94d45c06..dd00c8076f 100644 --- a/webviews/common/context.tsx +++ b/webviews/common/context.tsx @@ -89,6 +89,8 @@ export class PRContext { public readyForReview = (): Promise => this.postMessage({ command: 'pr.readyForReview' }); + public readyForReviewAndMerge = (args: { mergeMethod: MergeMethod }): Promise => this.postMessage({ command: 'pr.readyForReviewAndMerge', args }); + public addReviewers = () => this.postMessage({ command: 'pr.change-reviewers' }); public changeProjects = (): Promise => this.postMessage({ command: 'pr.change-projects' }); public removeProject = (project: IProjectItem) => this.postMessage({ command: 'pr.remove-project', args: project }); diff --git a/webviews/components/icon.tsx b/webviews/components/icon.tsx index 45f2bb938e..67c7ee53aa 100644 --- a/webviews/components/icon.tsx +++ b/webviews/components/icon.tsx @@ -15,6 +15,7 @@ export default Icon; export const accountIcon = ; export const addIcon = ; export const checkIcon = ; +export const checkAllIcon = ; export const chevronDownIcon = ; export const circleFilledIcon = ; export const closeIcon = ; diff --git a/webviews/components/merge.tsx b/webviews/components/merge.tsx index 95851207f4..46d5d758cf 100644 --- a/webviews/components/merge.tsx +++ b/webviews/components/merge.tsx @@ -14,7 +14,7 @@ import React, { } from 'react'; import { AutoMerge, QueuedToMerge } from './automergeSelect'; import { Dropdown } from './dropdown'; -import { checkIcon, circleFilledIcon, closeIcon, gitMergeIcon, requestChangesIcon, skipIcon, warningIcon } from './icon'; +import { checkAllIcon, checkIcon, circleFilledIcon, closeIcon, gitMergeIcon, loadingIcon, requestChangesIcon, skipIcon, warningIcon } from './icon'; import { nbsp } from './space'; import { Avatar } from './user'; import { EventType, ReviewEvent } from '../../src/common/timelineEvent'; @@ -283,9 +283,10 @@ export const OfferToUpdate = ({ mergeable, isSimple, isCurrentlyCheckedOut, canU }; -export const ReadyForReview = ({ isSimple }: { isSimple: boolean }) => { +export const ReadyForReview = ({ isSimple, isCopilotOnMyBehalf, mergeMethod }: { isSimple: boolean; isCopilotOnMyBehalf?: boolean; mergeMethod: MergeMethod }) => { const [isBusy, setBusy] = useState(false); - const { readyForReview, updatePR } = useContext(PullRequestContext); + const [isMergeBusy, setMergeBusy] = useState(false); + const { readyForReview, readyForReviewAndMerge, updatePR } = useContext(PullRequestContext); const markReadyForReview = useCallback(async () => { try { @@ -297,6 +298,18 @@ export const ReadyForReview = ({ isSimple }: { isSimple: boolean }) => { } }, [setBusy, readyForReview, updatePR]); + const markReadyAndMerge = useCallback(async () => { + try { + setBusy(true); + setMergeBusy(true); + const result = await readyForReviewAndMerge({ mergeMethod: mergeMethod }); + updatePR(result); + } finally { + setBusy(false); + setMergeBusy(false); + } + }, [readyForReviewAndMerge, updatePR, mergeMethod]); + return (
        @@ -307,6 +320,17 @@ export const ReadyForReview = ({ isSimple }: { isSimple: boolean }) => {
        + {isCopilotOnMyBehalf && ( + + )}
        @@ -340,10 +364,14 @@ export const Merge = (pr: PullRequest) => { }; export const PrActions = ({ pr, isSimple }: { pr: PullRequest; isSimple: boolean }) => { - const { hasWritePermission, canEdit, isDraft, mergeable } = pr; + const { hasWritePermission, canEdit, isDraft, mergeable, isCopilotOnMyBehalf, defaultMergeMethod } = pr; if (isDraft) { // Only PR author and users with push rights can mark draft as ready for review - return canEdit ? : null; + if (!canEdit) { + return null; + } + + return ; } if (mergeable === PullRequestMergeability.Mergeable && hasWritePermission && !pr.mergeQueueEntry) { diff --git a/webviews/editorWebview/index.css b/webviews/editorWebview/index.css index f73c8c39c3..4716e0152e 100644 --- a/webviews/editorWebview/index.css +++ b/webviews/editorWebview/index.css @@ -314,6 +314,12 @@ button.input-box { align-items: center; } +.ready-for-review-container .button-container { + flex-direction: row; + display: flex; + align-items: center; +} + .ready-for-review-icon { width: 16px; height: 16px; From 49ec1f3089447bcd7b23d5ff94c082f22de4cc4a Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Wed, 12 Nov 2025 09:53:19 +0100 Subject: [PATCH 056/402] Update to macos 15 for pipeline (#8136) --- azure-pipeline.pr.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipeline.pr.yml b/azure-pipeline.pr.yml index 3f86e93a24..029cfe80f3 100644 --- a/azure-pipeline.pr.yml +++ b/azure-pipeline.pr.yml @@ -2,7 +2,7 @@ jobs: - job: test_suite displayName: Test suite pool: - vmImage: 'macos-13' + vmImage: 'macos-15' steps: - template: scripts/ci/common-setup.yml From 047345212f1eb9f5b7473cac2fbd75d2814885f0 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Wed, 12 Nov 2025 09:01:10 +0000 Subject: [PATCH 057/402] Adopt codicon tree item label for current issue in issues view (#8134) * Initial plan * Adopt codicon tree item label for current issue in issues view Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Add escaping for special characters in issue titles Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/issues/issuesView.ts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/issues/issuesView.ts b/src/issues/issuesView.ts index b33079ac12..d839c2d6e8 100644 --- a/src/issues/issuesView.ts +++ b/src/issues/issuesView.ts @@ -117,7 +117,12 @@ export class IssuesTreeData }; if (this.stateManager.currentIssue(element.uri)?.issue.number === element.number) { - treeItem.label = `✓ ${treeItem.label as string}`; + // Escape any $(...) syntax to avoid rendering issue titles as icons. + const escapedTitle = element.title.replace(/\$\([a-zA-Z0-9~-]+\)/g, '\\$&'); + const label: vscode.TreeItemLabel2 = { + label: new vscode.MarkdownString(`$(check) ${escapedTitle}`, true) + }; + treeItem.label = label as vscode.TreeItemLabel; treeItem.contextValue = 'currentissue'; } else { const savedState = this.stateManager.getSavedIssueState(element.number); From 0e0f6737ae4bfb77e63170f0f06ce83f77d218b1 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Wed, 12 Nov 2025 17:42:34 +0100 Subject: [PATCH 058/402] 0.122.0 (#8141) --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 442a0f740b..422f445df9 100644 --- a/package.json +++ b/package.json @@ -38,7 +38,7 @@ "treeItemMarkdownLabel", "treeViewMarkdownMessage" ], - "version": "0.120.0", + "version": "0.122.0", "publisher": "GitHub", "engines": { "vscode": "^1.106.0" From 8a8a4dbf70aaa1ab7d6271d05eaed24315be23ad Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 13:41:12 +0000 Subject: [PATCH 059/402] Make Comment the primary action in review dropdown (#8139) * Initial plan * Make Comment the primary action in review dropdown with proper ordering and styling Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * clean up --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- package.json | 26 +++++++++++++++---------- webviews/components/comment.tsx | 14 +++++++------ webviews/components/contextDropdown.tsx | 7 +++++-- 3 files changed, 29 insertions(+), 18 deletions(-) diff --git a/package.json b/package.json index 422f445df9..7ee791b5b8 100644 --- a/package.json +++ b/package.json @@ -3480,16 +3480,19 @@ "group": "1_create@2" }, { - "command": "review.approve", - "when": "webviewId == 'github:activePullRequest' && github:reviewCommentMenu && github:reviewCommentApprove" + "command": "review.comment", + "when": "webviewId == 'github:activePullRequest' && github:reviewCommentMenu && github:reviewCommentComment", + "group": "1_review@1" }, { - "command": "review.comment", - "when": "webviewId == 'github:activePullRequest' && github:reviewCommentMenu && github:reviewCommentComment" + "command": "review.approve", + "when": "webviewId == 'github:activePullRequest' && github:reviewCommentMenu && github:reviewCommentApprove", + "group": "1_review@2" }, { "command": "review.requestChanges", - "when": "webviewId == 'github:activePullRequest' && github:reviewCommentMenu && github:reviewCommentRequestChanges" + "when": "webviewId == 'github:activePullRequest' && github:reviewCommentMenu && github:reviewCommentRequestChanges", + "group": "1_review@3" }, { "command": "review.approveOnDotCom", @@ -3500,16 +3503,19 @@ "when": "webviewId == 'github:activePullRequest' && github:reviewCommentMenu && github:reviewCommentRequestChangesOnDotCom" }, { - "command": "review.approveDescription", - "when": "webviewId == PullRequestOverview && github:reviewCommentMenu && github:reviewCommentApprove" + "command": "review.commentDescription", + "when": "webviewId == PullRequestOverview && github:reviewCommentMenu && github:reviewCommentComment", + "group": "1_review@1" }, { - "command": "review.commentDescription", - "when": "webviewId == PullRequestOverview && github:reviewCommentMenu && github:reviewCommentComment" + "command": "review.approveDescription", + "when": "webviewId == PullRequestOverview && github:reviewCommentMenu && github:reviewCommentApprove", + "group": "1_review@2" }, { "command": "review.requestChangesDescription", - "when": "webviewId == PullRequestOverview && github:reviewCommentMenu && github:reviewCommentRequestChanges" + "when": "webviewId == PullRequestOverview && github:reviewCommentMenu && github:reviewCommentRequestChanges", + "group": "1_review@3" }, { "command": "review.approveOnDotComDescription", diff --git a/webviews/components/comment.tsx b/webviews/components/comment.tsx index c83c74bc31..bfd5e63286 100644 --- a/webviews/components/comment.tsx +++ b/webviews/components/comment.tsx @@ -460,12 +460,12 @@ export function AddComment({ defaultOptionValue={() => currentSelection} allOptions={() => { const actions: { label: string; value: string; optionDisabled: boolean; action: (event: React.MouseEvent) => void }[] = []; - if (availableActions.approve) { - actions.push({ label: availableActions[ReviewType.Approve]!, value: ReviewType.Approve, action: () => submitAction(ReviewType.Approve), optionDisabled: shouldDisableApproveButton }); - } if (availableActions.comment) { actions.push({ label: availableActions[ReviewType.Comment]!, value: ReviewType.Comment, action: () => submitAction(ReviewType.Comment), optionDisabled: shouldDisableNonApproveButtons }); } + if (availableActions.approve) { + actions.push({ label: availableActions[ReviewType.Approve]!, value: ReviewType.Approve, action: () => submitAction(ReviewType.Approve), optionDisabled: shouldDisableApproveButton }); + } if (availableActions.requestChanges) { actions.push({ label: availableActions[ReviewType.RequestChanges]!, value: ReviewType.RequestChanges, action: () => submitAction(ReviewType.RequestChanges), optionDisabled: shouldDisableNonApproveButtons }); } @@ -475,6 +475,7 @@ export function AddComment({ disabled={isBusy || busy} hasSingleAction={Object.keys(availableActions).length === 1} spreadable={true} + primaryOptionValue={ReviewType.Comment} />
      @@ -608,12 +609,12 @@ export const AddCommentSimple = (pr: PullRequest) => { defaultOptionValue={() => currentSelection} allOptions={() => { const actions: { label: string; value: string; optionDisabled: boolean; action: (event: React.MouseEvent) => void }[] = []; - if (availableActions.approve) { - actions.push({ label: availableActions[ReviewType.Approve]!, value: ReviewType.Approve, action: () => submitAction(ReviewType.Approve), optionDisabled: shouldDisableApproveButton }); - } if (availableActions.comment) { actions.push({ label: availableActions[ReviewType.Comment]!, value: ReviewType.Comment, action: () => submitAction(ReviewType.Comment), optionDisabled: shouldDisableNonApproveButtons }); } + if (availableActions.approve) { + actions.push({ label: availableActions[ReviewType.Approve]!, value: ReviewType.Approve, action: () => submitAction(ReviewType.Approve), optionDisabled: shouldDisableApproveButton }); + } if (availableActions.requestChanges) { actions.push({ label: availableActions[ReviewType.RequestChanges]!, value: ReviewType.RequestChanges, action: () => submitAction(ReviewType.RequestChanges), optionDisabled: shouldDisableNonApproveButtons }); } @@ -623,6 +624,7 @@ export const AddCommentSimple = (pr: PullRequest) => { disabled={isBusy || pr.busy} hasSingleAction={Object.keys(availableActions).length === 1} spreadable={true} + primaryOptionValue={ReviewType.Comment} />
    diff --git a/webviews/components/contextDropdown.tsx b/webviews/components/contextDropdown.tsx index 9863dbb203..907777903d 100644 --- a/webviews/components/contextDropdown.tsx +++ b/webviews/components/contextDropdown.tsx @@ -17,6 +17,7 @@ interface ContextDropdownProps { hasSingleAction?: boolean; spreadable: boolean; isSecondary?: boolean; + primaryOptionValue?: string; } function useWindowSize() { @@ -32,7 +33,7 @@ function useWindowSize() { return size; } -export const ContextDropdown = ({ optionsContext, defaultOptionLabel, defaultOptionValue, defaultAction, allOptions: options, optionsTitle, disabled, hasSingleAction, spreadable, isSecondary }: ContextDropdownProps) => { +export const ContextDropdown = ({ optionsContext, defaultOptionLabel, defaultOptionValue, defaultAction, allOptions: options, optionsTitle, disabled, hasSingleAction, spreadable, isSecondary, primaryOptionValue }: ContextDropdownProps) => { const [expanded, setExpanded] = useState(false); const onHideAction = (e: MouseEvent | KeyboardEvent) => { if (e.target instanceof HTMLElement && e.target.classList.contains('split-right')) { @@ -56,7 +57,9 @@ export const ContextDropdown = ({ optionsContext, defaultOptionLabel, defaultOpt return
    {divRef.current && spreadable && (divRef.current.clientWidth > 375) && options && !hasSingleAction ? options().map(({ label, value, action, optionDisabled }) => { - return ; + // Only the primary option should use the primary (blue) button style when expanded + const isPrimary = primaryOptionValue && value === primaryOptionValue; + return ; }) :
    From 889edb3c49319f639fe590d918b210d62592e3cc Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:11:23 +0000 Subject: [PATCH 060/402] Show commit count as decoration on Commits tree node (#7959) * Initial plan * Add commit count decoration to Commits tree node Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Use file decoration instead of description for commit count Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Address review feedback: add owner/repo, use scheme constant, listen for timeline changes Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Remove commit count from URI, fetch from PR model instead Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Clean up --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/common/uri.ts | 33 +++++++++++- src/extension.ts | 3 +- src/view/commitsDecorationProvider.ts | 61 +++++++++++++++++++++++ src/view/treeDecorationProviders.ts | 2 +- src/view/treeNodes/commitsCategoryNode.ts | 5 ++ 5 files changed, 101 insertions(+), 3 deletions(-) create mode 100644 src/view/commitsDecorationProvider.ts diff --git a/src/common/uri.ts b/src/common/uri.ts index 7258baa2c3..6d2f031560 100644 --- a/src/common/uri.ts +++ b/src/common/uri.ts @@ -520,6 +520,36 @@ export function createPRNodeUri( }); } +export interface CommitsNodeUriParams { + owner: string; + repo: string; + prNumber: number; +} + +export function createCommitsNodeUri(owner: string, repo: string, prNumber: number): vscode.Uri { + const params: CommitsNodeUriParams = { + owner, + repo, + prNumber + }; + + return vscode.Uri.parse(`${Schemes.CommitsNode}:${owner}/${repo}/${prNumber}`).with({ + scheme: Schemes.CommitsNode, + query: JSON.stringify(params) + }); +} + +export function fromCommitsNodeUri(uri: vscode.Uri): CommitsNodeUriParams | undefined { + if (uri.scheme !== Schemes.CommitsNode) { + return undefined; + } + try { + return JSON.parse(uri.query) as CommitsNodeUriParams; + } catch (e) { + return undefined; + } +} + export interface NotificationUriParams { key: string; } @@ -750,7 +780,8 @@ export enum Schemes { Repo = 'repo', // New issue file for passing data Git = 'git', // File content from the git extension PRQuery = 'prquery', // PR query tree item - GitHubCommit = 'githubcommit' // file content from GitHub for a commit + GitHubCommit = 'githubcommit', // file content from GitHub for a commit + CommitsNode = 'commitsnode' // Commits tree node, for decorations } export function resolvePath(from: vscode.Uri, to: string) { diff --git a/src/extension.ts b/src/extension.ts index 78f8936cb0..d7ef5fbcdc 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -41,6 +41,7 @@ import { NotificationsProvider } from './notifications/notificationsProvider'; import { ThemeWatcher } from './themeWatcher'; import { resumePendingCheckout, UriHandler } from './uriHandler'; import { CommentDecorationProvider } from './view/commentDecorationProvider'; +import { CommitsDecorationProvider } from './view/commitsDecorationProvider'; import { CompareChanges } from './view/compareChangesTreeDataProvider'; import { CreatePullRequestHelper } from './view/createPullRequestHelper'; import { EmojiCompletionProvider } from './view/emojiCompletionProvider'; @@ -171,7 +172,7 @@ async function init( ); const treeDecorationProviders = new TreeDecorationProviders(reposManager); context.subscriptions.push(treeDecorationProviders); - treeDecorationProviders.registerProviders([new FileTypeDecorationProvider(), new CommentDecorationProvider(reposManager)]); + treeDecorationProviders.registerProviders([new FileTypeDecorationProvider(), new CommentDecorationProvider(reposManager), new CommitsDecorationProvider(reposManager)]); const notificationsProvider = new NotificationsProvider(credentialStore, reposManager); context.subscriptions.push(notificationsProvider); diff --git a/src/view/commitsDecorationProvider.ts b/src/view/commitsDecorationProvider.ts new file mode 100644 index 0000000000..a9008b19a5 --- /dev/null +++ b/src/view/commitsDecorationProvider.ts @@ -0,0 +1,61 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { TreeDecorationProvider } from './treeDecorationProviders'; +import { createCommitsNodeUri, fromCommitsNodeUri, Schemes } from '../common/uri'; +import { FolderRepositoryManager } from '../github/folderRepositoryManager'; +import { PullRequestModel } from '../github/pullRequestModel'; +import { RepositoriesManager } from '../github/repositoriesManager'; + +export class CommitsDecorationProvider extends TreeDecorationProvider { + + constructor(private readonly _repositoriesManager: RepositoriesManager) { + super(); + } + + registerPullRequestPropertyChangedListeners(_folderManager: FolderRepositoryManager, model: PullRequestModel): vscode.Disposable { + return model.onDidChange(e => { + if (e.timeline) { + // Timeline changed, which may include new commits, so update the decoration + const uri = createCommitsNodeUri(model.remote.owner, model.remote.repositoryName, model.number); + this._onDidChangeFileDecorations.fire(uri); + } + }); + } + + provideFileDecoration( + uri: vscode.Uri, + _token: vscode.CancellationToken, + ): vscode.ProviderResult { + if (uri.scheme !== Schemes.CommitsNode) { + return undefined; + } + + const params = fromCommitsNodeUri(uri); + if (!params) { + return undefined; + } + + const folderManager = this._repositoriesManager.getManagerForRepository(params.owner, params.repo); + + if (folderManager) { + const repo = folderManager.findExistingGitHubRepository({ owner: params.owner, repositoryName: params.repo }); + if (repo) { + const pr = repo.getExistingPullRequestModel(params.prNumber); + if (pr) { + const commitsCount = pr.item.commits.length; + return { + badge: commitsCount.toString(), + tooltip: vscode.l10n.t('{0} commits', commitsCount) + }; + } + } + } + + return undefined; + } + +} diff --git a/src/view/treeDecorationProviders.ts b/src/view/treeDecorationProviders.ts index b872e0040f..daa014bd31 100644 --- a/src/view/treeDecorationProviders.ts +++ b/src/view/treeDecorationProviders.ts @@ -11,7 +11,7 @@ import { PullRequestModel } from '../github/pullRequestModel'; import { RepositoriesManager } from '../github/repositoriesManager'; export abstract class TreeDecorationProvider extends Disposable implements vscode.FileDecorationProvider { - private _onDidChangeFileDecorations: vscode.EventEmitter = this._register(new vscode.EventEmitter< + protected _onDidChangeFileDecorations: vscode.EventEmitter = this._register(new vscode.EventEmitter< vscode.Uri | vscode.Uri[] >()); onDidChangeFileDecorations?: vscode.Event | undefined = this._onDidChangeFileDecorations.event; diff --git a/src/view/treeNodes/commitsCategoryNode.ts b/src/view/treeNodes/commitsCategoryNode.ts index 88adf29b8a..7a0fb197ca 100644 --- a/src/view/treeNodes/commitsCategoryNode.ts +++ b/src/view/treeNodes/commitsCategoryNode.ts @@ -7,13 +7,16 @@ import * as vscode from 'vscode'; import { CommitNode } from './commitNode'; import { TreeNode, TreeNodeParent } from './treeNode'; import Logger, { PR_TREE } from '../../common/logger'; +import { createCommitsNodeUri } from '../../common/uri'; import { FolderRepositoryManager } from '../../github/folderRepositoryManager'; import { PullRequestModel } from '../../github/pullRequestModel'; export class CommitsNode extends TreeNode implements vscode.TreeItem { public collapsibleState: vscode.TreeItemCollapsibleState; + public resourceUri: vscode.Uri; private _folderRepoManager: FolderRepositoryManager; private _pr: PullRequestModel; + public tooltip?: string; constructor( parent: TreeNodeParent, @@ -25,6 +28,8 @@ export class CommitsNode extends TreeNode implements vscode.TreeItem { this._pr = pr; this._folderRepoManager = reposManager; this.collapsibleState = vscode.TreeItemCollapsibleState.Collapsed; + this.resourceUri = createCommitsNodeUri(pr.remote.owner, pr.remote.repositoryName, pr.number); + this.tooltip = vscode.l10n.t('Commits'); this.childrenDisposables = []; this.childrenDisposables.push(this._pr.onDidChangeReviewThreads(() => { From cda402b251eb25f8db611eb00fcc699dfbce0b43 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:11:33 +0000 Subject: [PATCH 061/402] Adopt comment draft state extension API proposal (#8135) * Initial plan * Adopt comment draft state extension API proposal - Created vscode.proposed.commentState.d.ts with CommentState enum - Added commentState to enabledApiProposals in package.json - Updated CommentBase, TemporaryComment, and GHPRComment to use new state property - Comments now have state set to Draft or Published based on isDraft flag Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * clean up --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- package.json | 1 + .../vscode.proposed.commentsDraftState.d.ts | 18 ++++++++++++++++++ src/github/prComment.ts | 11 ++++++++++- 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 src/@types/vscode.proposed.commentsDraftState.d.ts diff --git a/package.json b/package.json index 7ee791b5b8..83f8c4e1da 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "commentingRangeHint", "commentReactor", "commentReveal", + "commentsDraftState", "commentThreadApplicability", "contribAccessibilityHelpContent", "contribCommentEditorActionsMenu", diff --git a/src/@types/vscode.proposed.commentsDraftState.d.ts b/src/@types/vscode.proposed.commentsDraftState.d.ts new file mode 100644 index 0000000000..ee41130910 --- /dev/null +++ b/src/@types/vscode.proposed.commentsDraftState.d.ts @@ -0,0 +1,18 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + // https://github.com/microsoft/vscode/issues/171166 + + export enum CommentState { + Published = 0, + Draft = 1 + } + + export interface Comment { + state?: CommentState; + } +} diff --git a/src/github/prComment.ts b/src/github/prComment.ts index 4547387758..12d74662f3 100644 --- a/src/github/prComment.ts +++ b/src/github/prComment.ts @@ -107,6 +107,11 @@ abstract class CommentBase implements vscode.Comment { */ public contextValue: string; + /** + * The state of the comment (Published or Draft) + */ + public state?: vscode.CommentState; + constructor( parent: GHPRCommentThread, ) { @@ -173,6 +178,7 @@ export class TemporaryComment extends CommentBase { iconPath: currentUser.avatarUrl ? vscode.Uri.parse(`${currentUser.avatarUrl}&s=64`) : undefined, }; this.label = isDraft ? vscode.l10n.t('Pending') : undefined; + this.state = isDraft ? vscode.CommentState.Draft : vscode.CommentState.Published; this.contextValue = 'temporary,canEdit,canDelete'; this.originalBody = originalComment ? originalComment.rawComment.body : undefined; this.reactions = originalComment ? originalComment.reactions : undefined; @@ -249,6 +255,7 @@ export class GHPRComment extends CommentBase { updateCommentReactions(this, comment.reactions); this.label = comment.isDraft ? vscode.l10n.t('Pending') : undefined; + this.state = comment.isDraft ? vscode.CommentState.Draft : vscode.CommentState.Published; const contextValues: string[] = []; if (comment.canEdit) { @@ -288,7 +295,9 @@ export class GHPRComment extends CommentBase { const oldLabel = this.label; this.label = comment.isDraft ? vscode.l10n.t('Pending') : undefined; - if (this.label !== oldLabel) { + const newState = comment.isDraft ? vscode.CommentState.Draft : vscode.CommentState.Published; + if (this.label !== oldLabel || this.state !== newState) { + this.state = newState; refresh = true; } From a6839975035c690227b5250a6b6ac89dbe0bbf26 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Thu, 13 Nov 2025 14:37:42 +0000 Subject: [PATCH 062/402] Convert "Ready for Review" icon button to ContextDropdown (#8138) * Initial plan * Convert icon button to dropdown in ReadyForReview component Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Replace Dropdown with ContextDropdown per feedback Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Add menu configuration for ContextDropdown in package.json Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Also adopt in focus view * Implement pr.readyForReviewAndMerge command with busy state handling Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Move command registrations from pullRequestOverview.ts to commands.ts Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Revert "Move command registrations from pullRequestOverview.ts to commands.ts" This reverts commit c01c8240935a5b198bc93c43c19edf41e368e9ea. * Finish implementing context dropdowns. --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- package.json | 45 ++++++++++++++++++- package.nls.json | 3 +- src/commands.ts | 31 ------------- src/github/activityBarViewProvider.ts | 63 ++++++++++++++++++++++++--- src/github/pullRequestOverview.ts | 53 +++++++++++++++++++++- src/github/views.ts | 7 +++ webviews/activityBarView/index.css | 12 ++++- webviews/common/context.tsx | 32 +++++++++++++- webviews/components/merge.tsx | 60 +++++++++++++++++-------- 9 files changed, 246 insertions(+), 60 deletions(-) diff --git a/package.json b/package.json index 83f8c4e1da..37f7c53366 100644 --- a/package.json +++ b/package.json @@ -982,6 +982,21 @@ "title": "%command.pr.readyForReview.title%", "category": "%command.pull.request.category%" }, + { + "command": "pr.readyForReviewAndMerge", + "title": "%command.pr.readyForReviewAndMerge.title%", + "category": "%command.pull.request.category%" + }, + { + "command": "pr.readyForReviewDescription", + "title": "%command.pr.readyForReview.title%", + "category": "%command.pull.request.category%" + }, + { + "command": "pr.readyForReviewAndMergeDescription", + "title": "%command.pr.readyForReviewAndMerge.title%", + "category": "%command.pull.request.category%" + }, { "command": "pr.openPullRequestOnGitHub", "title": "%command.pr.openPullRequestOnGitHub.title%", @@ -2058,7 +2073,19 @@ }, { "command": "pr.readyForReview", - "when": "gitHubOpenRepositoryCount != 0 && github:inReviewMode" + "when": "false" + }, + { + "command": "pr.readyForReviewDescription", + "when": "false" + }, + { + "command": "pr.readyForReviewAndMergeDescription", + "when": "false" + }, + { + "command": "pr.readyForReviewAndMerge", + "when": "false" }, { "command": "pr.openPullRequestOnGitHub", @@ -3534,6 +3561,22 @@ "command": "pr.copyVscodeDevPrLink", "when": "webviewId == PullRequestOverview && github:copyMenu" }, + { + "command": "pr.readyForReviewDescription", + "when": "(webviewId == PullRequestOverview) && github:readyForReviewMenu" + }, + { + "command": "pr.readyForReviewAndMergeDescription", + "when": "(webviewId == PullRequestOverview) && github:readyForReviewMenu && github:readyForReviewMenuWithMerge" + }, + { + "command": "pr.readyForReview", + "when": "(webviewId == 'github:activePullRequest') && github:readyForReviewMenu" + }, + { + "command": "pr.readyForReviewAndMerge", + "when": "(webviewId == 'github:activePullRequest') && github:readyForReviewMenu && github:readyForReviewMenuWithMerge" + }, { "command": "pr.openChanges", "group": "checkout@0", diff --git a/package.nls.json b/package.nls.json index 17aeb3a2b5..60019ff091 100644 --- a/package.nls.json +++ b/package.nls.json @@ -193,7 +193,8 @@ "command.pr.dismissNotification.title": "Dismiss Notification", "command.pr.markAllCopilotNotificationsAsRead.title": "Dismiss All Copilot Notifications", "command.pr.merge.title": "Merge Pull Request", - "command.pr.readyForReview.title": "Mark Pull Request Ready For Review", + "command.pr.readyForReview.title": "Ready for Review", + "command.pr.readyForReviewAndMerge.title": "Ready, Approve, and Auto-Merge", "command.pr.openPullRequestOnGitHub.title": "Open Pull Request on GitHub", "command.pr.openAllDiffs.title": "Open All Diffs", "command.pr.refreshPullRequest.title": "Refresh Pull Request", diff --git a/src/commands.ts b/src/commands.ts index 4844af4cfa..1bd7fdb68d 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -813,37 +813,6 @@ export function registerCommands( }), ); - context.subscriptions.push( - vscode.commands.registerCommand('pr.readyForReview', async (pr?: PRNode) => { - const folderManager = reposManager.getManagerForIssueModel(pr?.pullRequestModel); - if (!folderManager) { - return; - } - const pullRequest = ensurePR(folderManager, pr); - const yes = vscode.l10n.t('Yes'); - return vscode.window - .showWarningMessage( - vscode.l10n.t('Are you sure you want to mark this pull request as ready to review on GitHub?'), - { modal: true }, - yes, - ) - .then(async value => { - let isDraft; - if (value === yes) { - try { - isDraft = (await pullRequest.setReadyForReview()).isDraft; - return isDraft; - } catch (e) { - vscode.window.showErrorMessage( - `Unable to mark pull request as ready to review. ${formatError(e)}`, - ); - return isDraft; - } - } - }); - }), - ); - context.subscriptions.push( vscode.commands.registerCommand('pr.dismissNotification', node => { if (node instanceof PRNode) { diff --git a/src/github/activityBarViewProvider.ts b/src/github/activityBarViewProvider.ts index b9243558b7..3a509f1c56 100644 --- a/src/github/activityBarViewProvider.ts +++ b/src/github/activityBarViewProvider.ts @@ -6,12 +6,12 @@ import * as vscode from 'vscode'; import { openPullRequestOnGitHub } from '../commands'; import { FolderRepositoryManager } from './folderRepositoryManager'; -import { GithubItemStateEnum, IAccount, isITeam, ITeam, PullRequestMergeability, reviewerId, ReviewEventEnum, ReviewState } from './interface'; -import { PullRequestModel } from './pullRequestModel'; +import { GithubItemStateEnum, IAccount, isITeam, ITeam, MergeMethod, PullRequestMergeability, reviewerId, ReviewEventEnum, ReviewState } from './interface'; +import { isCopilotOnMyBehalf, PullRequestModel } from './pullRequestModel'; import { getDefaultMergeMethod } from './pullRequestOverview'; import { PullRequestView } from './pullRequestOverviewCommon'; import { isInCodespaces, parseReviewers } from './utils'; -import { MergeArguments, PullRequest, ReviewType, SubmitReviewReply } from './views'; +import { MergeArguments, PullRequest, ReadyForReviewReply, ReviewType, SubmitReviewReply } from './views'; import { IComment } from '../common/comment'; import { emojify, ensureEmojis } from '../common/emoji'; import { disposeAll } from '../common/lifecycle'; @@ -34,6 +34,12 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W ) { super(extensionUri); + this._register(vscode.commands.registerCommand('pr.readyForReview', async () => { + return this.readyForReviewCommand(); + })); + this._register(vscode.commands.registerCommand('pr.readyForReviewAndMerge', async (context: { mergeMethod: MergeMethod }) => { + return this.readyForReviewAndMergeCommand(context); + })); this._register(vscode.commands.registerCommand('review.approve', (e: { body: string }) => this.approvePullRequestCommand(e))); this._register(vscode.commands.registerCommand('review.comment', (e: { body: string }) => this.submitReviewCommand(e))); this._register(vscode.commands.registerCommand('review.requestChanges', (e: { body: string }) => this.requestChangesCommand(e))); @@ -212,7 +218,7 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W this.registerPrSpecificListeners(pullRequestModel); } this._item = pullRequestModel; - const [pullRequest, repositoryAccess, timelineEvents, requestedReviewers, branchInfo, defaultBranch, currentUser, viewerCanEdit, hasReviewDraft] = await Promise.all([ + const [pullRequest, repositoryAccess, timelineEvents, requestedReviewers, branchInfo, defaultBranch, currentUser, viewerCanEdit, hasReviewDraft, coAuthors] = await Promise.all([ this._folderRepositoryManager.resolvePullRequest( pullRequestModel.remote.owner, pullRequestModel.remote.repositoryName, @@ -226,6 +232,7 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W this._folderRepositoryManager.getCurrentUser(pullRequestModel.githubRepository), pullRequestModel.canEdit(), pullRequestModel.validateDraftMode(), + pullRequestModel.getCoAuthors(), ensureEmojis(this._folderRepositoryManager.context) ]); @@ -308,7 +315,8 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W isDarkTheme: vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark, isEnterprise: pullRequest.githubRepository.remote.isEnterprise, hasReviewDraft, - currentUserReviewState: reviewState + currentUserReviewState: reviewState, + isCopilotOnMyBehalf: await isCopilotOnMyBehalf(pullRequest, currentUser, coAuthors) }; this._postMessage({ @@ -457,6 +465,51 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W }); } + private async readyForReviewCommand(): Promise { + this._postMessage({ + command: 'pr.readying-for-review' + }); + try { + const result = await this._item.setReadyForReview(); + + const readiedResult: ReadyForReviewReply = { + isDraft: result.isDraft + }; + await this._postMessage({ + command: 'pr.readied-for-review', + result: readiedResult + }); + } catch (e) { + vscode.window.showErrorMessage(`Unable to set pull request ready for review. ${formatError(e)}`); + this._throwError(undefined, e.message); + } + } + + private async readyForReviewAndMergeCommand(context: { mergeMethod: MergeMethod }): Promise { + this._postMessage({ + command: 'pr.readying-for-review' + }); + try { + const [readyResult, approveResult] = await Promise.all([this._item.setReadyForReview(), this._item.approve(this._folderRepositoryManager.repository)]); + await this._item.enableAutoMerge(context.mergeMethod); + this.updateReviewers(approveResult); + + const readiedResult: ReadyForReviewReply = { + isDraft: readyResult.isDraft, + autoMerge: true, + reviewEvent: approveResult, + reviewers: this._existingReviewers + }; + await this._postMessage({ + command: 'pr.readied-for-review', + result: readiedResult + }); + } catch (e) { + vscode.window.showErrorMessage(`Unable to set pull request ready for review. ${formatError(e)}`); + this._throwError(undefined, e.message); + } + } + private async mergePullRequest( message: IRequestMessage, ): Promise { diff --git a/src/github/pullRequestOverview.ts b/src/github/pullRequestOverview.ts index e7fb80cd68..2a81aef95b 100644 --- a/src/github/pullRequestOverview.ts +++ b/src/github/pullRequestOverview.ts @@ -27,7 +27,7 @@ import { isCopilotOnMyBehalf, PullRequestModel } from './pullRequestModel'; import { PullRequestView } from './pullRequestOverviewCommon'; import { pickEmail, reviewersQuickPick } from './quickPicks'; import { parseReviewers } from './utils'; -import { CancelCodingAgentReply, DeleteReviewResult, MergeArguments, MergeResult, PullRequest, ReviewType, SubmitReviewReply } from './views'; +import { CancelCodingAgentReply, DeleteReviewResult, MergeArguments, MergeResult, PullRequest, ReadyForReviewReply, ReviewType, SubmitReviewReply } from './views'; import { IComment } from '../common/comment'; import { COPILOT_SWE_AGENT, copilotEventToStatus, CopilotPRStatus, mostRecentCopilotEvent } from '../common/copilot'; import { commands, contexts } from '../common/executeCommands'; @@ -142,6 +142,12 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel { + return this.readyForReviewCommand(); + })); + this._register(vscode.commands.registerCommand('pr.readyForReviewAndMergeDescription', async (context: { mergeMethod: MergeMethod }) => { + return this.readyForReviewAndMergeCommand(context); + })); this._register(vscode.commands.registerCommand('review.approveDescription', (e) => this.approvePullRequestCommand(e))); this._register(vscode.commands.registerCommand('review.commentDescription', (e) => this.submitReviewCommand(e))); this._register(vscode.commands.registerCommand('review.requestChangesDescription', (e) => this.requestChangesCommand(e))); @@ -678,6 +684,51 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel { + this._postMessage({ + command: 'pr.readying-for-review' + }); + try { + const result = await this._item.setReadyForReview(); + + const readiedResult: ReadyForReviewReply = { + isDraft: result.isDraft + }; + await this._postMessage({ + command: 'pr.readied-for-review', + result: readiedResult + }); + } catch (e) { + vscode.window.showErrorMessage(`Unable to set pull request ready for review. ${formatError(e)}`); + this._throwError(undefined, e.message); + } + } + + private async readyForReviewAndMergeCommand(context: { mergeMethod: MergeMethod }): Promise { + this._postMessage({ + command: 'pr.readying-for-review' + }); + try { + const [readyResult, approveResult] = await Promise.all([this._item.setReadyForReview(), this._item.approve(this._folderRepositoryManager.repository)]); + await this._item.enableAutoMerge(context.mergeMethod); + this.updateReviewers(approveResult); + + const readiedResult: ReadyForReviewReply = { + isDraft: readyResult.isDraft, + autoMerge: true, + reviewEvent: approveResult, + reviewers: this._existingReviewers + }; + await this._postMessage({ + command: 'pr.readied-for-review', + result: readiedResult + }); + } catch (e) { + vscode.window.showErrorMessage(`Unable to set pull request ready for review. ${formatError(e)}`); + this._throwError(undefined, e.message); + } + } + private async checkoutDefaultBranch(message: IRequestMessage): Promise { try { const prBranch = this._folderRepositoryManager.repository.state.HEAD?.name; diff --git a/src/github/views.ts b/src/github/views.ts index 70a9dc245a..a2866cb004 100644 --- a/src/github/views.ts +++ b/src/github/views.ts @@ -126,6 +126,13 @@ export interface SubmitReviewReply { reviewers?: ReviewState[]; } +export interface ReadyForReviewReply { + isDraft: boolean; + reviewEvent?: ReviewEvent; + reviewers?: ReviewState[]; + autoMerge?: boolean; +} + export interface MergeArguments { title: string | undefined; description: string | undefined; diff --git a/webviews/activityBarView/index.css b/webviews/activityBarView/index.css index 863dbf9c15..96ba38b972 100644 --- a/webviews/activityBarView/index.css +++ b/webviews/activityBarView/index.css @@ -75,7 +75,17 @@ form, width: 100%; } -.button-container button { +.button-container>button { + width: 100%; +} + +.button-container:has(> .dropdown-container) { + display: flex; + min-width: 0; +} + +.dropdown-container { + flex-grow: 1; width: 100%; } diff --git a/webviews/common/context.tsx b/webviews/common/context.tsx index dd00c8076f..d5aa38643a 100644 --- a/webviews/common/context.tsx +++ b/webviews/common/context.tsx @@ -10,7 +10,7 @@ import { CloseResult, OpenCommitChangesArgs } from '../../common/views'; import { IComment } from '../../src/common/comment'; import { EventType, ReviewEvent, SessionLinkInfo, TimelineEvent } from '../../src/common/timelineEvent'; import { IProjectItem, MergeMethod, ReadyForReview } from '../../src/github/interface'; -import { CancelCodingAgentReply, ChangeAssigneesReply, DeleteReviewResult, MergeArguments, MergeResult, ProjectItemsReply, PullRequest, SubmitReviewReply } from '../../src/github/views'; +import { CancelCodingAgentReply, ChangeAssigneesReply, DeleteReviewResult, MergeArguments, MergeResult, ProjectItemsReply, PullRequest, ReadyForReviewReply, SubmitReviewReply } from '../../src/github/views'; export class PRContext { constructor( @@ -245,6 +245,32 @@ export class PRContext { this.updatePR(state); } + private readyForReviewComplete(reply: ReadyForReviewReply) { + const { pr: state } = this; + if (!state) { + throw new Error('Unexpectedly no pull request when trying to ready for review'); + } + const { isDraft, reviewEvent, reviewers } = reply; + state.busy = false; + state.isDraft = isDraft; + if (!reviewEvent) { + this.updatePR(state); + return; + } + if (reviewers) { + state.reviewers = reviewers; + } + state.events = [...state.events, reviewEvent]; + if (reviewEvent.event === EventType.Reviewed) { + state.currentUserReviewState = reviewEvent.state; + } + if (reply.autoMerge !== undefined) { + state.autoMerge = reply.autoMerge; + state.autoMergeMethod = state.defaultMergeMethod; + } + this.updatePR(state); + } + public reRequestReview = async (reviewerId: string) => { const { pr: state } = this; if (!state) { @@ -388,6 +414,10 @@ export class PRContext { return this.updatePR({ busy: true, lastReviewType: message.lastReviewType }); case 'pr.append-review': return this.appendReview(message); + case 'pr.readying-for-review': + return this.updatePR({ busy: true }); + case 'pr.readied-for-review': + return this.readyForReviewComplete(message); } }; diff --git a/webviews/components/merge.tsx b/webviews/components/merge.tsx index 46d5d758cf..725a793d90 100644 --- a/webviews/components/merge.tsx +++ b/webviews/components/merge.tsx @@ -13,8 +13,9 @@ import React, { useState, } from 'react'; import { AutoMerge, QueuedToMerge } from './automergeSelect'; +import { ContextDropdown } from './contextDropdown'; import { Dropdown } from './dropdown'; -import { checkAllIcon, checkIcon, circleFilledIcon, closeIcon, gitMergeIcon, loadingIcon, requestChangesIcon, skipIcon, warningIcon } from './icon'; +import { checkIcon, circleFilledIcon, closeIcon, gitMergeIcon, requestChangesIcon, skipIcon, warningIcon } from './icon'; import { nbsp } from './space'; import { Avatar } from './user'; import { EventType, ReviewEvent } from '../../src/common/timelineEvent'; @@ -284,9 +285,8 @@ export const OfferToUpdate = ({ mergeable, isSimple, isCurrentlyCheckedOut, canU }; export const ReadyForReview = ({ isSimple, isCopilotOnMyBehalf, mergeMethod }: { isSimple: boolean; isCopilotOnMyBehalf?: boolean; mergeMethod: MergeMethod }) => { - const [isBusy, setBusy] = useState(false); - const [isMergeBusy, setMergeBusy] = useState(false); - const { readyForReview, readyForReviewAndMerge, updatePR } = useContext(PullRequestContext); + const { readyForReview, readyForReviewAndMerge, updatePR, pr } = useContext(PullRequestContext); + const [isBusy, setBusy] = useState(pr?.busy ?? false); const markReadyForReview = useCallback(async () => { try { @@ -296,20 +296,38 @@ export const ReadyForReview = ({ isSimple, isCopilotOnMyBehalf, mergeMethod }: { } finally { setBusy(false); } - }, [setBusy, readyForReview, updatePR]); + }, [readyForReview, updatePR]); const markReadyAndMerge = useCallback(async () => { try { setBusy(true); - setMergeBusy(true); const result = await readyForReviewAndMerge({ mergeMethod: mergeMethod }); updatePR(result); } finally { setBusy(false); - setMergeBusy(false); } }, [readyForReviewAndMerge, updatePR, mergeMethod]); + const allOptions = useCallback(() => { + const actions: { label: string; value: string; action: (event: React.MouseEvent) => void }[] = [ + { + label: 'Ready for Review', + value: 'ready', + action: markReadyForReview + } + ]; + + if (isCopilotOnMyBehalf) { + actions.push({ + label: 'Ready, Approve, and Auto-Merge', + value: 'readyAndMerge', + action: markReadyAndMerge + }); + } + + return actions; + }, [isCopilotOnMyBehalf, markReadyForReview, markReadyAndMerge]); + return (
    @@ -320,18 +338,22 @@ export const ReadyForReview = ({ isSimple, isCopilotOnMyBehalf, mergeMethod }: {
    - {isCopilotOnMyBehalf && ( - - )} - + JSON.stringify({ + 'preventDefaultContextMenuItems': true, + 'github:readyForReviewMenu': true, + 'github:readyForReviewMenuWithMerge': isCopilotOnMyBehalf, + 'mergeMethod': mergeMethod + })} + defaultAction={markReadyForReview} + defaultOptionLabel={() => 'Ready for Review'} + defaultOptionValue={() => 'ready'} + allOptions={allOptions} + optionsTitle='Ready for Review' + disabled={isBusy || pr?.busy} + hasSingleAction={!isCopilotOnMyBehalf} + spreadable={false} + />
    ); From 1eee69886a248d3bfeea43ed5a37616eca886df6 Mon Sep 17 00:00:00 2001 From: Vignesh Date: Fri, 14 Nov 2025 16:37:04 +0530 Subject: [PATCH 063/402] feat: Display commit status icon for each commit (#8142) * feat(timeline): Display commit status checks in PR timeline * fix: CommitEvent status * fix: CommitStateIcon status prop type * Prefer undefined --------- Co-authored-by: Alex Ross <38270282+alexr00@users.noreply.github.com> --- src/common/timelineEvent.ts | 1 + src/github/graphql.ts | 3 +++ src/github/queriesShared.gql | 3 +++ src/github/utils.ts | 1 + webviews/components/timeline.tsx | 19 ++++++++++++++++++- 5 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/common/timelineEvent.ts b/src/common/timelineEvent.ts index 2f719c5f2e..8ef0c07ee0 100644 --- a/src/common/timelineEvent.ts +++ b/src/common/timelineEvent.ts @@ -81,6 +81,7 @@ export interface CommitEvent { message: string; bodyHTML?: string; committedDate: Date; + status?: 'EXPECTED' | 'ERROR' | 'FAILURE' | 'PENDING' | 'SUCCESS'; } export interface NewCommitsSinceReviewEvent { diff --git a/src/github/graphql.ts b/src/github/graphql.ts index ad48671e85..f1dddf0b8b 100644 --- a/src/github/graphql.ts +++ b/src/github/graphql.ts @@ -188,6 +188,9 @@ export interface Commit { oid: string; message: string; committedDate: Date; + statusCheckRollup?: { + state: 'EXPECTED' | 'ERROR' | 'FAILURE' | 'PENDING' | 'SUCCESS'; + }; }; url: string; diff --git a/src/github/queriesShared.gql b/src/github/queriesShared.gql index 7716dd9a4f..473841f0a1 100644 --- a/src/github/queriesShared.gql +++ b/src/github/queriesShared.gql @@ -123,6 +123,9 @@ fragment Commit on PullRequestCommit { oid message committedDate + statusCheckRollup { + state + } } url } diff --git a/src/github/utils.ts b/src/github/utils.ts index fba9d565ec..559bbc0266 100644 --- a/src/github/utils.ts +++ b/src/github/utils.ts @@ -1200,6 +1200,7 @@ export async function parseCombinedTimelineEvents( htmlUrl: commitEv.url, message: commitEv.commit.message, committedDate: new Date(commitEv.commit.committedDate), + status: commitEv.commit.statusCheckRollup?.state } as Common.CommitEvent); // TODO remove cast break; case Common.EventType.Merged: diff --git a/webviews/components/timeline.tsx b/webviews/components/timeline.tsx index 193828756f..b3b1cd24a0 100644 --- a/webviews/components/timeline.tsx +++ b/webviews/components/timeline.tsx @@ -6,7 +6,7 @@ import React, { useContext, useRef, useState } from 'react'; import { CommentView } from './comment'; import Diff from './diff'; -import { addIcon, errorIcon, gitCommitIcon, gitMergeIcon, loadingIcon, tasklistIcon, threeBars } from './icon'; +import { addIcon, checkIcon, circleFilledIcon, closeIcon, errorIcon, gitCommitIcon, gitMergeIcon, loadingIcon, tasklistIcon, threeBars } from './icon'; import { nbsp } from './space'; import { Timestamp } from './timestamp'; import { AuthorLink, Avatar } from './user'; @@ -105,6 +105,20 @@ export const Timeline = ({ events, isIssue }: { events: TimelineEvent[], isIssue export default Timeline; + +function CommitStateIcon({ status }: { status: 'EXPECTED' | 'ERROR' | 'FAILURE' | 'PENDING' | 'SUCCESS' | undefined; }) { + switch (status) { + case 'PENDING': + return circleFilledIcon; + case 'SUCCESS': + return checkIcon; + case 'FAILURE': + case 'ERROR': + return closeIcon; + } + return null; +} + const CommitEventView = (event: CommitEvent) => { const context = useContext(PullRequestContext); const [clickedElement, setClickedElement] = useState<'title' | 'sha' | undefined>(undefined); @@ -139,6 +153,9 @@ const CommitEventView = (event: CommitEvent) => {
+
+ +
handleCommitClick(e, 'sha')} From 7a035738dbc750bcfb339ad48e6caf26c7902de4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 14 Nov 2025 15:50:43 +0100 Subject: [PATCH 064/402] Bump koa from 3.0.1 to 3.0.3 (#8030) Bumps [koa](https://github.com/koajs/koa) from 3.0.1 to 3.0.3. - [Release notes](https://github.com/koajs/koa/releases) - [Changelog](https://github.com/koajs/koa/blob/master/History.md) - [Commits](https://github.com/koajs/koa/compare/v3.0.1...v3.0.3) --- updated-dependencies: - dependency-name: koa dependency-version: 3.0.3 dependency-type: indirect ... Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- yarn.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/yarn.lock b/yarn.lock index a95d623eb0..ccc6b621fc 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4921,9 +4921,9 @@ koa-static@^5.0.0: koa-send "^5.0.0" koa@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/koa/-/koa-3.0.1.tgz#b211a0f350d1cc6185047671f8ef7e019c16351d" - integrity sha512-oDxVkRwPOHhGlxKIDiDB2h+/l05QPtefD7nSqRgDfZt8P+QVYFWjfeK8jANf5O2YXjk8egd7KntvXKYx82wOag== + version "3.0.3" + resolved "https://registry.yarnpkg.com/koa/-/koa-3.0.3.tgz#40a97b6da0c0d49a704af82db2b3e5a618ed9042" + integrity sha512-MeuwbCoN1daWS32/Ni5qkzmrOtQO2qrnfdxDHjrm6s4b59yG4nexAJ0pTEFyzjLp0pBVO80CZp0vW8Ze30Ebow== dependencies: accepts "^1.3.8" content-disposition "~0.5.4" From 66a2b237234b433a2ad102de290f48008705196a Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 14 Nov 2025 15:19:46 +0000 Subject: [PATCH 065/402] Improve code reuse in PullRequestOverviewPanel and PullRequestViewProvider (#8145) * Initial plan * Extract common PR review methods into shared utility functions Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> * Clean up * More cleanup --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: alexr00 <38270282+alexr00@users.noreply.github.com> --- src/github/activityBarViewProvider.ts | 225 +++---------- src/github/pullRequestOverview.ts | 248 +++------------ src/github/pullRequestOverviewCommon.ts | 148 --------- src/github/pullRequestReviewCommon.ts | 406 ++++++++++++++++++++++++ 4 files changed, 507 insertions(+), 520 deletions(-) delete mode 100644 src/github/pullRequestOverviewCommon.ts create mode 100644 src/github/pullRequestReviewCommon.ts diff --git a/src/github/activityBarViewProvider.ts b/src/github/activityBarViewProvider.ts index 3a509f1c56..00f623fa1f 100644 --- a/src/github/activityBarViewProvider.ts +++ b/src/github/activityBarViewProvider.ts @@ -6,12 +6,12 @@ import * as vscode from 'vscode'; import { openPullRequestOnGitHub } from '../commands'; import { FolderRepositoryManager } from './folderRepositoryManager'; -import { GithubItemStateEnum, IAccount, isITeam, ITeam, MergeMethod, PullRequestMergeability, reviewerId, ReviewEventEnum, ReviewState } from './interface'; +import { GithubItemStateEnum, IAccount, MergeMethod, ReviewEventEnum, ReviewState } from './interface'; import { isCopilotOnMyBehalf, PullRequestModel } from './pullRequestModel'; import { getDefaultMergeMethod } from './pullRequestOverview'; -import { PullRequestView } from './pullRequestOverviewCommon'; +import { PullRequestReviewCommon, ReviewContext } from './pullRequestReviewCommon'; import { isInCodespaces, parseReviewers } from './utils'; -import { MergeArguments, PullRequest, ReadyForReviewReply, ReviewType, SubmitReviewReply } from './views'; +import { MergeArguments, PullRequest, ReviewType } from './views'; import { IComment } from '../common/comment'; import { emojify, ensureEmojis } from '../common/emoji'; import { disposeAll } from '../common/lifecycle'; @@ -63,29 +63,11 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W } private async updateBranch(message: IRequestMessage): Promise { - if (this._folderRepositoryManager.repository.state.workingTreeChanges.length > 0 || this._folderRepositoryManager.repository.state.indexChanges.length > 0) { - await vscode.window.showErrorMessage(vscode.l10n.t('The pull request branch cannot be updated when the there changed files in the working tree or index. Stash or commit all change and then try again.'), { modal: true }); - return this._replyMessage(message, {}); - } - const mergeSucceeded = await this._folderRepositoryManager.tryMergeBaseIntoHead(this._item, true); - if (!mergeSucceeded) { - this._replyMessage(message, {}); - } - // The mergability of the PR doesn't update immediately. Poll. - let mergability = PullRequestMergeability.Unknown; - let attemptsRemaining = 5; - do { - mergability = (await this._item.getMergeability()).mergeability; - attemptsRemaining--; - await new Promise(c => setTimeout(c, 1000)); - } while (attemptsRemaining > 0 && mergability === PullRequestMergeability.Unknown); - - const result: Partial = { - events: await this._item.getTimelineEvents(), - mergeable: mergability, - }; - await this.refresh(); - this._replyMessage(message, result); + return PullRequestReviewCommon.updateBranch( + this.getReviewContext(), + message, + () => this.refresh() + ); } protected override async _onDidReceiveMessage(message: IRequestMessage) { @@ -128,47 +110,11 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W } private async checkoutDefaultBranch(message: IRequestMessage): Promise { - try { - const defaultBranch = await this._folderRepositoryManager.getPullRequestRepositoryDefaultBranch(this._item); - const prBranch = this._folderRepositoryManager.repository.state.HEAD?.name; - await this._folderRepositoryManager.checkoutDefaultBranch(defaultBranch); - if (prBranch) { - await this._folderRepositoryManager.cleanupAfterPullRequest(prBranch, this._item); - } - } finally { - // Complete webview promise so that button becomes enabled again - this._replyMessage(message, {}); - } + return PullRequestReviewCommon.checkoutDefaultBranch(this.getReviewContext(), message); } private reRequestReview(message: IRequestMessage): void { - let targetReviewer: ReviewState | undefined; - const userReviewers: IAccount[] = []; - const teamReviewers: ITeam[] = []; - - for (const reviewer of this._existingReviewers) { - let id = reviewer.reviewer.id; - if (id && ((reviewer.state === 'REQUESTED') || (id === message.args))) { - if (id === message.args) { - targetReviewer = reviewer; - } - } - } - - if (targetReviewer && isITeam(targetReviewer.reviewer)) { - teamReviewers.push(targetReviewer.reviewer); - } else if (targetReviewer && !isITeam(targetReviewer.reviewer)) { - userReviewers.push(targetReviewer.reviewer); - } - - this._item.requestReview(userReviewers, teamReviewers, true).then(() => { - if (targetReviewer) { - targetReviewer.state = 'REQUESTED'; - } - this._replyMessage(message, { - reviewers: this._existingReviewers, - }); - }); + return PullRequestReviewCommon.reRequestReview(this.getReviewContext(), message); } public async refresh(): Promise { @@ -179,9 +125,22 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W } private getCurrentUserReviewState(reviewers: ReviewState[], currentUser: IAccount): string | undefined { - const review = reviewers.find(r => reviewerId(r.reviewer) === currentUser.login); - // There will always be a review. If not then the PR shouldn't have been or fetched/shown for the current user - return review?.state; + return PullRequestReviewCommon.getCurrentUserReviewState(reviewers, currentUser); + } + + /** + * Get the review context for helper functions + */ + private getReviewContext(): ReviewContext { + return { + item: this._item, + folderRepositoryManager: this._folderRepositoryManager, + existingReviewers: this._existingReviewers, + postMessage: (message: any) => this._postMessage(message), + replyMessage: (message: IRequestMessage, response: any) => this._replyMessage(message, response), + throwError: (message: IRequestMessage | undefined, error: string) => this._throwError(message, error), + getTimeline: () => this._item.getTimelineEvents() + }; } private _prDisposables: vscode.Disposable[] | undefined = undefined; @@ -353,83 +312,48 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W }); } - private updateReviewers(review?: ReviewEvent): void { - if (review && review.state) { - const existingReviewer = this._existingReviewers.find( - reviewer => review.user.login === reviewerId(reviewer.reviewer), - ); - if (existingReviewer) { - existingReviewer.state = review.state; - } else { - this._existingReviewers.push({ - reviewer: review.user, - state: review.state, - }); - } - } - } private async doReviewCommand(context: { body: string }, reviewType: ReviewType, action: (body: string) => Promise) { - const submittingMessage = { - command: 'pr.submitting-review', - lastReviewType: reviewType - }; - this._postMessage(submittingMessage); - try { - const review = await action(context.body); - this.updateReviewers(review); - const reviewMessage: SubmitReviewReply & { command: string } = { - command: 'pr.append-review', - events: [], - reviewers: this._existingReviewers, - reviewedEvent: review, - }; - await this._postMessage(reviewMessage); - } catch (e) { - vscode.window.showErrorMessage(vscode.l10n.t('Submitting review failed. {0}', formatError(e))); - this._throwError(undefined, `${formatError(e)}`); - this._postMessage({ command: 'pr.append-review' }); - } + return PullRequestReviewCommon.doReviewCommand( + this.getReviewContext(), + context, + reviewType, + false, + action + ); } private async doReviewMessage(message: IRequestMessage, action: (body) => Promise) { - try { - const review = await action(message.args); - this.updateReviewers(review); - const reviewMessage: SubmitReviewReply = { - events: [], - reviewedEvent: review, - reviewers: this._existingReviewers, - }; - this._replyMessage(message, reviewMessage); - } catch (e) { - vscode.window.showErrorMessage(vscode.l10n.t('Submitting review failed. {0}', formatError(e))); - this._throwError(message, `${formatError(e)}`); - } + return PullRequestReviewCommon.doReviewMessage( + this.getReviewContext(), + message, + false, + action + ); } private approvePullRequest(body: string): Promise { return this._item.approve(this._folderRepositoryManager.repository, body); } - private approvePullRequestMessage(message: IRequestMessage): Promise { - return this.doReviewMessage(message, (body) => this.approvePullRequest(body)); + private async approvePullRequestMessage(message: IRequestMessage): Promise { + await this.doReviewMessage(message, (body) => this.approvePullRequest(body)); } - private approvePullRequestCommand(context: { body: string }): Promise { - return this.doReviewCommand(context, ReviewType.Approve, (body) => this.approvePullRequest(body)); + private async approvePullRequestCommand(context: { body: string }): Promise { + await this.doReviewCommand(context, ReviewType.Approve, (body) => this.approvePullRequest(body)); } private requestChanges(body: string): Promise { return this._item.requestChanges(body); } - private requestChangesCommand(context: { body: string }): Promise { - return this.doReviewCommand(context, ReviewType.RequestChanges, (body) => this.requestChanges(body)); + private async requestChangesCommand(context: { body: string }): Promise { + await this.doReviewCommand(context, ReviewType.RequestChanges, (body) => this.requestChanges(body)); } - private requestChangesMessage(message: IRequestMessage): Promise { - return this.doReviewMessage(message, (body) => this.requestChanges(body)); + private async requestChangesMessage(message: IRequestMessage): Promise { + await this.doReviewMessage(message, (body) => this.requestChanges(body)); } private submitReview(body: string): Promise { @@ -445,7 +369,7 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W } private async deleteBranch(message: IRequestMessage) { - const result = await PullRequestView.deleteBranch(this._folderRepositoryManager, this._item); + const result = await PullRequestReviewCommon.deleteBranch(this._folderRepositoryManager, this._item); if (result.isReply) { this._replyMessage(message, result.message); } else { @@ -453,61 +377,16 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W } } - private setReadyForReview(message: IRequestMessage>): void { - this._item - .setReadyForReview() - .then(result => { - this._replyMessage(message, result); - }) - .catch(e => { - vscode.window.showErrorMessage(vscode.l10n.t('Unable to set pull request ready for review. {0}', formatError(e))); - this._throwError(message, ''); - }); + private async setReadyForReview(message: IRequestMessage>): Promise { + return PullRequestReviewCommon.setReadyForReview(this.getReviewContext(), message); } private async readyForReviewCommand(): Promise { - this._postMessage({ - command: 'pr.readying-for-review' - }); - try { - const result = await this._item.setReadyForReview(); - - const readiedResult: ReadyForReviewReply = { - isDraft: result.isDraft - }; - await this._postMessage({ - command: 'pr.readied-for-review', - result: readiedResult - }); - } catch (e) { - vscode.window.showErrorMessage(`Unable to set pull request ready for review. ${formatError(e)}`); - this._throwError(undefined, e.message); - } + return PullRequestReviewCommon.readyForReviewCommand(this.getReviewContext()); } private async readyForReviewAndMergeCommand(context: { mergeMethod: MergeMethod }): Promise { - this._postMessage({ - command: 'pr.readying-for-review' - }); - try { - const [readyResult, approveResult] = await Promise.all([this._item.setReadyForReview(), this._item.approve(this._folderRepositoryManager.repository)]); - await this._item.enableAutoMerge(context.mergeMethod); - this.updateReviewers(approveResult); - - const readiedResult: ReadyForReviewReply = { - isDraft: readyResult.isDraft, - autoMerge: true, - reviewEvent: approveResult, - reviewers: this._existingReviewers - }; - await this._postMessage({ - command: 'pr.readied-for-review', - result: readiedResult - }); - } catch (e) { - vscode.window.showErrorMessage(`Unable to set pull request ready for review. ${formatError(e)}`); - this._throwError(undefined, e.message); - } + return PullRequestReviewCommon.readyForReviewAndMergeCommand(this.getReviewContext(), context); } private async mergePullRequest( diff --git a/src/github/pullRequestOverview.ts b/src/github/pullRequestOverview.ts index 2a81aef95b..396d00276e 100644 --- a/src/github/pullRequestOverview.ts +++ b/src/github/pullRequestOverview.ts @@ -17,17 +17,15 @@ import { ITeam, MergeMethod, MergeMethodsAvailability, - PullRequestMergeability, - reviewerId, ReviewEventEnum, ReviewState, } from './interface'; import { IssueOverviewPanel } from './issueOverview'; import { isCopilotOnMyBehalf, PullRequestModel } from './pullRequestModel'; -import { PullRequestView } from './pullRequestOverviewCommon'; +import { PullRequestReviewCommon, ReviewContext } from './pullRequestReviewCommon'; import { pickEmail, reviewersQuickPick } from './quickPicks'; import { parseReviewers } from './utils'; -import { CancelCodingAgentReply, DeleteReviewResult, MergeArguments, MergeResult, PullRequest, ReadyForReviewReply, ReviewType, SubmitReviewReply } from './views'; +import { CancelCodingAgentReply, DeleteReviewResult, MergeArguments, MergeResult, PullRequest, ReviewType } from './views'; import { IComment } from '../common/comment'; import { COPILOT_SWE_AGENT, copilotEventToStatus, CopilotPRStatus, mostRecentCopilotEvent } from '../common/copilot'; import { commands, contexts } from '../common/executeCommands'; @@ -200,9 +198,22 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel reviewerId(r.reviewer) === currentUser.login); - // There will always be a review. If not then the PR shouldn't have been or fetched/shown for the current user - return review?.state; + return PullRequestReviewCommon.getCurrentUserReviewState(reviewers, currentUser); + } + + /** + * Get the review context for helper functions + */ + private getReviewContext(): ReviewContext { + return { + item: this._item, + folderRepositoryManager: this._folderRepositoryManager, + existingReviewers: this._existingReviewers, + postMessage: (message: any) => this._postMessage(message), + replyMessage: (message: IRequestMessage, response: any) => this._replyMessage(message, response), + throwError: (message: IRequestMessage | undefined, error: string) => this._throwError(message, error), + getTimeline: () => this._getTimeline() + }; } private isUpdateBranchWithGitHubEnabled(): boolean { @@ -636,7 +647,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel) { - const result = await PullRequestView.deleteBranch(this._folderRepositoryManager, this._item); + const result = await PullRequestReviewCommon.deleteBranch(this._folderRepositoryManager, this._item); if (result.isReply) { this._replyMessage(message, result.message); } else { @@ -645,160 +656,48 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel): void { - this._item - .setReadyForReview() - .then(result => { - this._replyMessage(message, result); - }) - .catch(e => { - vscode.window.showErrorMessage(`Unable to set pull request ready for review. ${formatError(e)}`); - this._throwError(message, ''); - }); + private async setReadyForReview(message: IRequestMessage<{}>): Promise { + return PullRequestReviewCommon.setReadyForReview(this.getReviewContext(), message); } private async setReadyForReviewAndMerge(message: IRequestMessage<{ mergeMethod: MergeMethod }>): Promise { - try { - const readyResult = await this._item.setReadyForReview(); - - try { - await this._item.approve(this._folderRepositoryManager.repository, ''); - } catch (e) { - vscode.window.showErrorMessage(`Pull request marked as ready for review, but failed to approve. ${formatError(e)}`); - this._replyMessage(message, readyResult); - return; - } - - try { - await this._item.enableAutoMerge(message.args.mergeMethod); - } catch (e) { - vscode.window.showErrorMessage(`Pull request marked as ready and approved, but failed to enable auto-merge. ${formatError(e)}`); - this._replyMessage(message, readyResult); - return; - } - - this._replyMessage(message, readyResult); - } catch (e) { - vscode.window.showErrorMessage(`Unable to mark pull request as ready for review. ${formatError(e)}`); - this._throwError(message, ''); - } + return PullRequestReviewCommon.setReadyForReviewAndMerge(this.getReviewContext(), message); } private async readyForReviewCommand(): Promise { - this._postMessage({ - command: 'pr.readying-for-review' - }); - try { - const result = await this._item.setReadyForReview(); - - const readiedResult: ReadyForReviewReply = { - isDraft: result.isDraft - }; - await this._postMessage({ - command: 'pr.readied-for-review', - result: readiedResult - }); - } catch (e) { - vscode.window.showErrorMessage(`Unable to set pull request ready for review. ${formatError(e)}`); - this._throwError(undefined, e.message); - } + return PullRequestReviewCommon.readyForReviewCommand(this.getReviewContext()); } private async readyForReviewAndMergeCommand(context: { mergeMethod: MergeMethod }): Promise { - this._postMessage({ - command: 'pr.readying-for-review' - }); - try { - const [readyResult, approveResult] = await Promise.all([this._item.setReadyForReview(), this._item.approve(this._folderRepositoryManager.repository)]); - await this._item.enableAutoMerge(context.mergeMethod); - this.updateReviewers(approveResult); - - const readiedResult: ReadyForReviewReply = { - isDraft: readyResult.isDraft, - autoMerge: true, - reviewEvent: approveResult, - reviewers: this._existingReviewers - }; - await this._postMessage({ - command: 'pr.readied-for-review', - result: readiedResult - }); - } catch (e) { - vscode.window.showErrorMessage(`Unable to set pull request ready for review. ${formatError(e)}`); - this._throwError(undefined, e.message); - } + return PullRequestReviewCommon.readyForReviewAndMergeCommand(this.getReviewContext(), context); } private async checkoutDefaultBranch(message: IRequestMessage): Promise { - try { - const prBranch = this._folderRepositoryManager.repository.state.HEAD?.name; - await this._folderRepositoryManager.checkoutDefaultBranch(message.args); - if (prBranch) { - await this._folderRepositoryManager.cleanupAfterPullRequest(prBranch, this._item); - } - } finally { - // Complete webview promise so that button becomes enabled again - this._replyMessage(message, {}); - } - } - - private updateReviewers(review?: ReviewEvent): void { - if (review && review.state) { - const existingReviewer = this._existingReviewers.find( - reviewer => review.user.login === (reviewer.reviewer as IAccount).login, - ); - if (existingReviewer) { - existingReviewer.state = review.state; - } else { - this._existingReviewers.push({ - reviewer: review.user, - state: review.state, - }); - } - } + return PullRequestReviewCommon.checkoutDefaultBranch(this.getReviewContext(), message); } private async doReviewCommand(context: { body: string }, reviewType: ReviewType, action: (body: string) => Promise) { - const submittingMessage = { - command: 'pr.submitting-review', - lastReviewType: reviewType - }; - this._postMessage(submittingMessage); - try { - const review = await action(context.body); - this.updateReviewers(review); - const allEvents = await this._getTimeline(); - const reviewMessage: SubmitReviewReply & { command: string } = { - command: 'pr.append-review', - reviewedEvent: review, - events: allEvents, - reviewers: this._existingReviewers - }; - this.tryScheduleCopilotRefresh(review.body, review.state); - await this._postMessage(reviewMessage); - } catch (e) { - vscode.window.showErrorMessage(vscode.l10n.t('Submitting review failed. {0}', formatError(e))); - this._throwError(undefined, `${formatError(e)}`); - } finally { - this._postMessage({ command: 'pr.append-review' }); + const result = await PullRequestReviewCommon.doReviewCommand( + this.getReviewContext(), + context, + reviewType, + true, + action, + ); + if (result) { + this.tryScheduleCopilotRefresh(result.body, result.state); } } private async doReviewMessage(message: IRequestMessage, action: (body) => Promise) { - try { - const review = await action(message.args); - this.updateReviewers(review); - const allEvents = await this._getTimeline(); - const reply: SubmitReviewReply = { - reviewedEvent: review, - events: allEvents, - reviewers: this._existingReviewers, - }; - this.tryScheduleCopilotRefresh(review.body, review.state); - this._replyMessage(message, reply); - } catch (e) { - vscode.window.showErrorMessage(vscode.l10n.t('Submitting review failed. {0}', formatError(e))); - this._throwError(message, `${formatError(e)}`); + const result = await PullRequestReviewCommon.doReviewMessage( + this.getReviewContext(), + message, + true, + action, + ); + if (result) { + this.tryScheduleCopilotRefresh(result.body, result.state); } } @@ -839,33 +738,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel): void { - let targetReviewer: ReviewState | undefined; - const userReviewers: IAccount[] = []; - const teamReviewers: ITeam[] = []; - - for (const reviewer of this._existingReviewers) { - let id = reviewer.reviewer.id; - if (id && ((reviewer.state === 'REQUESTED') || (id === message.args))) { - if (id === message.args) { - targetReviewer = reviewer; - } - } - } - - if (targetReviewer && isITeam(targetReviewer.reviewer)) { - teamReviewers.push(targetReviewer.reviewer); - } else if (targetReviewer && !isITeam(targetReviewer.reviewer)) { - userReviewers.push(targetReviewer.reviewer); - } - - this._item.requestReview(userReviewers, teamReviewers, true).then(() => { - if (targetReviewer) { - targetReviewer.state = 'REQUESTED'; - } - this._replyMessage(message, { - reviewers: this._existingReviewers, - }); - }); + return PullRequestReviewCommon.reRequestReview(this.getReviewContext(), message); } private async revert(message: IRequestMessage): Promise { @@ -903,35 +776,12 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel): Promise { - if (!this.isUpdateBranchWithGitHubEnabled()) { - await vscode.window.showErrorMessage(vscode.l10n.t('The pull request branch must be checked out to be updated.'), { modal: true }); - return this._replyMessage(message, {}); - } - - if (this._folderRepositoryManager.repository.state.workingTreeChanges.length > 0 || this._folderRepositoryManager.repository.state.indexChanges.length > 0) { - await vscode.window.showErrorMessage(vscode.l10n.t('The pull request branch cannot be updated when the there changed files in the working tree or index. Stash or commit all change and then try again.'), { modal: true }); - return this._replyMessage(message, {}); - } - const mergeSucceeded = await this._folderRepositoryManager.tryMergeBaseIntoHead(this._item, true); - if (!mergeSucceeded) { - this._replyMessage(message, {}); - } - // The mergability of the PR doesn't update immediately. Poll. - let mergability = PullRequestMergeability.Unknown; - let attemptsRemaining = 5; - do { - mergability = (await this._item.getMergeability()).mergeability; - attemptsRemaining--; - await new Promise(c => setTimeout(c, 1000)); - } while (attemptsRemaining > 0 && mergability === PullRequestMergeability.Unknown); - - const result: Partial = { - events: await this._getTimeline(), - mergeable: mergability, - }; - await this.refreshPanel(); - - this._replyMessage(message, result); + return PullRequestReviewCommon.updateBranch( + this.getReviewContext(), + message, + () => this.refreshPanel(), + () => this.isUpdateBranchWithGitHubEnabled() + ); } protected override editCommentPromise(comment: IComment, text: string): Promise { diff --git a/src/github/pullRequestOverviewCommon.ts b/src/github/pullRequestOverviewCommon.ts deleted file mode 100644 index bd069dc329..0000000000 --- a/src/github/pullRequestOverviewCommon.ts +++ /dev/null @@ -1,148 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -'use strict'; - -import * as vscode from 'vscode'; -import { FolderRepositoryManager } from './folderRepositoryManager'; -import { PullRequestModel } from './pullRequestModel'; -import { - DEFAULT_DELETION_METHOD, - PR_SETTINGS_NAMESPACE, - SELECT_LOCAL_BRANCH, - SELECT_REMOTE, -} from '../common/settingKeys'; -import { Schemes } from '../common/uri'; - -export namespace PullRequestView { - export async function deleteBranch(folderRepositoryManager: FolderRepositoryManager, item: PullRequestModel): Promise<{ isReply: boolean, message: any }> { - const branchInfo = await folderRepositoryManager.getBranchNameForPullRequest(item); - const actions: (vscode.QuickPickItem & { type: 'remoteHead' | 'local' | 'remote' | 'suspend' })[] = []; - const defaultBranch = await folderRepositoryManager.getPullRequestRepositoryDefaultBranch(item); - - if (item.isResolved()) { - const branchHeadRef = item.head.ref; - const headRepo = folderRepositoryManager.findRepo(repo => repo.remote.owner === item.head.owner && repo.remote.repositoryName === item.remote.repositoryName); - - const isDefaultBranch = defaultBranch === item.head.ref; - if (!isDefaultBranch && !item.isRemoteHeadDeleted) { - actions.push({ - label: vscode.l10n.t('Delete remote branch {0}', `${headRepo?.remote.remoteName}/${branchHeadRef}`), - description: `${item.remote.normalizedHost}/${item.head.repositoryCloneUrl.owner}/${item.remote.repositoryName}`, - type: 'remoteHead', - picked: true, - }); - } - } - - if (branchInfo) { - const preferredLocalBranchDeletionMethod = vscode.workspace - .getConfiguration(PR_SETTINGS_NAMESPACE) - .get(`${DEFAULT_DELETION_METHOD}.${SELECT_LOCAL_BRANCH}`); - actions.push({ - label: vscode.l10n.t('Delete local branch {0}', branchInfo.branch), - type: 'local', - picked: !!preferredLocalBranchDeletionMethod, - }); - - const preferredRemoteDeletionMethod = vscode.workspace - .getConfiguration(PR_SETTINGS_NAMESPACE) - .get(`${DEFAULT_DELETION_METHOD}.${SELECT_REMOTE}`); - - if (branchInfo.remote && branchInfo.createdForPullRequest && !branchInfo.remoteInUse) { - actions.push({ - label: vscode.l10n.t('Delete remote {0}, which is no longer used by any other branch', branchInfo.remote), - type: 'remote', - picked: !!preferredRemoteDeletionMethod, - }); - } - } - - if (vscode.env.remoteName === 'codespaces') { - actions.push({ - label: vscode.l10n.t('Suspend Codespace'), - type: 'suspend' - }); - } - - if (!actions.length) { - vscode.window.showWarningMessage( - vscode.l10n.t('There is no longer an upstream or local branch for Pull Request #{0}', item.number), - ); - return { - isReply: true, - message: { - cancelled: true - } - }; - } - - const selectedActions = await vscode.window.showQuickPick(actions, { - canPickMany: true, - ignoreFocusOut: true, - }); - - const deletedBranchTypes: string[] = []; - - if (selectedActions) { - const isBranchActive = item.equals(folderRepositoryManager.activePullRequest) || (folderRepositoryManager.repository.state.HEAD?.name && folderRepositoryManager.repository.state.HEAD.name === branchInfo?.branch); - - const promises = selectedActions.map(async action => { - switch (action.type) { - case 'remoteHead': - await folderRepositoryManager.deleteBranch(item); - deletedBranchTypes.push(action.type); - await folderRepositoryManager.repository.fetch({ prune: true }); - // If we're in a remote repository, then we should checkout the default branch. - if (folderRepositoryManager.repository.rootUri.scheme === Schemes.VscodeVfs) { - await folderRepositoryManager.repository.checkout(defaultBranch); - } - return; - case 'local': - if (isBranchActive) { - if (folderRepositoryManager.repository.state.workingTreeChanges.length) { - const yes = vscode.l10n.t('Yes'); - const response = await vscode.window.showWarningMessage( - vscode.l10n.t('Your local changes will be lost, do you want to continue?'), - { modal: true }, - yes, - ); - if (response === yes) { - await vscode.commands.executeCommand('git.cleanAll'); - } else { - return; - } - } - await folderRepositoryManager.checkoutDefaultBranch(defaultBranch); - } - await folderRepositoryManager.repository.deleteBranch(branchInfo!.branch, true); - return deletedBranchTypes.push(action.type); - case 'remote': - deletedBranchTypes.push(action.type); - return folderRepositoryManager.repository.removeRemote(branchInfo!.remote!); - case 'suspend': - deletedBranchTypes.push(action.type); - return vscode.commands.executeCommand('github.codespaces.disconnectSuspend'); - } - }); - - await Promise.all(promises); - - return { - isReply: false, - message: { - command: 'pr.deleteBranch', - branchTypes: deletedBranchTypes - } - }; - } else { - return { - isReply: true, - message: { - cancelled: true - } - }; - } - } -} \ No newline at end of file diff --git a/src/github/pullRequestReviewCommon.ts b/src/github/pullRequestReviewCommon.ts new file mode 100644 index 0000000000..f6b76eacd1 --- /dev/null +++ b/src/github/pullRequestReviewCommon.ts @@ -0,0 +1,406 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +'use strict'; + +import * as vscode from 'vscode'; +import { FolderRepositoryManager } from './folderRepositoryManager'; +import { IAccount, isITeam, ITeam, MergeMethod, PullRequestMergeability, reviewerId, ReviewState } from './interface'; +import { PullRequestModel } from './pullRequestModel'; +import { PullRequest, ReadyForReviewReply, ReviewType, SubmitReviewReply } from './views'; +import { DEFAULT_DELETION_METHOD, PR_SETTINGS_NAMESPACE, SELECT_LOCAL_BRANCH, SELECT_REMOTE } from '../common/settingKeys'; +import { ReviewEvent, TimelineEvent } from '../common/timelineEvent'; +import { Schemes } from '../common/uri'; +import { formatError } from '../common/utils'; +import { IRequestMessage } from '../common/webview'; + +/** + * Context required by review utility functions + */ +export interface ReviewContext { + item: PullRequestModel; + folderRepositoryManager: FolderRepositoryManager; + existingReviewers: ReviewState[]; + postMessage(message: any): Promise; + replyMessage(message: IRequestMessage, response: any): void; + throwError(message: IRequestMessage | undefined, error: string): void; + getTimeline(): Promise; +} + +/** + * Utility functions for handling pull request reviews. + * These are shared between PullRequestOverviewPanel and PullRequestViewProvider. + */ +export namespace PullRequestReviewCommon { + /** + * Find currently configured user's review status for the current PR + */ + export function getCurrentUserReviewState(reviewers: ReviewState[], currentUser: IAccount): string | undefined { + const review = reviewers.find(r => reviewerId(r.reviewer) === currentUser.login); + // There will always be a review. If not then the PR shouldn't have been or fetched/shown for the current user + return review?.state; + } + + function updateReviewers(existingReviewers: ReviewState[], review?: ReviewEvent): void { + if (review && review.state) { + const existingReviewer = existingReviewers.find( + reviewer => review.user.login === reviewerId(reviewer.reviewer), + ); + if (existingReviewer) { + existingReviewer.state = review.state; + } else { + existingReviewers.push({ + reviewer: review.user, + state: review.state, + }); + } + } + } + + export async function doReviewCommand( + ctx: ReviewContext, + context: { body: string }, + reviewType: ReviewType, + needsTimelineRefresh: boolean, + action: (body: string) => Promise, + ): Promise { + const submittingMessage = { + command: 'pr.submitting-review', + lastReviewType: reviewType + }; + ctx.postMessage(submittingMessage); + try { + const review = await action(context.body); + updateReviewers(ctx.existingReviewers, review); + const allEvents = needsTimelineRefresh ? await ctx.getTimeline() : []; + const reviewMessage: SubmitReviewReply & { command: string } = { + command: 'pr.append-review', + reviewedEvent: review, + events: allEvents, + reviewers: ctx.existingReviewers + }; + await ctx.postMessage(reviewMessage); + return review; + } catch (e) { + vscode.window.showErrorMessage(vscode.l10n.t('Submitting review failed. {0}', formatError(e))); + ctx.throwError(undefined, `${formatError(e)}`); + await ctx.postMessage({ command: 'pr.append-review' }); + } + } + + export async function doReviewMessage( + ctx: ReviewContext, + message: IRequestMessage, + needsTimelineRefresh: boolean, + action: (body: string) => Promise, + ): Promise { + try { + const review = await action(message.args); + updateReviewers(ctx.existingReviewers, review); + const allEvents = needsTimelineRefresh ? await ctx.getTimeline() : []; + const reply: SubmitReviewReply = { + reviewedEvent: review, + events: allEvents, + reviewers: ctx.existingReviewers, + }; + ctx.replyMessage(message, reply); + return review; + } catch (e) { + vscode.window.showErrorMessage(vscode.l10n.t('Submitting review failed. {0}', formatError(e))); + ctx.throwError(message, `${formatError(e)}`); + } + } + + export function reRequestReview(ctx: ReviewContext, message: IRequestMessage): void { + let targetReviewer: ReviewState | undefined; + const userReviewers: IAccount[] = []; + const teamReviewers: ITeam[] = []; + + for (const reviewer of ctx.existingReviewers) { + let id = reviewer.reviewer.id; + if (id && ((reviewer.state === 'REQUESTED') || (id === message.args))) { + if (id === message.args) { + targetReviewer = reviewer; + } + } + } + + if (targetReviewer && isITeam(targetReviewer.reviewer)) { + teamReviewers.push(targetReviewer.reviewer); + } else if (targetReviewer && !isITeam(targetReviewer.reviewer)) { + userReviewers.push(targetReviewer.reviewer); + } + + ctx.item.requestReview(userReviewers, teamReviewers, true).then(() => { + if (targetReviewer) { + targetReviewer.state = 'REQUESTED'; + } + ctx.replyMessage(message, { + reviewers: ctx.existingReviewers, + }); + }); + } + + export async function checkoutDefaultBranch(ctx: ReviewContext, message: IRequestMessage): Promise { + try { + const prBranch = ctx.folderRepositoryManager.repository.state.HEAD?.name; + await ctx.folderRepositoryManager.checkoutDefaultBranch(message.args); + if (prBranch) { + await ctx.folderRepositoryManager.cleanupAfterPullRequest(prBranch, ctx.item); + } + } finally { + // Complete webview promise so that button becomes enabled again + ctx.replyMessage(message, {}); + } + } + + export async function updateBranch( + ctx: ReviewContext, + message: IRequestMessage, + refreshAfterUpdate: () => Promise, + checkUpdateEnabled?: () => boolean + ): Promise { + if (checkUpdateEnabled && !checkUpdateEnabled()) { + await vscode.window.showErrorMessage(vscode.l10n.t('The pull request branch must be checked out to be updated.'), { modal: true }); + return ctx.replyMessage(message, {}); + } + + if (ctx.folderRepositoryManager.repository.state.workingTreeChanges.length > 0 || ctx.folderRepositoryManager.repository.state.indexChanges.length > 0) { + await vscode.window.showErrorMessage(vscode.l10n.t('The pull request branch cannot be updated when the there changed files in the working tree or index. Stash or commit all change and then try again.'), { modal: true }); + return ctx.replyMessage(message, {}); + } + const mergeSucceeded = await ctx.folderRepositoryManager.tryMergeBaseIntoHead(ctx.item, true); + if (!mergeSucceeded) { + ctx.replyMessage(message, {}); + } + // The mergability of the PR doesn't update immediately. Poll. + let mergability = PullRequestMergeability.Unknown; + let attemptsRemaining = 5; + do { + mergability = (await ctx.item.getMergeability()).mergeability; + attemptsRemaining--; + await new Promise(c => setTimeout(c, 1000)); + } while (attemptsRemaining > 0 && mergability === PullRequestMergeability.Unknown); + + const result: Partial = { + events: await ctx.getTimeline(), + mergeable: mergability, + }; + await refreshAfterUpdate(); + + ctx.replyMessage(message, result); + } + + export async function setReadyForReview(ctx: ReviewContext, message: IRequestMessage<{}>): Promise { + try { + const result = await ctx.item.setReadyForReview(); + ctx.replyMessage(message, result); + } catch (e) { + vscode.window.showErrorMessage(vscode.l10n.t('Unable to set pull request ready for review. {0}', formatError(e))); + ctx.throwError(message, ''); + } + } + + export async function setReadyForReviewAndMerge(ctx: ReviewContext, message: IRequestMessage<{ mergeMethod: MergeMethod }>): Promise { + try { + const readyResult = await ctx.item.setReadyForReview(); + + try { + await ctx.item.approve(ctx.folderRepositoryManager.repository, ''); + } catch (e) { + vscode.window.showErrorMessage(`Pull request marked as ready for review, but failed to approve. ${formatError(e)}`); + ctx.replyMessage(message, readyResult); + return; + } + + try { + await ctx.item.enableAutoMerge(message.args.mergeMethod); + } catch (e) { + vscode.window.showErrorMessage(`Pull request marked as ready and approved, but failed to enable auto-merge. ${formatError(e)}`); + ctx.replyMessage(message, readyResult); + return; + } + + ctx.replyMessage(message, readyResult); + } catch (e) { + vscode.window.showErrorMessage(`Unable to mark pull request as ready for review. ${formatError(e)}`); + ctx.throwError(message, ''); + } + } + + export async function readyForReviewCommand(ctx: ReviewContext): Promise { + ctx.postMessage({ + command: 'pr.readying-for-review' + }); + try { + const result = await ctx.item.setReadyForReview(); + + const readiedResult: ReadyForReviewReply = { + isDraft: result.isDraft + }; + await ctx.postMessage({ + command: 'pr.readied-for-review', + result: readiedResult + }); + } catch (e) { + vscode.window.showErrorMessage(`Unable to set pull request ready for review. ${formatError(e)}`); + ctx.throwError(undefined, e.message); + } + } + + export async function readyForReviewAndMergeCommand(ctx: ReviewContext, context: { mergeMethod: MergeMethod }): Promise { + ctx.postMessage({ + command: 'pr.readying-for-review' + }); + try { + const [readyResult, approveResult] = await Promise.all([ctx.item.setReadyForReview(), ctx.item.approve(ctx.folderRepositoryManager.repository)]); + await ctx.item.enableAutoMerge(context.mergeMethod); + updateReviewers(ctx.existingReviewers, approveResult); + + const readiedResult: ReadyForReviewReply = { + isDraft: readyResult.isDraft, + autoMerge: true, + reviewEvent: approveResult, + reviewers: ctx.existingReviewers + }; + await ctx.postMessage({ + command: 'pr.readied-for-review', + result: readiedResult + }); + } catch (e) { + vscode.window.showErrorMessage(`Unable to set pull request ready for review. ${formatError(e)}`); + ctx.throwError(undefined, e.message); + } + } + + export async function deleteBranch(folderRepositoryManager: FolderRepositoryManager, item: PullRequestModel): Promise<{ isReply: boolean, message: any }> { + const branchInfo = await folderRepositoryManager.getBranchNameForPullRequest(item); + const actions: (vscode.QuickPickItem & { type: 'remoteHead' | 'local' | 'remote' | 'suspend' })[] = []; + const defaultBranch = await folderRepositoryManager.getPullRequestRepositoryDefaultBranch(item); + + if (item.isResolved()) { + const branchHeadRef = item.head.ref; + const headRepo = folderRepositoryManager.findRepo(repo => repo.remote.owner === item.head.owner && repo.remote.repositoryName === item.remote.repositoryName); + + const isDefaultBranch = defaultBranch === item.head.ref; + if (!isDefaultBranch && !item.isRemoteHeadDeleted) { + actions.push({ + label: vscode.l10n.t('Delete remote branch {0}', `${headRepo?.remote.remoteName}/${branchHeadRef}`), + description: `${item.remote.normalizedHost}/${item.head.repositoryCloneUrl.owner}/${item.remote.repositoryName}`, + type: 'remoteHead', + picked: true, + }); + } + } + + if (branchInfo) { + const preferredLocalBranchDeletionMethod = vscode.workspace + .getConfiguration(PR_SETTINGS_NAMESPACE) + .get(`${DEFAULT_DELETION_METHOD}.${SELECT_LOCAL_BRANCH}`); + actions.push({ + label: vscode.l10n.t('Delete local branch {0}', branchInfo.branch), + type: 'local', + picked: !!preferredLocalBranchDeletionMethod, + }); + + const preferredRemoteDeletionMethod = vscode.workspace + .getConfiguration(PR_SETTINGS_NAMESPACE) + .get(`${DEFAULT_DELETION_METHOD}.${SELECT_REMOTE}`); + + if (branchInfo.remote && branchInfo.createdForPullRequest && !branchInfo.remoteInUse) { + actions.push({ + label: vscode.l10n.t('Delete remote {0}, which is no longer used by any other branch', branchInfo.remote), + type: 'remote', + picked: !!preferredRemoteDeletionMethod, + }); + } + } + + if (vscode.env.remoteName === 'codespaces') { + actions.push({ + label: vscode.l10n.t('Suspend Codespace'), + type: 'suspend' + }); + } + + if (!actions.length) { + vscode.window.showWarningMessage( + vscode.l10n.t('There is no longer an upstream or local branch for Pull Request #{0}', item.number), + ); + return { + isReply: true, + message: { + cancelled: true + } + }; + } + + const selectedActions = await vscode.window.showQuickPick(actions, { + canPickMany: true, + ignoreFocusOut: true, + }); + + const deletedBranchTypes: string[] = []; + + if (selectedActions) { + const isBranchActive = item.equals(folderRepositoryManager.activePullRequest) || (folderRepositoryManager.repository.state.HEAD?.name && folderRepositoryManager.repository.state.HEAD.name === branchInfo?.branch); + + const promises = selectedActions.map(async action => { + switch (action.type) { + case 'remoteHead': + await folderRepositoryManager.deleteBranch(item); + deletedBranchTypes.push(action.type); + await folderRepositoryManager.repository.fetch({ prune: true }); + // If we're in a remote repository, then we should checkout the default branch. + if (folderRepositoryManager.repository.rootUri.scheme === Schemes.VscodeVfs) { + await folderRepositoryManager.repository.checkout(defaultBranch); + } + return; + case 'local': + if (isBranchActive) { + if (folderRepositoryManager.repository.state.workingTreeChanges.length) { + const yes = vscode.l10n.t('Yes'); + const response = await vscode.window.showWarningMessage( + vscode.l10n.t('Your local changes will be lost, do you want to continue?'), + { modal: true }, + yes, + ); + if (response === yes) { + await vscode.commands.executeCommand('git.cleanAll'); + } else { + return; + } + } + await folderRepositoryManager.checkoutDefaultBranch(defaultBranch); + } + await folderRepositoryManager.repository.deleteBranch(branchInfo!.branch, true); + return deletedBranchTypes.push(action.type); + case 'remote': + deletedBranchTypes.push(action.type); + return folderRepositoryManager.repository.removeRemote(branchInfo!.remote!); + case 'suspend': + deletedBranchTypes.push(action.type); + return vscode.commands.executeCommand('github.codespaces.disconnectSuspend'); + } + }); + + await Promise.all(promises); + + return { + isReply: false, + message: { + command: 'pr.deleteBranch', + branchTypes: deletedBranchTypes + } + }; + } else { + return { + isReply: true, + message: { + cancelled: true + } + }; + } + } +} From 357fac1a14b1b6771fdf811dd44899efdf32bd6c Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Mon, 17 Nov 2025 13:48:17 +0100 Subject: [PATCH 066/402] Fix only one reviewer on PR page (#8153) * Fix only one reviewer on PR page * Tets --- src/github/graphql.ts | 4 +- src/test/github/graphql.test.ts | 141 ++++++++++++++++++++++++++++++++ 2 files changed, 143 insertions(+), 2 deletions(-) create mode 100644 src/test/github/graphql.test.ts diff --git a/src/github/graphql.ts b/src/github/graphql.ts index f1dddf0b8b..68f41b1ded 100644 --- a/src/github/graphql.ts +++ b/src/github/graphql.ts @@ -122,12 +122,12 @@ export interface Account extends Actor { export function isAccount(x: Actor | Team | Node | undefined | null): x is Account { const asAccount = x as Partial; - return !!asAccount && !!asAccount?.name && (asAccount?.email !== undefined); + return !!asAccount && (asAccount?.name !== undefined) && (asAccount?.email !== undefined); } export function isTeam(x: Actor | Team | Node | undefined | null): x is Team { const asTeam = x as Partial; - return !!asTeam && !!asTeam?.slug; + return !!asTeam && (asTeam?.slug !== undefined); } export interface Team { diff --git a/src/test/github/graphql.test.ts b/src/test/github/graphql.test.ts new file mode 100644 index 0000000000..cb0bacfb01 --- /dev/null +++ b/src/test/github/graphql.test.ts @@ -0,0 +1,141 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { isAccount, isTeam, Actor, Account, Team, Node } from '../../github/graphql'; + +describe('graphql type guards', () => { + + describe('isAccount', () => { + it('returns true for a valid Account', () => { + const account: Account = { + __typename: 'User', + id: 'acct1', + login: 'alice', + avatarUrl: 'https://example.com/a.png', + url: 'https://example.com/alice', + name: 'Alice', + email: 'alice@example.com' + }; + assert.strictEqual(isAccount(account), true); + }); + + it('returns false for Actor missing name/email', () => { + const actor: Actor = { + __typename: 'User', + id: 'act1', + login: 'bob', + avatarUrl: 'https://example.com/b.png', + url: 'https://example.com/bob' + }; + assert.strictEqual(isAccount(actor), false); + }); + + it('returns false for Team object', () => { + const team: Team = { + avatarUrl: 'https://example.com/t.png', + name: 'Dev Team', + url: 'https://example.com/team', + repositories: { nodes: [] }, + slug: 'dev-team', + id: 'team1' + }; + assert.strictEqual(isAccount(team), false); + }); + + it('returns false for Node object', () => { + const node: Node = { id: 'node1' }; + assert.strictEqual(isAccount(node), false); + }); + + it('returns false for null and undefined', () => { + assert.strictEqual(isAccount(null), false); + assert.strictEqual(isAccount(undefined), false); + }); + + it('returns true when name and email are null', () => { + const obj: any = { + __typename: 'User', id: 'null1', login: 'nullUser', avatarUrl: '', url: '', name: null, email: null + }; + assert.strictEqual(isAccount(obj), true); + }); + + it('returns true when name is null but email present', () => { + const obj: any = { + __typename: 'User', id: 'null2', login: 'nullName', avatarUrl: '', url: '', name: null, email: 'e@example.com' + }; + assert.strictEqual(isAccount(obj), true); + }); + + it('returns false when email or name is undefined', () => { + const obj: any = { + __typename: 'User', id: 'null3', login: 'nullEmail', avatarUrl: '', url: '', name: undefined, email: undefined + }; + assert.strictEqual(isAccount(obj), false); + }); + }); + + describe('isTeam', () => { + it('returns true for a valid Team', () => { + const team: Team = { + avatarUrl: 'https://example.com/t.png', + name: 'Engineering', + url: 'https://example.com/eng', + repositories: { nodes: [] }, + slug: 'engineering', + id: 'team2' + }; + assert.strictEqual(isTeam(team), true); + }); + + it('returns false for Account object', () => { + const account: Account = { + __typename: 'User', + id: 'acct2', + login: 'carol', + avatarUrl: 'https://example.com/c.png', + url: 'https://example.com/carol', + name: 'Carol', + email: 'carol@example.com' + }; + assert.strictEqual(isTeam(account), false); + }); + + it('returns false for Actor without slug', () => { + const actor: Actor = { + __typename: 'User', + id: 'act2', + login: 'dave', + avatarUrl: 'https://example.com/d.png', + url: 'https://example.com/dave' + }; + assert.strictEqual(isTeam(actor), false); + }); + + it('returns false for Node object', () => { + const node: Node = { id: 'node2' }; + assert.strictEqual(isTeam(node), false); + }); + + it('returns false for null and undefined', () => { + assert.strictEqual(isTeam(null), false); + assert.strictEqual(isTeam(undefined), false); + }); + + it('returns false when slug is undefined', () => { + const obj: any = { + avatarUrl: '', name: 'Team', url: '', repositories: { nodes: [] }, slug: undefined, id: 'tslugnull' + }; + assert.strictEqual(isTeam(obj), false); + }); + it('returns true when slug is null', () => { + const obj: any = { + avatarUrl: '', name: 'Team', url: '', repositories: { nodes: [] }, slug: null, id: 'tslugnull' + }; + assert.strictEqual(isTeam(obj), true); + }); + }); +}); + From ccdbe08985dd3e93b70ebae557d57841d3adeaba Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Mon, 17 Nov 2025 13:48:33 +0100 Subject: [PATCH 067/402] Fix dropdown in PR header (#8156) --- package.json | 4 ++++ src/commands.ts | 11 ++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 37f7c53366..5df7f84235 100644 --- a/package.json +++ b/package.json @@ -3586,6 +3586,10 @@ "command": "pr.checkoutOnVscodeDevFromDescription", "group": "checkout@1", "when": "webviewId == PullRequestOverview && github:checkoutMenu" + }, + { + "command": "pr.openSessionLogFromDescription", + "when": "webviewId == PullRequestOverview && github:codingAgentMenu" } ], "chat/chatSessions": [ diff --git a/src/commands.ts b/src/commands.ts index 1bd7fdb68d..ddb34e8736 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -14,12 +14,13 @@ import Logger from './common/logger'; import { FILE_LIST_LAYOUT, HIDE_VIEWED_FILES, PR_SETTINGS_NAMESPACE } from './common/settingKeys'; import { editQuery } from './common/settingsUtils'; import { ITelemetry } from './common/telemetry'; +import { SessionLinkInfo } from './common/timelineEvent'; import { asTempStorageURI, fromPRUri, fromReviewUri, Schemes, toPRUri } from './common/uri'; import { formatError } from './common/utils'; import { EXTENSION_ID } from './constants'; import { ICopilotRemoteAgentCommandArgs } from './github/common'; import { ChatSessionWithPR, CrossChatSessionWithPR } from './github/copilotApi'; -import { CopilotRemoteAgentManager } from './github/copilotRemoteAgent'; +import { CopilotRemoteAgentManager, SessionIdForPr } from './github/copilotRemoteAgent'; import { FolderRepositoryManager } from './github/folderRepositoryManager'; import { GitHubRepository } from './github/githubRepository'; import { Issue } from './github/interface'; @@ -724,6 +725,14 @@ export function registerCommands( return vscode.env.openExternal(vscode.Uri.parse(vscodeDevPrLink(resolved.pr))); })); + context.subscriptions.push(vscode.commands.registerCommand('pr.openSessionLogFromDescription', async (context: SessionLinkInfo | undefined) => { + if (!context) { + return vscode.window.showErrorMessage(vscode.l10n.t('No pull request context provided for checkout.')); + } + const resource = SessionIdForPr.getResource(context.pullNumber, context.sessionIndex); + return vscode.commands.executeCommand('vscode.open', resource); + })); + context.subscriptions.push( vscode.commands.registerCommand('pr.exit', async (pr: PRNode | RepositoryChangesNode | PullRequestModel | undefined) => { let pullRequestModel: PullRequestModel | undefined; From 69ec8339f267b6145a91766d209cbbd2e1491180 Mon Sep 17 00:00:00 2001 From: Vignesh Date: Mon, 17 Nov 2025 18:19:02 +0530 Subject: [PATCH 068/402] feat: Add copy comment link button in PR overview (#8150) --- webviews/components/comment.tsx | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/webviews/components/comment.tsx b/webviews/components/comment.tsx index bfd5e63286..76fe901207 100644 --- a/webviews/components/comment.tsx +++ b/webviews/components/comment.tsx @@ -5,7 +5,7 @@ import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; import { ContextDropdown } from './contextDropdown'; -import { editIcon, quoteIcon, trashIcon } from './icon'; +import { copyIcon, editIcon, quoteIcon, trashIcon } from './icon'; import { nbsp, Spaced } from './space'; import { Timestamp } from './timestamp'; import { AuthorLink, Avatar } from './user'; @@ -48,6 +48,7 @@ export function CommentView(commentProps: Props) { const currentDraft = pr?.pendingCommentDrafts && pr.pendingCommentDrafts[id]; const [inEditMode, setEditMode] = useState(!!currentDraft); const [showActionBar, setShowActionBar] = useState(false); + const commentUrl = (comment as Partial).htmlUrl || (comment as PullRequest).url; if (inEditMode) { return React.cloneElement(headerInEditMode ? : <>, {}, [ @@ -96,6 +97,15 @@ export function CommentView(commentProps: Props) { > {quoteIcon} + {commentUrl ? ( + + ) : null} {canEdit ? (