From 64476867923ada4dccd74187353cfe55bd376e90 Mon Sep 17 00:00:00 2001 From: mohamedamara1 Date: Sat, 26 Apr 2025 22:46:04 +0100 Subject: [PATCH 1/6] the closingIssues are are received successfully in the sidebar props --- src/github/githubRepository.ts | 1 + src/github/graphql.ts | 8 ++++ src/github/interface.ts | 1 + src/github/pullRequestModel.ts | 3 ++ src/github/pullRequestOverview.ts | 3 +- src/github/queries.gql | 7 ++++ src/github/utils.ts | 15 ++++++++ src/github/views.ts | 1 + webviews/components/sidebar.tsx | 38 ++++++++++++++++++- .../editorWebview/test/builder/pullRequest.ts | 1 + 10 files changed, 75 insertions(+), 3 deletions(-) diff --git a/src/github/githubRepository.ts b/src/github/githubRepository.ts index 39b7d274b1..348c989e71 100644 --- a/src/github/githubRepository.ts +++ b/src/github/githubRepository.ts @@ -988,6 +988,7 @@ export class GitHubRepository extends Disposable { number: id, }, }, true); + console.log('data:', data); if (data.repository === null) { Logger.error('Unexpected null repository when getting PR', this.id); return; diff --git a/src/github/graphql.ts b/src/github/graphql.ts index 8959383941..0f2f613f4a 100644 --- a/src/github/graphql.ts +++ b/src/github/graphql.ts @@ -632,6 +632,14 @@ export interface PullRequest extends Issue { viewerCanDisableAutoMerge: boolean; isDraft?: boolean; suggestedReviewers: SuggestedReviewerResponse[]; + closingIssuesReferences?: { + nodes: { + id: number, + title: string, + number: number + + }[]; + }; } export enum DefaultCommitTitle { diff --git a/src/github/interface.ts b/src/github/interface.ts index 71e4f156be..0e93aa1a45 100644 --- a/src/github/interface.ts +++ b/src/github/interface.ts @@ -223,6 +223,7 @@ export interface PullRequest extends Issue { mergeCommitMeta?: { title: string, description: string }; squashCommitMeta?: { title: string, description: string }; suggestedReviewers?: ISuggestedReviewer[]; + closingIssues?: Pick[] hasComments?: boolean; } diff --git a/src/github/pullRequestModel.ts b/src/github/pullRequestModel.ts index a11d43e7af..298a452777 100644 --- a/src/github/pullRequestModel.ts +++ b/src/github/pullRequestModel.ts @@ -57,6 +57,7 @@ import { IGitTreeItem, IRawFileChange, IRawFileContent, + Issue, ISuggestedReviewer, ITeam, MergeMethod, @@ -121,6 +122,7 @@ export class PullRequestModel extends IssueModel implements IPullRe public conflicts?: string[]; public suggestedReviewers?: ISuggestedReviewer[]; public hasChangesSinceLastReview?: boolean; + public closingIssues: Pick[]; private _showChangesSinceReview: boolean; private _hasPendingReview: boolean = false; private _onDidChangePendingReviewState: vscode.EventEmitter = new vscode.EventEmitter(); @@ -248,6 +250,7 @@ export class PullRequestModel extends IssueModel implements IPullRe super.update(item); this.isDraft = item.isDraft; this.suggestedReviewers = item.suggestedReviewers; + this.closingIssues = item.closingIssues ?? []; if (item.isRemoteHeadDeleted != null) { this.isRemoteHeadDeleted = item.isRemoteHeadDeleted; diff --git a/src/github/pullRequestOverview.ts b/src/github/pullRequestOverview.ts index 62657bd527..961fbb6f75 100644 --- a/src/github/pullRequestOverview.ts +++ b/src/github/pullRequestOverview.ts @@ -283,7 +283,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel | undefined +): Array<{ id: number, number: number, title: string }> { + if (!closingIssuesReferences) { + return []; + } + + return closingIssuesReferences.map(issue => ({ + id: issue.id, + number: issue.number, + title: issue.title + })); +} + /** * Used for case insensitive sort by login */ diff --git a/src/github/views.ts b/src/github/views.ts index 0f261208f2..5e06c60cee 100644 --- a/src/github/views.ts +++ b/src/github/views.ts @@ -96,6 +96,7 @@ export interface PullRequest extends Issue { lastReviewType?: ReviewType; revertable?: boolean; busy?: boolean; + closingIssues: Pick[]; } export interface ProjectItemsReply { diff --git a/webviews/components/sidebar.tsx b/webviews/components/sidebar.tsx index eeb5d6cf24..84e22caf08 100644 --- a/webviews/components/sidebar.tsx +++ b/webviews/components/sidebar.tsx @@ -14,7 +14,7 @@ import { AuthorLink, Avatar } from '../components/user'; import { closeIcon, copilotIcon, settingsIcon } from './icon'; import { Reviewer } from './reviewer'; -export default function Sidebar({ reviewers, labels, hasWritePermission, isIssue, projectItems: projects, milestone, assignees, canAssignCopilot }: PullRequest) { +export default function Sidebar({ reviewers, labels, closingIssues, hasWritePermission, isIssue, projectItems: projects, milestone, assignees, canAssignCopilot }: PullRequest) { const { addReviewers, addAssignees, @@ -199,9 +199,32 @@ export default function Sidebar({ reviewers, labels, hasWritePermission, isIssue {milestone ? ( ) : ( -
No milestone
+ <> +
No milestoooone
+ + )} + {/*
+
{ + const newMilestone = await addMilestone(); + updatePR({ milestone: newMilestone.added }); + }}> +
Linked issues
+
+ {closingIssues.length ? ( +
+ {closingIssues.map(issue => ( + + ))} +
+ ) : ( +
None yet
+ )} +
*/} ); } @@ -273,3 +296,14 @@ function Project(project: IProjectItem & { canDelete: boolean }) { ); } + +function Issue(issue: any) { + return ( +
+
+

{issue.title}

+ {/* Add more details about the issue as needed */} +
+
+ ); +} diff --git a/webviews/editorWebview/test/builder/pullRequest.ts b/webviews/editorWebview/test/builder/pullRequest.ts index af776ab9a2..9ba3703cf0 100644 --- a/webviews/editorWebview/test/builder/pullRequest.ts +++ b/webviews/editorWebview/test/builder/pullRequest.ts @@ -57,5 +57,6 @@ export const PullRequestBuilder = createBuilderClass()({ hasReviewDraft: { default: false }, busy: { default: undefined }, lastReviewType: { default: undefined }, + closingIssues: { default: [] }, canAssignCopilot: { default: false }, }); From d714138c62adcbc6f09f4e510cb767fcd49a7008 Mon Sep 17 00:00:00 2001 From: mohamedamara1 Date: Sat, 26 Apr 2025 22:53:42 +0100 Subject: [PATCH 2/6] removed console.log --- src/github/githubRepository.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/github/githubRepository.ts b/src/github/githubRepository.ts index 348c989e71..39b7d274b1 100644 --- a/src/github/githubRepository.ts +++ b/src/github/githubRepository.ts @@ -988,7 +988,6 @@ export class GitHubRepository extends Disposable { number: id, }, }, true); - console.log('data:', data); if (data.repository === null) { Logger.error('Unexpected null repository when getting PR', this.id); return; From dc5e5584017269b0b8fba5198262a4529a7e980f Mon Sep 17 00:00:00 2001 From: mohamedamara1 Date: Sun, 27 Apr 2025 00:31:12 +0100 Subject: [PATCH 3/6] the closing issues (linked issues) are listed in the UI; only the icons are missing --- src/github/graphql.ts | 4 +-- src/github/interface.ts | 2 +- src/github/pullRequestModel.ts | 9 ++--- src/github/queries.gql | 1 + src/github/utils.ts | 8 +++-- src/github/views.ts | 2 +- webviews/components/sidebar.tsx | 64 +++++++++++++++++++-------------- 7 files changed, 52 insertions(+), 38 deletions(-) diff --git a/src/github/graphql.ts b/src/github/graphql.ts index 0f2f613f4a..632083453b 100644 --- a/src/github/graphql.ts +++ b/src/github/graphql.ts @@ -636,8 +636,8 @@ export interface PullRequest extends Issue { nodes: { id: number, title: string, - number: number - + number: number, + state: 'CLOSED' | 'OPEN' }[]; }; } diff --git a/src/github/interface.ts b/src/github/interface.ts index 0e93aa1a45..2723bf7120 100644 --- a/src/github/interface.ts +++ b/src/github/interface.ts @@ -223,7 +223,7 @@ export interface PullRequest extends Issue { mergeCommitMeta?: { title: string, description: string }; squashCommitMeta?: { title: string, description: string }; suggestedReviewers?: ISuggestedReviewer[]; - closingIssues?: Pick[] + closingIssues?: Pick[] hasComments?: boolean; } diff --git a/src/github/pullRequestModel.ts b/src/github/pullRequestModel.ts index 298a452777..ec663dad50 100644 --- a/src/github/pullRequestModel.ts +++ b/src/github/pullRequestModel.ts @@ -57,7 +57,6 @@ import { IGitTreeItem, IRawFileChange, IRawFileContent, - Issue, ISuggestedReviewer, ITeam, MergeMethod, @@ -122,7 +121,7 @@ export class PullRequestModel extends IssueModel implements IPullRe public conflicts?: string[]; public suggestedReviewers?: ISuggestedReviewer[]; public hasChangesSinceLastReview?: boolean; - public closingIssues: Pick[]; + public closingIssues: Pick[]; private _showChangesSinceReview: boolean; private _hasPendingReview: boolean = false; private _onDidChangePendingReviewState: vscode.EventEmitter = new vscode.EventEmitter(); @@ -250,8 +249,10 @@ export class PullRequestModel extends IssueModel implements IPullRe super.update(item); this.isDraft = item.isDraft; this.suggestedReviewers = item.suggestedReviewers; - this.closingIssues = item.closingIssues ?? []; - + this.closingIssues = (item.closingIssues ?? []).map(issue => ({ + ...issue, + state: issue.state as GithubItemStateEnum + })); if (item.isRemoteHeadDeleted != null) { this.isRemoteHeadDeleted = item.isRemoteHeadDeleted; } diff --git a/src/github/queries.gql b/src/github/queries.gql index 8625f479ed..357d33a1a2 100644 --- a/src/github/queries.gql +++ b/src/github/queries.gql @@ -209,6 +209,7 @@ fragment PullRequestFragment on PullRequest { id number title + state } } merged diff --git a/src/github/utils.ts b/src/github/utils.ts index e062677500..29b32ba996 100644 --- a/src/github/utils.ts +++ b/src/github/utils.ts @@ -26,6 +26,7 @@ import { GitHubRepository, ViewerPermission } from './githubRepository'; import * as GraphQL from './graphql'; import { AccountType, + GithubItemStateEnum, IAccount, IActor, IGitHubRef, @@ -924,8 +925,8 @@ function parseSuggestedReviewers( } function parseClosingIssuesReferences( - closingIssuesReferences: Array<{ id: number, number: number, title: string }> | undefined -): Array<{ id: number, number: number, title: string }> { + closingIssuesReferences: Array<{ id: number, number: number, title: string, state: 'CLOSED' | 'OPEN' }> | undefined +): Array<{ id: number, number: number, title: string, state: GithubItemStateEnum }> { if (!closingIssuesReferences) { return []; } @@ -933,7 +934,8 @@ function parseClosingIssuesReferences( return closingIssuesReferences.map(issue => ({ id: issue.id, number: issue.number, - title: issue.title + title: issue.title, + state: issue.state as GithubItemStateEnum })); } diff --git a/src/github/views.ts b/src/github/views.ts index 5e06c60cee..70ebc0fab4 100644 --- a/src/github/views.ts +++ b/src/github/views.ts @@ -96,7 +96,7 @@ export interface PullRequest extends Issue { lastReviewType?: ReviewType; revertable?: boolean; busy?: boolean; - closingIssues: Pick[]; + closingIssues: Pick[]; } export interface ProjectItemsReply { diff --git a/webviews/components/sidebar.tsx b/webviews/components/sidebar.tsx index 84e22caf08..fd0aa8420e 100644 --- a/webviews/components/sidebar.tsx +++ b/webviews/components/sidebar.tsx @@ -6,7 +6,7 @@ import React, { useContext } from 'react'; import { COPILOT_LOGINS } from '../../src/common/copilot'; import { gitHubLabelColor } from '../../src/common/utils'; -import { IMilestone, IProjectItem, reviewerId } from '../../src/github/interface'; +import { IMilestone, IProjectItem, Issue, reviewerId, } from '../../src/github/interface'; import { PullRequest } from '../../src/github/views'; import PullRequestContext from '../common/context'; import { Label } from '../common/label'; @@ -54,7 +54,7 @@ export default function Sidebar({ reviewers, labels, closingIssues, hasWritePerm {reviewers && reviewers.length ? ( reviewers.map(state => ( - + )) ) : (
None yet
@@ -199,32 +199,29 @@ export default function Sidebar({ reviewers, labels, closingIssues, hasWritePerm {milestone ? ( ) : ( - <> -
No milestoooone
- - + <> +
No milestone
+ )} - {/*
-
{ - const newMilestone = await addMilestone(); - updatePR({ milestone: newMilestone.added }); - }}> -
Linked issues
+
+
+
Linked Issues
- {closingIssues.length ? ( -
+ {closingIssues.length > 0 ? ( +
{closingIssues.map(issue => ( - +
+
+ +
+
))}
) : ( -
None yet
+
None yet
)} -
*/} +
); } @@ -297,13 +294,26 @@ function Project(project: IProjectItem & { canDelete: boolean }) { ); } -function Issue(issue: any) { +function IssueItem({ issue }: { issue: Pick }) { return ( -
-
-

{issue.title}

- {/* Add more details about the issue as needed */} -
-
+ <> + + {issue.title} + + ); } + +function IssueStateIcon({ state }: { state: string }) { + const normalizedState = state.toLowerCase().trim(); + + switch (normalizedState) { + case 'open': + return settingsIcon; + case 'closed': + return closeIcon; + default: + return closeIcon; + } +} + From 41842abf73b9a103d98882ccb0d373154918cb84 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Wed, 7 May 2025 12:55:34 +0200 Subject: [PATCH 4/6] Refactor out state and an `IssueReference` --- src/github/interface.ts | 9 ++++++++- src/github/issueModel.ts | 8 ++------ src/github/pullRequestModel.ts | 18 ++++++------------ src/github/utils.ts | 14 ++++++++++++-- 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/github/interface.ts b/src/github/interface.ts index 2723bf7120..d388af9be7 100644 --- a/src/github/interface.ts +++ b/src/github/interface.ts @@ -204,6 +204,13 @@ export interface Issue { reactionCount: number; } +export interface IssueReference { + id: number; + number: number; + title: string; + state: GithubItemStateEnum; +} + export interface PullRequest extends Issue { isDraft?: boolean; isRemoteHeadDeleted?: boolean; @@ -223,7 +230,7 @@ export interface PullRequest extends Issue { mergeCommitMeta?: { title: string, description: string }; squashCommitMeta?: { title: string, description: string }; suggestedReviewers?: ISuggestedReviewer[]; - closingIssues?: Pick[] + closingIssues?: IssueReference[] hasComments?: boolean; } diff --git a/src/github/issueModel.ts b/src/github/issueModel.ts index 39e903e374..8a6a58d172 100644 --- a/src/github/issueModel.ts +++ b/src/github/issueModel.ts @@ -18,7 +18,7 @@ import { UpdateIssueResponse, } from './graphql'; import { GithubItemStateEnum, IAccount, IIssueEditData, IMilestone, IProject, IProjectItem, Issue } from './interface'; -import { parseGraphQlIssueComment, parseGraphQLTimelineEvents } from './utils'; +import { parseGraphQlIssueComment, parseGraphQLTimelineEvents, parsePullRequestState } from './utils'; export class IssueModel { static ID = 'IssueModel'; @@ -104,11 +104,7 @@ export class IssueModel { } protected updateState(state: string) { - if (state.toLowerCase() === 'open') { - this.state = GithubItemStateEnum.Open; - } else { - this.state = GithubItemStateEnum.Closed; - } + this.state = parsePullRequestState(state); } update(issue: TItem): void { diff --git a/src/github/pullRequestModel.ts b/src/github/pullRequestModel.ts index ec663dad50..19cb2ae19b 100644 --- a/src/github/pullRequestModel.ts +++ b/src/github/pullRequestModel.ts @@ -57,6 +57,7 @@ import { IGitTreeItem, IRawFileChange, IRawFileContent, + IssueReference, ISuggestedReviewer, ITeam, MergeMethod, @@ -83,6 +84,7 @@ import { parseGraphQLTimelineEvents, parseMergeability, parseMergeQueueEntry, + parsePullRequestState, restPaginate, } from './utils'; @@ -121,7 +123,7 @@ export class PullRequestModel extends IssueModel implements IPullRe public conflicts?: string[]; public suggestedReviewers?: ISuggestedReviewer[]; public hasChangesSinceLastReview?: boolean; - public closingIssues: Pick[]; + public closingIssues: IssueReference[]; private _showChangesSinceReview: boolean; private _hasPendingReview: boolean = false; private _onDidChangePendingReviewState: vscode.EventEmitter = new vscode.EventEmitter(); @@ -236,23 +238,15 @@ export class PullRequestModel extends IssueModel implements IPullRe public base: GitHubRef; protected override updateState(state: string) { - if (state.toLowerCase() === 'open') { - this.state = GithubItemStateEnum.Open; - } else if (state.toLowerCase() === 'merged' || this.item.merged) { - this.state = GithubItemStateEnum.Merged; - } else { - this.state = GithubItemStateEnum.Closed; - } + const newState = parsePullRequestState(state); + this.state = this.item.merged ? GithubItemStateEnum.Merged : newState; } override update(item: PullRequest): void { super.update(item); this.isDraft = item.isDraft; this.suggestedReviewers = item.suggestedReviewers; - this.closingIssues = (item.closingIssues ?? []).map(issue => ({ - ...issue, - state: issue.state as GithubItemStateEnum - })); + this.closingIssues = item.closingIssues ?? []; if (item.isRemoteHeadDeleted != null) { this.isRemoteHeadDeleted = item.isRemoteHeadDeleted; } diff --git a/src/github/utils.ts b/src/github/utils.ts index 29b32ba996..3b65723440 100644 --- a/src/github/utils.ts +++ b/src/github/utils.ts @@ -742,6 +742,16 @@ export function parseMergeability(mergeability: 'UNKNOWN' | 'MERGEABLE' | 'CONFL return parsed; } +export function parsePullRequestState(state: string): GithubItemStateEnum { + if (state.toLowerCase() === 'open') { + return GithubItemStateEnum.Open; + } else if (state.toLowerCase() === 'merged') { + return GithubItemStateEnum.Merged; + } else { + return GithubItemStateEnum.Closed; + } +} + export function parseGraphQLPullRequest( graphQLPullRequest: GraphQL.PullRequest, githubRepository: GitHubRepository, @@ -925,7 +935,7 @@ function parseSuggestedReviewers( } function parseClosingIssuesReferences( - closingIssuesReferences: Array<{ id: number, number: number, title: string, state: 'CLOSED' | 'OPEN' }> | undefined + closingIssuesReferences: Array<{ id: number, number: number, title: string, state: string }> | undefined ): Array<{ id: number, number: number, title: string, state: GithubItemStateEnum }> { if (!closingIssuesReferences) { return []; @@ -935,7 +945,7 @@ function parseClosingIssuesReferences( id: issue.id, number: issue.number, title: issue.title, - state: issue.state as GithubItemStateEnum + state: parsePullRequestState(issue.state) })); } From c41246295ceb810abd19fd7323492c0b8329c5e0 Mon Sep 17 00:00:00 2001 From: Alex Ross <38270282+alexr00@users.noreply.github.com> Date: Fri, 12 Dec 2025 12:07:47 +0100 Subject: [PATCH 5/6] Merge branch 'main' into pr/mohamedamara1/6835 --- .eslintignore | 5 - .eslintrc.base.json | 262 - .eslintrc.browser.json | 9 - .eslintrc.json | 10 - .eslintrc.node.json | 9 - .eslintrc.webviews.json | 9 - .github/ISSUE_TEMPLATE/bug_report.md | 2 +- .github/copilot-instructions.md | 32 + .github/workflows/copilot-setup-steps.yml | 68 + .husky/pre-commit | 1 + .vscode-test.mjs | 29 + .vscode/extensions.json | 3 +- .vscode/launch.json | 4 +- .vscode/settings.json | 8 + .vscodeignore | 3 +- CHANGELOG.md | 336 ++ azure-pipeline.nightly.yml | 4 +- azure-pipeline.pr.yml | 11 +- azure-pipeline.release.yml | 2 + build/eslint-rules/index.js | 13 + .../no-any-except-union-method-signature.js | 66 + build/eslint-rules/no-cast-to-any.js | 18 + build/eslint-rules/no-pr-in-user-strings.js | 96 + .../public-methods-well-defined-types.js | 172 + build/filters.js | 1 + build/hygiene.js | 2 +- build/update-codicons.ts | 107 + common/sessionParsing.ts | 329 ++ common/types.ts | 9 + common/views.ts | 23 +- documentation/IssueFeatures.md | 11 +- .../changelog/0.110.0/already-pr-branch.png | Bin 0 -> 31521 bytes .../0.110.0/copilot-address-comments.png | Bin 0 -> 94476 bytes .../changelog/0.110.0/issue-webview.png | Bin 0 -> 384786 bytes .../changelog/0.112.0/copilot-start-stop.png | Bin 0 -> 45070 bytes .../changelog/0.114.0/coding-agent-start.png | Bin 0 -> 39546 bytes .../changelog/0.114.0/copilot-pr-status.png | Bin 0 -> 131863 bytes .../changelog/0.114.0/session-log.png | Bin 0 -> 144503 bytes .../changelog/0.116.0/coding-agent-status.png | Bin 0 -> 20250 bytes .../changelog/0.116.0/pr-card-in-chat.png | Bin 0 -> 86040 bytes .../0.116.0/pr-header-copy-actions.png | Bin 0 -> 51406 bytes .../0.116.0/simplified-pr-header-buttons.png | Bin 0 -> 78791 bytes .../0.116.0/suggest-configure-queries.png | Bin 0 -> 19456 bytes .../0.118.0/collapsed-sidbar-content.png | Bin 0 -> 33404 bytes .../delegate-to-coding-agent-action.png | Bin 0 -> 42600 bytes .../changelog/0.122.0/cancel-review.png | Bin 0 -> 14698 bytes .../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 .../0.124.0/explicit-chat-context.png | Bin 0 -> 51669 bytes .../0.124.0/pull-request-implicit-context.png | Bin 0 -> 19858 bytes .../0.124.0/single-button-copilot-pr.png | Bin 0 -> 33345 bytes eslint.config.mjs | 284 + package.json | 785 ++- package.nls.json | 124 +- 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-all.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/merge.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/skip.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-error.svg | 4 + resources/icons/copilot-in-progress.svg | 4 + resources/icons/copilot-success.svg | 4 + resources/icons/copilot.svg | 1 - resources/icons/copy.svg | 2 - ...bview.svg => git-pull-request_webview.svg} | 0 resources/icons/dark/output.svg | 13 + resources/icons/delete.svg | 3 - resources/icons/dot.svg | 3 - resources/icons/edit.svg | 4 - 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/merge_icon.svg | 3 - resources/icons/merge_method.svg | 3 - resources/icons/milestone.svg | 3 - 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/pr_merge.svg | 3 - resources/icons/reactions/confused.png | Bin 2384 -> 0 bytes resources/icons/reactions/eyes.png | Bin 2768 -> 0 bytes resources/icons/reactions/heart.png | Bin 4190 -> 0 bytes resources/icons/reactions/hooray.png | Bin 4549 -> 0 bytes resources/icons/reactions/laugh.png | Bin 3686 -> 0 bytes resources/icons/reactions/rocket.png | Bin 5298 -> 0 bytes resources/icons/reactions/thumbs_down.png | Bin 3500 -> 0 bytes resources/icons/reactions/thumbs_up.png | Bin 3566 -> 0 bytes resources/icons/request_changes.svg | 3 - resources/icons/reviewer.svg | 3 - resources/icons/settings.svg | 3 - resources/icons/skip.svg | 1 - resources/icons/sparkle.svg | 1 - resources/icons/stop-circle.svg | 1 - resources/icons/sync.svg | 1 - scripts/check-commands.js | 73 + scripts/ci/common-setup.yml | 2 +- src/@types/git.d.ts | 118 +- .../vscode.proposed.chatContextProvider.d.ts | 92 + ...ode.proposed.chatParticipantAdditions.d.ts | 684 +++ ...scode.proposed.chatParticipantPrivate.d.ts | 383 ++ .../vscode.proposed.chatSessionsProvider.d.ts | 381 ++ .../vscode.proposed.commentsDraftState.d.ts | 18 + ...vscode.proposed.languageModelDataPart.d.ts | 164 + ...posed.languageModelToolResultAudience.d.ts | 36 + .../vscode.proposed.markdownAlertSyntax.d.ts | 29 + .../vscode.proposed.remoteCodingAgents.d.ts | 8 + ...vscode.proposed.treeItemMarkdownLabel.d.ts | 23 + src/api/api.d.ts | 27 +- src/api/api1.ts | 51 +- src/authentication/githubServer.ts | 4 +- src/commands.ts | 718 ++- src/common/comment.ts | 15 +- src/common/config.ts | 33 + src/common/copilot.ts | 47 +- src/common/diffHunk.ts | 10 +- src/common/emoji.ts | 5 +- src/common/executeCommands.ts | 7 + src/common/gitUtils.ts | 34 + src/common/githubRef.ts | 2 +- src/common/logger.ts | 30 +- src/common/protocol.ts | 3 +- src/common/remote.ts | 4 +- src/common/resources.ts | 26 - src/common/settingKeys.ts | 23 +- src/common/settingsUtils.ts | 154 +- src/common/timelineEvent.ts | 77 +- src/common/uri.ts | 296 +- src/common/utils.ts | 48 +- src/common/uuid.ts | 58 + src/common/webview.ts | 14 +- src/extension.ts | 200 +- src/extensionState.ts | 1 + .../GitHubContactServiceProvider.ts | 4 +- src/gitProviders/api.ts | 4 +- src/gitProviders/builtinGit.ts | 13 +- src/gitProviders/vsls.ts | 5 +- src/gitProviders/vslsguest.ts | 8 +- src/gitProviders/vslshost.ts | 1 + src/github/activityBarViewProvider.ts | 524 +- src/github/common.ts | 115 +- src/github/conflictResolutionCoordinator.ts | 4 +- src/github/copilotApi.ts | 462 ++ src/github/copilotPrWatcher.ts | 294 + src/github/copilotRemoteAgent.ts | 1685 ++++++ .../chatSessionContentBuilder.ts | 468 ++ .../gitOperationsManager.ts | 193 + src/github/copilotRemoteAgentUtils.ts | 64 + src/github/createPRLinkProvider.ts | 2 +- src/github/createPRViewProvider.ts | 118 +- src/github/credentials.ts | 73 +- src/github/emptyCommitWebview.ts | 83 + src/github/folderRepositoryManager.ts | 568 +- src/github/githubRepository.ts | 342 +- src/github/graphql.ts | 111 +- src/github/interface.ts | 32 +- src/github/issueModel.ts | 362 +- src/github/issueOverview.ts | 269 +- src/github/loggingOctokit.ts | 18 +- src/github/markdownUtils.ts | 27 +- src/github/notifications.ts | 384 -- src/github/overviewRestorer.ts | 95 + src/github/prComment.ts | 92 +- src/github/pullRequestGitHelper.ts | 83 +- src/github/pullRequestModel.ts | 749 ++- src/github/pullRequestOverview.ts | 508 +- src/github/pullRequestOverviewCommon.ts | 150 - src/github/pullRequestReviewCommon.ts | 457 ++ src/github/queries.gql | 383 +- src/github/queriesExtra.gql | 385 +- src/github/queriesLimited.gql | 352 +- src/github/queriesShared.gql | 95 +- src/github/quickPicks.ts | 97 +- src/github/repositoriesManager.ts | 55 +- src/github/revertPRViewProvider.ts | 6 +- src/github/utils.ts | 417 +- src/github/views.ts | 54 +- src/integrations/gitlens/gitlensImpl.ts | 2 +- src/issues/currentIssue.ts | 9 +- src/issues/issueCompletionProvider.ts | 58 +- src/issues/issueFeatureRegistrar.ts | 192 +- src/issues/issueHoverProvider.ts | 10 +- src/issues/issueLinkProvider.ts | 89 - src/issues/issueTodoProvider.ts | 118 +- src/issues/issuesView.ts | 47 +- src/issues/shareProviders.ts | 2 +- src/issues/stateManager.ts | 39 +- src/issues/userCompletionProvider.ts | 20 +- src/issues/userHoverProvider.ts | 2 +- src/issues/util.ts | 39 +- src/lm/issueContextProvider.ts | 87 + src/lm/participants.ts | 20 +- src/lm/participantsPrompt.ts | 43 + src/lm/participantsPrompt.tsx | 31 - src/lm/pullRequestContextProvider.ts | 143 + src/lm/tools/activePullRequestTool.ts | 153 +- src/lm/tools/copilotRemoteAgentTool.ts | 154 + src/lm/tools/displayIssuesTool.ts | 4 +- src/lm/tools/fetchIssueTool.ts | 15 +- src/lm/tools/fetchNotificationTool.ts | 2 +- src/lm/tools/openPullRequestTool.ts | 86 + src/lm/tools/searchTools.ts | 4 +- src/lm/tools/summarizeIssueTool.ts | 2 +- src/lm/tools/tools.ts | 17 +- src/migrations.ts | 62 +- .../notificationDecorationProvider.ts | 2 +- src/notifications/notificationItem.ts | 11 +- .../notificationsFeatureRegistar.ts | 61 +- src/notifications/notificationsManager.ts | 179 +- src/notifications/notificationsProvider.ts | 50 +- src/test/browser/index.ts | 6 + .../graphql/latestReviewCommitBuilder.ts | 2 +- .../builders/graphql/pullRequestBuilder.ts | 5 +- .../builders/graphql/timelineEventsBuilder.ts | 7 +- src/test/builders/rest/pullRequestBuilder.ts | 11 +- src/test/builders/rest/refBuilder.ts | 18 +- src/test/builders/rest/repoBuilder.ts | 8 +- src/test/builders/rest/userBuilder.ts | 4 +- .../common/fixtures/gitdiff/sessionParsing.ts | 23 + src/test/common/sessionParsing.test.ts | 609 +++ src/test/common/uri.test.ts | 113 + src/test/extension.isSubmodule.test.ts | 121 + src/test/github/copilotPrWatcher.test.ts | 140 + src/test/github/copilotRemoteAgent.test.ts | 344 ++ .../github/copilotRemoteAgentUtils.test.ts | 98 + .../github/folderRepositoryManager.test.ts | 7 +- src/test/github/graphql.test.ts | 141 + src/test/github/markdownUtils.test.ts | 40 + src/test/github/prComment.test.ts | 56 + src/test/github/pullRequestGitHelper.test.ts | 92 + src/test/github/pullRequestModel.test.ts | 2 - src/test/github/pullRequestOverview.test.ts | 9 +- src/test/index.ts | 50 +- src/test/issues/issueTodoProvider.test.ts | 186 + src/test/issues/issuesUtils.test.ts | 16 + src/test/issues/stateManager.test.ts | 127 + .../lm/tools/copilotRemoteAgentTool.test.ts | 384 ++ src/test/mocks/mockExtensionContext.ts | 4 + src/test/mocks/mockGitHubRepository.ts | 7 +- src/test/mocks/mockNotificationManager.ts | 15 + src/test/mocks/mockPRsTreeModel.ts | 108 + src/test/mocks/mockRepository.ts | 12 +- src/test/mocks/mockThemeWatcher.ts | 34 + src/test/reference-types.d.ts | 6 + src/test/view/prsTree.test.ts | 86 +- src/test/view/reviewCommentController.test.ts | 23 +- src/themeWatcher.ts | 41 + src/uriHandler.ts | 212 + src/view/commentControllBase.ts | 30 +- src/view/commentDecorationProvider.ts | 4 +- src/view/commitsDecorationProvider.ts | 61 + src/view/compareChangesTreeDataProvider.ts | 5 +- src/view/createPullRequestDataModel.ts | 7 +- src/view/createPullRequestHelper.ts | 4 +- src/view/emojiCompletionProvider.ts | 61 + src/view/fileChangeModel.ts | 26 +- src/view/fileTypeDecorationProvider.ts | 2 +- src/view/gitContentProvider.ts | 10 +- src/view/gitHubContentProvider.ts | 24 +- src/view/githubFileContentProvider.ts | 39 + src/view/inMemPRContentProvider.ts | 4 +- src/view/prChangesTreeDataProvider.ts | 8 +- src/view/prNotificationDecorationProvider.ts | 47 - src/view/prStatusDecorationProvider.ts | 132 +- src/view/prsTreeDataProvider.ts | 390 +- src/view/prsTreeModel.ts | 417 +- src/view/pullRequestCommentController.ts | 71 +- .../pullRequestCommentControllerRegistry.ts | 38 +- src/view/repositoryFileSystemProvider.ts | 19 +- src/view/reviewCommentController.ts | 75 +- src/view/reviewManager.ts | 126 +- src/view/reviewsManager.ts | 133 +- src/view/theme.ts | 90 + src/view/treeDecorationProviders.ts | 4 +- src/view/treeNodes/categoryNode.ts | 253 +- src/view/treeNodes/commitNode.ts | 10 +- src/view/treeNodes/commitsCategoryNode.ts | 21 +- src/view/treeNodes/directoryTreeNode.ts | 28 +- src/view/treeNodes/fileChangeNode.ts | 10 +- src/view/treeNodes/filesCategoryNode.ts | 37 +- src/view/treeNodes/pullRequestNode.ts | 99 +- src/view/treeNodes/repositoryChangesNode.ts | 11 +- src/view/treeNodes/treeNode.ts | 20 +- src/view/treeNodes/workspaceFolderNode.ts | 64 +- src/view/webviewViewCoordinator.ts | 2 +- tsconfig.eslint.json | 4 - tsconfig.json | 1 - tsconfig.scripts.json | 14 + webpack.config.js | 106 +- webviews/activityBarView/app.tsx | 2 +- webviews/activityBarView/index.css | 47 +- webviews/activityBarView/overview.tsx | 2 +- webviews/common/cache.ts | 8 +- webviews/common/common.css | 100 +- webviews/common/context.tsx | 212 +- webviews/common/createContextNew.ts | 20 +- webviews/common/errorBoundary.tsx | 24 +- webviews/common/label.tsx | 16 +- webviews/common/message.ts | 6 +- webviews/components/automergeSelect.tsx | 4 +- webviews/components/comment.tsx | 187 +- webviews/components/contextDropdown.tsx | 27 +- webviews/components/diff.tsx | 4 +- webviews/components/dropdown.tsx | 6 +- webviews/components/header.tsx | 314 +- webviews/components/icon.tsx | 78 +- webviews/components/merge.tsx | 120 +- webviews/components/reviewer.tsx | 12 +- webviews/components/sidebar.tsx | 459 +- webviews/components/timeline.tsx | 343 +- webviews/components/timestamp.tsx | 85 +- webviews/components/user.tsx | 14 +- webviews/createPullRequestViewNew/app.tsx | 30 +- webviews/createPullRequestViewNew/index.css | 22 +- webviews/editorWebview/app.tsx | 4 +- webviews/editorWebview/index.css | 298 +- webviews/editorWebview/overview.tsx | 8 +- .../editorWebview/test/builder/pullRequest.ts | 7 +- yarn.lock | 4724 +++++++++++------ 364 files changed, 25691 insertions(+), 6038 deletions(-) delete mode 100644 .eslintignore delete mode 100644 .eslintrc.base.json delete mode 100644 .eslintrc.browser.json delete mode 100644 .eslintrc.json delete mode 100644 .eslintrc.node.json delete mode 100644 .eslintrc.webviews.json create mode 100644 .github/copilot-instructions.md create mode 100644 .github/workflows/copilot-setup-steps.yml create mode 100644 .vscode-test.mjs create mode 100644 build/eslint-rules/index.js create mode 100644 build/eslint-rules/no-any-except-union-method-signature.js create mode 100644 build/eslint-rules/no-cast-to-any.js create mode 100644 build/eslint-rules/no-pr-in-user-strings.js create mode 100644 build/eslint-rules/public-methods-well-defined-types.js create mode 100644 build/update-codicons.ts create mode 100644 common/sessionParsing.ts create mode 100644 common/types.ts create mode 100644 documentation/changelog/0.110.0/already-pr-branch.png create mode 100644 documentation/changelog/0.110.0/copilot-address-comments.png create mode 100644 documentation/changelog/0.110.0/issue-webview.png create mode 100644 documentation/changelog/0.112.0/copilot-start-stop.png create mode 100644 documentation/changelog/0.114.0/coding-agent-start.png create mode 100644 documentation/changelog/0.114.0/copilot-pr-status.png create mode 100644 documentation/changelog/0.114.0/session-log.png create mode 100644 documentation/changelog/0.116.0/coding-agent-status.png create mode 100644 documentation/changelog/0.116.0/pr-card-in-chat.png create mode 100644 documentation/changelog/0.116.0/pr-header-copy-actions.png create mode 100644 documentation/changelog/0.116.0/simplified-pr-header-buttons.png create mode 100644 documentation/changelog/0.116.0/suggest-configure-queries.png create mode 100644 documentation/changelog/0.118.0/collapsed-sidbar-content.png create mode 100644 documentation/changelog/0.118.0/delegate-to-coding-agent-action.png create mode 100644 documentation/changelog/0.122.0/cancel-review.png 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 create mode 100644 documentation/changelog/0.124.0/explicit-chat-context.png create mode 100644 documentation/changelog/0.124.0/pull-request-implicit-context.png create mode 100644 documentation/changelog/0.124.0/single-button-copilot-pr.png create mode 100644 eslint.config.mjs 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-all.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/merge.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/skip.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 create mode 100644 resources/icons/copilot-error.svg create mode 100644 resources/icons/copilot-in-progress.svg create mode 100644 resources/icons/copilot-success.svg delete mode 100644 resources/icons/copilot.svg delete mode 100644 resources/icons/copy.svg rename resources/icons/dark/{pr_webview.svg => git-pull-request_webview.svg} (100%) create mode 100644 resources/icons/dark/output.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/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/merge_icon.svg delete mode 100644 resources/icons/merge_method.svg delete mode 100644 resources/icons/milestone.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/pr_merge.svg delete mode 100644 resources/icons/reactions/confused.png delete mode 100644 resources/icons/reactions/eyes.png delete mode 100644 resources/icons/reactions/heart.png delete mode 100644 resources/icons/reactions/hooray.png delete mode 100644 resources/icons/reactions/laugh.png delete mode 100644 resources/icons/reactions/rocket.png delete mode 100644 resources/icons/reactions/thumbs_down.png delete mode 100644 resources/icons/reactions/thumbs_up.png 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/skip.svg delete mode 100644 resources/icons/sparkle.svg delete mode 100644 resources/icons/stop-circle.svg delete mode 100644 resources/icons/sync.svg create mode 100644 scripts/check-commands.js create mode 100644 src/@types/vscode.proposed.chatContextProvider.d.ts create mode 100644 src/@types/vscode.proposed.chatParticipantAdditions.d.ts create mode 100644 src/@types/vscode.proposed.chatParticipantPrivate.d.ts create mode 100644 src/@types/vscode.proposed.chatSessionsProvider.d.ts create mode 100644 src/@types/vscode.proposed.commentsDraftState.d.ts create mode 100644 src/@types/vscode.proposed.languageModelDataPart.d.ts create mode 100644 src/@types/vscode.proposed.languageModelToolResultAudience.d.ts create mode 100644 src/@types/vscode.proposed.markdownAlertSyntax.d.ts create mode 100644 src/@types/vscode.proposed.remoteCodingAgents.d.ts create mode 100644 src/@types/vscode.proposed.treeItemMarkdownLabel.d.ts create mode 100644 src/common/config.ts create mode 100644 src/common/gitUtils.ts delete mode 100644 src/common/resources.ts create mode 100644 src/common/uuid.ts create mode 100644 src/github/copilotApi.ts create mode 100644 src/github/copilotPrWatcher.ts create mode 100644 src/github/copilotRemoteAgent.ts create mode 100644 src/github/copilotRemoteAgent/chatSessionContentBuilder.ts create mode 100644 src/github/copilotRemoteAgent/gitOperationsManager.ts create mode 100644 src/github/copilotRemoteAgentUtils.ts create mode 100644 src/github/emptyCommitWebview.ts delete mode 100644 src/github/notifications.ts create mode 100644 src/github/overviewRestorer.ts delete mode 100644 src/github/pullRequestOverviewCommon.ts create mode 100644 src/github/pullRequestReviewCommon.ts delete mode 100644 src/issues/issueLinkProvider.ts create mode 100644 src/lm/issueContextProvider.ts create mode 100644 src/lm/participantsPrompt.ts delete mode 100644 src/lm/participantsPrompt.tsx create mode 100644 src/lm/pullRequestContextProvider.ts create mode 100644 src/lm/tools/copilotRemoteAgentTool.ts create mode 100644 src/lm/tools/openPullRequestTool.ts create mode 100644 src/test/common/fixtures/gitdiff/sessionParsing.ts create mode 100644 src/test/common/sessionParsing.test.ts create mode 100644 src/test/common/uri.test.ts create mode 100644 src/test/extension.isSubmodule.test.ts create mode 100644 src/test/github/copilotPrWatcher.test.ts create mode 100644 src/test/github/copilotRemoteAgent.test.ts create mode 100644 src/test/github/copilotRemoteAgentUtils.test.ts create mode 100644 src/test/github/graphql.test.ts create mode 100644 src/test/github/markdownUtils.test.ts create mode 100644 src/test/github/prComment.test.ts create mode 100644 src/test/issues/issueTodoProvider.test.ts create mode 100644 src/test/issues/stateManager.test.ts create mode 100644 src/test/lm/tools/copilotRemoteAgentTool.test.ts create mode 100644 src/test/mocks/mockNotificationManager.ts create mode 100644 src/test/mocks/mockPRsTreeModel.ts create mode 100644 src/test/mocks/mockThemeWatcher.ts create mode 100644 src/test/reference-types.d.ts create mode 100644 src/themeWatcher.ts create mode 100644 src/uriHandler.ts create mode 100644 src/view/commitsDecorationProvider.ts create mode 100644 src/view/emojiCompletionProvider.ts create mode 100644 src/view/githubFileContentProvider.ts delete mode 100644 src/view/prNotificationDecorationProvider.ts create mode 100644 src/view/theme.ts delete mode 100644 tsconfig.eslint.json create mode 100644 tsconfig.scripts.json diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index d1e4847917..0000000000 --- a/.eslintignore +++ /dev/null @@ -1,5 +0,0 @@ -build/* -dist/* -src/test/* -**/@types/* -webpack.config.js diff --git a/.eslintrc.base.json b/.eslintrc.base.json deleted file mode 100644 index b3fb1b47d4..0000000000 --- a/.eslintrc.base.json +++ /dev/null @@ -1,262 +0,0 @@ -{ - "env": { - "es6": true - }, - "extends": [ - "eslint:recommended", - "plugin:@typescript-eslint/recommended", - "plugin:@typescript-eslint/recommended-requiring-type-checking", - "plugin:import/errors", - "plugin:import/warnings", - "plugin:import/typescript" - ], - "parser": "@typescript-eslint/parser", - "parserOptions": { - "ecmaVersion": 2019, - "sourceType": "module", - "ecmaFeatures": { - "impliedStrict": true - } - }, - "plugins": ["import", "@typescript-eslint"], - "reportUnusedDisableDirectives": true, - "root": true, - "rules": { - "new-parens": "error", - "no-async-promise-executor": "off", - "no-console": "off", - "no-constant-condition": ["warn", { "checkLoops": false }], - "no-caller": "error", - "no-case-declarations": "off", // TODO@eamodio revisit - "no-debugger": "warn", - "no-dupe-class-members": "off", - "no-duplicate-imports": "error", - "no-else-return": "off", // TODO@eamodio revisit - "no-empty": "off", // TODO@eamodio revisit - //"no-empty": ["warn", { "allowEmptyCatch": true }], - "no-eval": "error", - "no-ex-assign": "warn", - "no-extend-native": "error", - "no-extra-bind": "error", - "no-extra-boolean-cast": "off", // TODO@eamodio revisit - "no-floating-decimal": "error", - "no-implicit-coercion": "off", - "no-implied-eval": "error", - // Turn off until fix for: https://github.com/typescript-eslint/typescript-eslint/issues/239 - "no-inner-declarations": "off", - "no-lone-blocks": "error", - "no-lonely-if": "off", - "no-loop-func": "error", - "no-multi-spaces": "off", - "no-prototype-builtins": "off", - "no-return-assign": "error", - "no-return-await": "off", // TODO@eamodio revisit - "no-self-compare": "error", - "no-sequences": "error", - "no-template-curly-in-string": "warn", - "no-throw-literal": "error", - "no-unmodified-loop-condition": "warn", - "no-unneeded-ternary": "error", - "no-use-before-define": "off", - "no-useless-call": "error", - "no-useless-catch": "error", - "no-useless-computed-key": "error", - "no-useless-concat": "error", - "no-useless-escape": "off", - "no-useless-rename": "error", - "no-useless-return": "off", - "no-var": "error", - "no-with": "error", - "object-shorthand": "off", - "one-var": "off", // TODO@eamodio revisit - // "one-var": ["error", "never"], - "prefer-arrow-callback": "off", // TODO@eamodio revisit - "prefer-const": "off", - "prefer-numeric-literals": "error", - "prefer-object-spread": "error", - "prefer-rest-params": "error", - "prefer-spread": "error", - "prefer-template": "off", // TODO@eamodio revisit - "quotes": ["error", "single", { "avoidEscape": true, "allowTemplateLiterals": true }], - // Turn off until fix for: https://github.com/eslint/eslint/issues/11899 - "require-atomic-updates": "off", - "semi": ["error", "always"], - "semi-style": ["error", "last"], - "sort-imports": [ - "error", - { - "ignoreCase": true, - "ignoreDeclarationSort": true, - "ignoreMemberSort": false, - "memberSyntaxSortOrder": ["none", "all", "multiple", "single"] - } - ], - "yoda": "error", - "import/export": "off", - "import/extensions": ["error", "never"], - "import/named": "off", - "import/namespace": "off", - "import/newline-after-import": "warn", - "import/no-cycle": "off", - "import/no-dynamic-require": "error", - "import/no-default-export": "off", // TODO@eamodio revisit - "import/no-duplicates": "error", - "import/no-self-import": "error", - "import/no-unresolved": ["warn", { "ignore": ["vscode", "ghpr", "git", "extensionApi", "@octokit/rest", "@octokit/types"] }], - "import/order": [ - "warn", - { - "groups": ["builtin", "external", "internal", ["parent", "sibling", "index"]], - "newlines-between": "ignore", - "alphabetize": { - "order": "asc", - "caseInsensitive": true - } - } - ], - "@typescript-eslint/await-thenable": "error", - "@typescript-eslint/ban-types": "off", // TODO@eamodio revisit - // "@typescript-eslint/ban-types": [ - // "error", - // { - // "extendDefaults": false, - // "types": { - // "String": { - // "message": "Use string instead", - // "fixWith": "string" - // }, - // "Boolean": { - // "message": "Use boolean instead", - // "fixWith": "boolean" - // }, - // "Number": { - // "message": "Use number instead", - // "fixWith": "number" - // }, - // "Symbol": { - // "message": "Use symbol instead", - // "fixWith": "symbol" - // }, - // "Function": { - // "message": "The `Function` type accepts any function-like value.\nIt provides no type safety when calling the function, which can be a common source of bugs.\nIt also accepts things like class declarations, which will throw at runtime as they will not be called with `new`.\nIf you are expecting the function to accept certain arguments, you should explicitly define the function shape." - // }, - // "Object": { - // "message": "The `Object` type actually means \"any non-nullish value\", so it is marginally better than `unknown`.\n- If you want a type meaning \"any object\", you probably want `Record` instead.\n- If you want a type meaning \"any value\", you probably want `unknown` instead." - // }, - // "{}": { - // "message": "`{}` actually means \"any non-nullish value\".\n- If you want a type meaning \"any object\", you probably want `object` or `Record` instead.\n- If you want a type meaning \"any value\", you probably want `unknown` instead.", - // "fixWith": "object" - // } - // // "object": { - // // "message": "The `object` type is currently hard to use ([see this issue](https://github.com/microsoft/TypeScript/issues/21732)).\nConsider using `Record` instead, as it allows you to more easily inspect and use the keys." - // // } - // } - // } - // ], - "@typescript-eslint/consistent-type-assertions": [ - "warn", - { - "assertionStyle": "as", - "objectLiteralTypeAssertions": "allow-as-parameter" - } - ], - "@typescript-eslint/explicit-function-return-type": "off", - "@typescript-eslint/explicit-member-accessibility": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", // TODO@eamodio revisit - // "@typescript-eslint/naming-convention": [ - // "error", - // { - // "selector": "variable", - // "format": ["camelCase", "PascalCase"], - // "leadingUnderscore": "allow", - // "filter": { - // "regex": "^_$", - // "match": false - // } - // }, - // { - // "selector": "variableLike", - // "format": ["camelCase"], - // "leadingUnderscore": "allow", - // "filter": { - // "regex": "^_$", - // "match": false - // } - // }, - // { - // "selector": "memberLike", - // "modifiers": ["private"], - // "format": ["camelCase"], - // "leadingUnderscore": "allow" - // }, - // { - // "selector": "memberLike", - // "modifiers": ["private", "readonly"], - // "format": ["camelCase", "PascalCase"], - // "leadingUnderscore": "allow" - // }, - // { - // "selector": "memberLike", - // "modifiers": ["static", "readonly"], - // "format": ["camelCase", "PascalCase"] - // }, - // { - // "selector": "interface", - // "format": ["PascalCase"] - // // "custom": { - // // "regex": "^I[A-Z]", - // // "match": false - // // } - // } - // ], - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/no-empty-interface": "error", - "@typescript-eslint/no-explicit-any": "off", - "@typescript-eslint/no-floating-promises": "off", // TODO@eamodio revisit - "@typescript-eslint/no-implied-eval": "error", - "@typescript-eslint/no-inferrable-types": "off", // TODO@eamodio revisit - // "@typescript-eslint/no-inferrable-types": ["warn", { "ignoreParameters": true, "ignoreProperties": true }], - "@typescript-eslint/no-misused-promises": ["error", { "checksConditionals": false, "checksVoidReturn": false }], - "@typescript-eslint/no-namespace": "off", - "@typescript-eslint/no-non-null-assertion": "off", - "@typescript-eslint/no-parameter-properties": "off", - "@typescript-eslint/no-redundant-type-constituents": "off", - "@typescript-eslint/no-this-alias": "off", - "@typescript-eslint/no-unnecessary-condition": "off", - "@typescript-eslint/no-unnecessary-type-assertion": "off", // TODO@eamodio revisit - "@typescript-eslint/no-unsafe-argument": "off", - "@typescript-eslint/no-unsafe-assignment": "off", // TODO@eamodio revisit - "@typescript-eslint/no-unsafe-call": "off", // TODO@eamodio revisit - "@typescript-eslint/no-unsafe-enum-comparison": "off", - "@typescript-eslint/no-unsafe-member-access": "off", // TODO@eamodio revisit - "@typescript-eslint/no-unsafe-return": "off", // TODO@eamodio revisit - "@typescript-eslint/no-unused-expressions": ["warn", { "allowShortCircuit": true }], - "@typescript-eslint/no-unused-vars": ["warn", { "argsIgnorePattern": "^_" }], - // "@typescript-eslint/no-unused-vars": [ - // "warn", - // { - // "args": "after-used", - // "argsIgnorePattern": "^_", - // "ignoreRestSiblings": true, - // "varsIgnorePattern": "^_$" - // } - // ], - "@typescript-eslint/no-use-before-define": "off", - "@typescript-eslint/prefer-regexp-exec": "off", // TODO@eamodio revisit - "@typescript-eslint/prefer-nullish-coalescing": "off", - "@typescript-eslint/prefer-optional-chain": "off", - "@typescript-eslint/require-await": "off", // TODO@eamodio revisit - "@typescript-eslint/restrict-plus-operands": "error", - "@typescript-eslint/restrict-template-expressions": "off", // TODO@eamodio revisit - // "@typescript-eslint/restrict-template-expressions": [ - // "error", - // { "allowAny": true, "allowBoolean": true, "allowNumber": true, "allowNullish": true } - // ], - "@typescript-eslint/strict-boolean-expressions": "off", - // "@typescript-eslint/strict-boolean-expressions": [ - // "warn", - // { "allowNullableBoolean": true, "allowNullableNumber": true, "allowNullableString": true } - // ], - "@typescript-eslint/unbound-method": "off" // Too many bugs right now: https://github.com/typescript-eslint/typescript-eslint/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+unbound-method - } -} diff --git a/.eslintrc.browser.json b/.eslintrc.browser.json deleted file mode 100644 index bd2d1e8008..0000000000 --- a/.eslintrc.browser.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": [".eslintrc.base.json"], - "env": { - "worker": true - }, - "parserOptions": { - "project": "tsconfig.browser.json" - } -} diff --git a/.eslintrc.json b/.eslintrc.json deleted file mode 100644 index 61265a66e7..0000000000 --- a/.eslintrc.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "extends": [".eslintrc.base.json"], - "env": { - "browser": true, - "node": true - }, - "parserOptions": { - "project": "tsconfig.eslint.json" - } -} diff --git a/.eslintrc.node.json b/.eslintrc.node.json deleted file mode 100644 index 14e0614015..0000000000 --- a/.eslintrc.node.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": [".eslintrc.base.json"], - "env": { - "node": true - }, - "parserOptions": { - "project": "tsconfig.json" - } -} diff --git a/.eslintrc.webviews.json b/.eslintrc.webviews.json deleted file mode 100644 index ae8e5d7124..0000000000 --- a/.eslintrc.webviews.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": [".eslintrc.base.json"], - "env": { - "browser": true - }, - "parserOptions": { - "project": "tsconfig.webviews.json" - } -} diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index ab7b2028aa..d9d5ca7ad5 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -11,7 +11,7 @@ about: Create a report to help us improve - VSCode Version: - OS: - Repository Clone Configuration (single repository/fork of an upstream repository): -- Github Product (Github.com/Github Enterprise version x.x.x): +- GitHub Product (GitHub.com/GitHub Enterprise version x.x.x): Steps to Reproduce: diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..0c48ce8f75 --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,32 @@ +## General Guidelines +- **Follow the existing code style**: Use single quotes, semicolons, and 2-space indentation. See `.eslintrc.base.json` for detailed linting rules. +- **TypeScript**: Use modern TypeScript features. Prefer explicit types where clarity is needed, but do not over-annotate. +- **React/JSX**: Webviews use React-style JSX with custom factories (`vscpp`, `vscppf`). +- **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`. +- **Configuration**: All user-facing settings must be declared in `package.json` under `contributes.configuration`. +- **Activation Events**: Only add new activation events if absolutely necessary. +- **Webviews**: Place webview code in the `webviews/` directory. Use the shared `common/` code where possible. +- **Commands**: Register new commands in `package.json` and implement them in `src/commands.ts` or a relevant module. +- **Logging**: Use the `Logger` utility for all logging purposes. Don't use console.log or similar methods directly. +- **Test Running**: Use tools over tasks over scripts to run tests. + +## Specific Feature Practices +- **Commands**: When adding a new command, consider whether it should be available in the command palette, context menus, or both. Add the appropriate menu entries in `package.json` to ensure the command is properly included, or excluded (command palette), from menus. + +## Pull Request Guidelines +- Never touch the yarn.lock file. +- Run `yarn run lint` and also `npm run hygiene` and fix any errors or warnings before committing. + +## Testing +- Use `describe` and `it` blocks from Mocha for structuring tests. +- Tests MUST run product code. +- Tests should be placed near the code they are testing, in a `test` folder. Name test fies with a `.test.ts` suffix. + +--- +_Last updated: 2025-06-20_ diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 0000000000..c9f9ff8872 --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,68 @@ +name: "Copilot Setup Steps" + +# Automatically run the setup steps when they are changed to allow for easy validation, and +# allow manual testing through the repository's "Actions" tab +on: + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + # The job MUST be called `copilot-setup-steps` or it will not be picked up by Copilot. + copilot-setup-steps: + runs-on: ubuntu-latest + + # Set the permissions to the lowest permissions possible needed for your steps. + # Copilot will be given its own token for its operations. + permissions: + contents: read + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + cache: "yarn" + + - name: Install dependencies + run: yarn install --frozen-lockfile + env: + # Skip Playwright browser downloads to avoid installation failures + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 1 + + - name: Install Playwright browsers (if needed) + run: | + if [ -d "node_modules/playwright" ]; then + echo "Installing Playwright browsers..." + npx playwright install --with-deps + else + echo "Playwright not found, skipping browser installation" + fi + continue-on-error: true + + - name: Update VS Code type definitions + run: yarn update-dts + + - name: Basic build verification + run: | + echo "Verifying basic setup..." + if [ ! -d "node_modules" ]; then + echo "❌ node_modules not found" + exit 1 + fi + if [ ! -f "node_modules/.bin/tsc" ]; then + echo "❌ TypeScript compiler not found" + exit 1 + fi + if [ ! -f "node_modules/.bin/webpack" ]; then + echo "❌ Webpack not found" + exit 1 + fi + echo "✅ Basic setup verification successful" diff --git a/.husky/pre-commit b/.husky/pre-commit index 05e7278194..41bddbd059 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -2,3 +2,4 @@ . "$(dirname -- "$0")/_/husky.sh" npm run hygiene +npm run lint \ No newline at end of file diff --git a/.vscode-test.mjs b/.vscode-test.mjs new file mode 100644 index 0000000000..c3406cbc00 --- /dev/null +++ b/.vscode-test.mjs @@ -0,0 +1,29 @@ + +import { defineConfig } from "@vscode/test-cli"; + +/** + * @param {string} label + */ +function generateConfig(label) { + /** @type {import('@vscode/test-cli').TestConfiguration} */ + let config = { + label, + files: ["dist/**/*.test.js"], + version: "insiders", + srcDir: "src", + launchArgs: [ + "--enable-proposed-api", + "--disable-extension=GitHub.vscode-pull-request-github-insiders", + ], + // env, + mocha: { + ui: "bdd", + color: true, + timeout: 25000 + }, + }; + + return config; +} + +export default defineConfig(generateConfig("Local Tests")); diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 93f54d0843..cd6d08e737 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -1,6 +1,7 @@ { "recommendations": [ "dbaeumer.vscode-eslint", - "amodio.tsl-problem-matcher" + "amodio.tsl-problem-matcher", + "ms-vscode.extension-test-runner" ] } diff --git a/.vscode/launch.json b/.vscode/launch.json index 1cae2e40e1..07d36ff613 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -92,6 +92,7 @@ "--extensionDevelopmentPath=${workspaceFolder}", "--extensionTestsPath=${workspaceFolder}/out/src/test", "--disable-extensions", + "--user-data-dir=${workspaceFolder}/.vscode-test" ], "preLaunchTask": "npm: test:preprocess", "smartStep": true, @@ -101,12 +102,11 @@ "type": "node", "request": "launch", "name": "Attach Web Test", - "program": "${workspaceFolder}/node_modules/vscode-test-web/out/index.js", + "program": "${workspaceFolder}/node_modules/@vscode/test-web/out/server/index.js", "args": [ "--extensionTestsPath=dist/browser/test/index.js", "--extensionDevelopmentPath=.", "--browserType=chromium", - "--attach=9229" ], "cascadeTerminateToConfigurations": [ "Launch Web Test" diff --git a/.vscode/settings.json b/.vscode/settings.json index 62614fba5c..71538296dc 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,6 +4,14 @@ ".eslintrc.json": "jsonc", ".eslintrc.*.json": "jsonc" }, + "json.schemas": [ + { + "fileMatch": [ + "**/.eslintrc.*.json" + ], + "url": "https://json.schemastore.org/eslintrc" + } + ], "files.trimTrailingWhitespace": true, "gitlens.advanced.blame.customArguments": ["--ignore-revs-file", ".gitignore-revs"] } diff --git a/.vscodeignore b/.vscodeignore index 27af4fabc3..ec86cb30a1 100644 --- a/.vscodeignore +++ b/.vscodeignore @@ -12,6 +12,7 @@ node_modules/** scripts/** src/** webviews/** +**/test/** .eslintcache* .eslintignore .eslintrc* @@ -28,7 +29,7 @@ azure-pipeline.* yarn.lock **/*.map **/*.svg -!**/pr_webview.svg +!**/git-pull-request_webview.svg **/*.ts *.vsix **/*.bak diff --git a/CHANGELOG.md b/CHANGELOG.md index c4633eeb5c..2f5be76376 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,341 @@ # Changelog +## 0.124.0 + +### Changes + +- The active pull request or issue webview title is now included as implicit context when using Copilot Chat. You can click on the implicit context item to include all the PR information in your prompt. + +![Pull request title as implicit context in Chat](./documentation/changelog/0.124.0/pull-request-implicit-context.png) + +- Pull request and issue context can also be manually added to Chat from the "Add Context" menu. + +![Explicit Chat context](./documentation/changelog/0.124.0/explicit-chat-context.png) + +- Single button for marking Copilot pull requests as ready for review, approved, and automerge (if enabled). + +![Single button to mark Copilot pull requests](./documentation/changelog/0.124.0/single-button-copilot-pr.png) + +- The "Copy link" action for individual comments shows in the pull request description webview. +- Comments that are part of an un-submitted review (only visible to you) now show with at "comment draft" icon in the editor gutter and in the Comments view. +- For commits where checks have run, the commit status icon now shows next to each commit in the pull request description webview. + +### Fixes + +- Comments don't show when PR is on non-default repo. https://github.com/microsoft/vscode-pull-request-github/issues/8050 +- ignoreSubmodules is honored differently for Pull Requests and Issues view. https://github.com/microsoft/vscode-pull-request-github/issues/7741 + +**_Thank You_** + +* [@vicky1999 (Vignesh)](https://github.com/vicky1999) + * fix: message wrapping in narrow editor panes [PR #8121](https://github.com/microsoft/vscode-pull-request-github/pull/8121) + * feat: Display commit status icon for each commit [PR #8142](https://github.com/microsoft/vscode-pull-request-github/pull/8142) + * feat: Add copy comment link button in PR overview [PR #8150](https://github.com/microsoft/vscode-pull-request-github/pull/8150) + +## 0.122.1 + +### Fixes + +- Only one reviewer can be seen on the PR page. https://github.com/microsoft/vscode-pull-request-github/issues/8131 +- Drop down not doing anything. https://github.com/microsoft/vscode-pull-request-github/issues/8149 +- Pull in icon fixes. https://github.com/microsoft/vscode-pull-request-github/issues/8159 + +## 0.122.0 + +### Changes + +- 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. + +![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) + +- 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 + +### Fixes + +- Unable to open PR webview within VSCode. https://github.com/microsoft/vscode-pull-request-github/issues/8028 + +## 0.120.1 + +### Fixes + +- Extension cannot find git repo when VS Code didn't open the git root directory. https://github.com/microsoft/vscode-pull-request-github/issues/7964 + +## 0.120.0 + +### Changes + +- The `#openPullRequest` tool recognizes open PR diffs and PR files as being the "open pull request". +- All Copilot PR notifications can be marked as ready using the right-click context menu on the Copilot section header in the Pull Requests view. +- The setting `githubIssues.issueAvatarDisplay` can be used to control whether the first assignee's avatar or the author's avatar is shown in the Issues view. +- Instead of always running the pull request queries that back the Pull Requests view when refreshing, we now check to see if there are new PRs in the repo before running the queries. This should reduce API usage when there are no new PRs. +- The "Copy link" action is back near the PR title in the pull request description webview. +- You can configure that the default branch is pulled when you're "done" with a PR using `"githubPullRequests.postDone": "checkoutDefaultBranchAndPull"`. + +### Fixes + +- Unable to get list of users to assign them to a pull request. https://github.com/microsoft/vscode-pull-request-github/issues/7908 +- Error notifications when using GitHub Enterprise Server. https://github.com/microsoft/vscode-pull-request-github/issues/7901 +- Ignore worktrees that aren't in one of the workspace folders. https://github.com/microsoft/vscode-pull-request-github/issues/7896 +- Typing "#" and then Enter or Tab opens the GitHub issue queries settings. https://github.com/microsoft/vscode-pull-request-github/issues/7838 +- Unexpected branch switching when githubIssues.useBranchForIssues = off. https://github.com/microsoft/vscode-pull-request-github/issues/7827 +- Extension enters rapid refresh loop, causing high API usage and rate limiting. https://github.com/microsoft/vscode-pull-request-github/issues/7816 +- GitHub PR view highlights all repos with Copilot notification. https://github.com/microsoft/vscode-pull-request-github/issues/7852 +- Wrong commit is checked out when local branch exists with the same name. https://github.com/microsoft/vscode-pull-request-github/issues/7702 +- Visual Label not provided for "Title" and "Description" field. https://github.com/microsoft/vscode-pull-request-github/issues/7595 +- VSCode unresponsive during GitHub Pull Requests PR checkout (large number of files changed). https://github.com/microsoft/vscode-pull-request-github/issues/6952 +- extension explodes and kicks back out to GITHUB: LOGIN when non github repos are in working directory (specifically codeberg). https://github.com/microsoft/vscode-pull-request-github/issues/6945 + +## 0.118.2 + +### Fixes + +- Long coding agent problem statement results in unrecoverable error (Truncate coding agent problem_statement). https://github.com/microsoft/vscode-pull-request-github/issues/7861 + +## 0.118.1 + +### Fixes + +- _No logs available for this session_ race condition. https://github.com/microsoft/vscode-pull-request-github/issues/7783 + +## 0.118.0 + +### Changes + +- There's a new code action "Delegate to coding agent" which shows on `TODO` comments. The "to do" keywords are configurable using the existing setting ``. + +![Todo comment with "delegate to coding agent" action](./documentation/changelog/0.118.0/delegate-to-coding-agent-action.png) + +- More of the new Copilot coding agent user entry points prompt for sign in if you aren't already signed in to GitHub (GitHub Copilot Coding Agent view in Chat Sessions, `#copilotCodingAgent` tool, "Delegate to coding agent" button). +- Some of the individual extension views used the same icon, making it difficult to distinguish between them if you drag them into their own view container. To solve this, several views use a new icon: "Pull Requests" uses `github-inverted`, "Active Pull Request" tree view uses `diff-multiple`, "Active Pull Request" webview view uses `code-review`. +- The "sidebar" content (reviewers, assignees, labels, etc.) in the pull request description webview have always moved above the pull request body when the webview width is narrow. Now, it also collapses into a compact, readonly view, which can be expanded to make modifications. + +![Collapsed sidbar content on a narrow view](./documentation/changelog/0.118.0/collapsed-sidbar-content.png) + +- Pull request and issue webviews restore after reload. +- The new `#openPullRequest` tool in Copilot Chat lets you reference the pull request currently open in a webview. The `#activePullRequest` tool continues to reference the pull request currently checked out. +- The "Edit Query" command in the "Pull Requests" view has an option to edit the query with Copilot. +- Setting `"githubPullRequests.ignoreSubmodules": true` will cause the extension to ignore submodules when looking for pull requests. +- In the "Issues" view, you can right click on an issue and "Assign to Coding Agent". + +### Fixes + +- Only update coding agent PRs when view is open. https://github.com/microsoft/vscode-pull-request-github/issues/7643 +- Chat participant not honoring selected tools and thinks they are all selected. https://github.com/microsoft/vscode-pull-request-github/issues/7637 +- Red "closed" on closed issues is confusing. https://github.com/microsoft/vscode-pull-request-github/issues/7628 +- github-pull-request_activePullRequest returns empty comments array. https://github.com/microsoft/vscode-pull-request-github/issues/7601 +- Allows me to believe I assigned an issue on a repo where I lack that permission. https://github.com/microsoft/vscode-pull-request-github/issues/7534 +- clicked comment with no contents gave weird state. https://github.com/microsoft/vscode-pull-request-github/issues/7476 +- In GH PR review page, headers have redundant url content. https://github.com/microsoft/vscode-pull-request-github/issues/7509 +- Spurious error when checking out a PR with untracked files. https://github.com/microsoft/vscode-pull-request-github/issues/7294 + +**_Thank You_** + +* [@krassowski (Michał Krassowski)](https://github.com/krassowski): Fix typo "will be replace" → "will be replaced" [PR #7540](https://github.com/microsoft/vscode-pull-request-github/pull/7540) + +## 0.116.1 + +### Fixes + +- Closing a PR causes a flurry of search API calls. https://github.com/microsoft/vscode-pull-request-github/issues/7537 +- Opening a PR description can cause a flurry of GitHub search API calls. https://github.com/microsoft/vscode-pull-request-github/issues/7542 + +## 0.116.0 + +### Changes + +- `#copilotCodingAgent` renders the pull requests it creates as a PR card. + +![pull request card in chat](./documentation/changelog/0.116.0/pr-card-in-chat.png) + +- When checking out a Copilot-authored PR, the Chat view no longer opens. +- You can dismiss the activity bar badge that indicates that Copilot has udpates to a PR by opening the PR description. +- We've simplified the button bar on the pull request description. + +![simplified button bar in pull request header](./documentation/changelog/0.116.0/simplified-pr-header-buttons.png) + +![pull request copy actions moved to link context menu](./documentation/changelog/0.116.0/pr-header-copy-actions.png) + +- You can see a summary of the Copilot coding agent's status in the "Copilot on My Behalf" tree item + +![coding agent summary](./documentation/changelog/0.116.0/coding-agent-status.png) + +- The commit links in the pull request description will open in VS Code in the multidiff editor instead of going to GitHub.com. +- The `[WIP]` prefix that Copilot adds to PR titles is no longer shown in the Pull Requests view. +- Using `@githubpr` is now sticky and will be pre-populated into the chat input for subsequent messages. +- Changes in a PR are pre-fetched when the PR description is opened. +- Pull requested created by Copilot will have `@copilot` as placeholder text in comment inputs. +- If your issue queries (setting `githubIssues.queries`) return no issues, a suggestion to configure your queries is offered. + +![suggestion in scm input to configure queries](./documentation/changelog/0.116.0/suggest-configure-queries.png) + +- The "Checkout Pull Request by Number" command will also accept a pull URL. + +### Fixes + +- Improve PR list view performance. https://github.com/microsoft/vscode-pull-request-github/issues/7141 +- "Cancel coding agent" could use status. https://github.com/microsoft/vscode-pull-request-github/issues/7451 +- Icon missing from the tools picker for coding agent. https://github.com/microsoft/vscode-pull-request-github/issues/7446 +- Copy GitHub Permalink doesn't work for GitHub Managed User (ghe.com). https://github.com/microsoft/vscode-pull-request-github/issues/7389 +- Closing a pull request doesn't remove it from the copilot on my behalf section. https://github.com/microsoft/vscode-pull-request-github/issues/7364 +- `@githubpr` doesn't know PR assignees. https://github.com/microsoft/vscode-pull-request-github/issues/7349 +- "Copilot on My Behalf" tooltip. https://github.com/microsoft/vscode-pull-request-github/issues/7276 +- Unassigning myself from a PR removes all comments from the PR editor. https://github.com/microsoft/vscode-pull-request-github/issues/7218 +- GitHub warning icons aren't well aligned in PR view. https://github.com/microsoft/vscode-pull-request-github/issues/7219 +- pr.openDescription command error. https://github.com/microsoft/vscode/issues/253900 +- Can't assign Copilot when creating new issue from GHPRI directly. https://github.com/microsoft/vscode-pull-request-github/issues/7033 +- Create PR shows error if there has been a previous PR on that branch. https://github.com/microsoft/vscode-pull-request-github/issues/7018 +- Changing around assignees for PRs causes timeline to hide until refresh. https://github.com/microsoft/vscode-pull-request-github/issues/7012 +- Can times in the timeline update periodically? https://github.com/microsoft/vscode-pull-request-github/issues/7006 +- Pull requests view should refresh if a new PR suddenly appears linked in an issue. https://github.com/microsoft/vscode-pull-request-github/issues/6898 +- Opening Issue editor should be instantaneous. https://github.com/microsoft/vscode-pull-request-github/issues/6863 + +## 0.114.2 + +### Fixes + +- Copilot never shows as assignee. https://github.com/microsoft/vscode-pull-request-github/issues/7324 + +## 0.114.1 + +### Fixes + +- Element with id Local Pull Request Branches is already registered. https://github.com/microsoft/vscode-pull-request-github/issues/7264 + +## 0.114.0 + +### Changes + +- We have expanded the integration with GitHub's [Copilot coding agent](https://docs.github.com/en/copilot/how-tos/agents/copilot-coding-agent) (enablement [instructions](https://docs.github.com/en/copilot/how-tos/agents/copilot-coding-agent/enabling-copilot-coding-agent)). You can see the status of all your Coding Agent pull requests in the "Pull Requests" view, and you'll get a badge indicating when a pull request from the Coding Agent has changes. + +![Pull Requests view with Copilot status](./documentation/changelog/0.114.0/copilot-pr-status.png) + +- Links for viewing the Coding Agent session log now open within VS Code instead of opening in the browser. + +![Coding Agent Session Log](./documentation/changelog/0.114.0/session-log.png) + +- The `#activePullRequest` tool in Copilot chat now knows more about the active pull request: changes and Coding Agent session information. This tool is automatically attached to chat when opening a pull request created through the coding agent experience, so you can maintain the context and keep working on the pull request if needed to. + +- When checking out a pull request which doesn't have any diff from the parent branch, the pull request description will be opened, instead of the changes when `"githubPullRequests.focusedMode"` is set to `"multiDiff"` or `"firstDiff"`. + +- You can start a new Coding Agent session by invoking the `#copilotCodingAgent` tool in chat. This tool automatically pushes pending changes to a remote branch and initiates a coding agent session from that branch along with the user's instruction. **Experimental:** Deeper UI integration can be enabled with the `setting(githubPullRequests.codingAgent.uiIntegration)` setting. Once enabled, a new **Delegate to coding agent** button appears in the Chat view for repositories that have the coding agent enabled. + +![Coding Agent Start](./documentation/changelog/0.114.0/coding-agent-start.png) + +### Fixes + +- Leaving a comment shows a pending comment box with an empty input. https://github.com/microsoft/vscode-pull-request-github/issues/7200 +- Lack of 👀 reaction in PR view is important for coding agent. https://github.com/microsoft/vscode-pull-request-github/issues/7213 +- Don't use comment icon to mean quote. https://github.com/microsoft/vscode-pull-request-github/issues/7185 +- PR view always expands and fetches "All Open". https://github.com/microsoft/vscode-pull-request-github/issues/7150 +- Expect option to Open issue in editor after creating new issue. https://github.com/microsoft/vscode-pull-request-github/issues/7034 +- Consider setting a default githubIssues.issueCompletionFormatScm. https://github.com/microsoft/vscode-pull-request-github/issues/7017 +- Times are inconsistent with .com. https://github.com/microsoft/vscode-pull-request-github/issues/7007 +- Padawan Start/Stop Events/Icons. https://github.com/microsoft/vscode-pull-request-github/issues/7004 +- Can't check out a local pull request branch. https://github.com/microsoft/vscode-pull-request-github/issues/6994 +- Unable to get the currently logged-in user. https://github.com/microsoft/vscode-pull-request-github/issues/6971 +- Stuck at creating fork. https://github.com/microsoft/vscode-pull-request-github/issues/6968 + +**_Thank You_** + +* [@dyhagho (Dyhagho Briceño)](https://github.com/dyhagho): fix: Allow Github.com auth when `github-enterprise.uri` is set [PR #7002](https://github.com/microsoft/vscode-pull-request-github/pull/7002) + +## 0.112.0 + +### Changes +- Images in comments from private repositories are now shown in pull request file comments. +- The "Notifications" view is now shown by default, collapsed. +- Issue and pull request links in the timeline an in the issue/pull request body are now opened in VS Code, rather than going to the browser. +- The "Assigned to Me" query in the "Pull Requests" view has been removed, and the "Local Pull Request Branches" and "All Open" queries can be removed using the setting `githubPullRequests.queries`. For repositories with Copilot, a "Copilot on My Behalf" query is added when the setting is unconfigured. +- Unassigned events are now shown in the timeline. +- Copilot "start working", "stop working", and "View Session" are now shown in the timeline. + +![Copilot start and stop working](./documentation/changelog/0.112.0/copilot-start-stop.png) + +### Fixes + +- Interference with interactive rebase. https://github.com/microsoft/vscode-pull-request-github/issues/4904 +- Closed PRs get associated with new branches of the same name. https://github.com/microsoft/vscode-pull-request-github/issues/6711 +- Fails to open PR's description in some repos on GitHub Enterprise. https://github.com/microsoft/vscode-pull-request-github/issues/6736 +- Support closing an issue. https://github.com/microsoft/vscode-pull-request-github/issues/6864 +- Pull request diff shows outdated diff. https://github.com/microsoft/vscode-pull-request-github/issues/6889 + +**_Thank You_** + +* [@kabel (Kevin Abel)](https://github.com/kabel): Allow verified GitHub emails when none are private [PR #6921](https://github.com/microsoft/vscode-pull-request-github/pull/6921) + +## 0.110.0 + +### Changes + +- In preparation for the release of [Project Padawan](https://github.blog/news-insights/product-news/github-copilot-the-agent-awakens/), we added support for assigning to Copilot in the issue webview and @-mentioning Copilot in comments within files. +- There's a new tool and context available in Copilot chat: `#activePullRequest`. This tool gives Copilot chat information about the pull request you have currently open in a webview (or checked out if no webview is open). + +![Ask Copilot to address the comments in the active pull request](./documentation/changelog/0.110.0/copilot-address-comments.png) + +- The issue webview will show when an issue is opened from the "Issues" view or from the notifications view. + +![Issue webview](./documentation/changelog/0.110.0/issue-webview.png) + +- We revisited the top level actions in the Notifications view to make it cleaner, and aligned the display of the Pull Requests view and the Issues view so that they're more consistent. + +- There's a new warning before you try to create a pull request when there's already a pull request open for the same branch. + +![Warning shown when there's already a pull request for a branch](./documentation/changelog/0.110.0/already-pr-branch.png) + +- Pull Request webviews and issue webviews are refreshed every 60 seconds when they are the active tab. +- The default action when adding a comment in a file is now to start a review rather than submit a single comment. +- There's a new action on the Notifications view to mark all pull request notifications with "trivial" updates as done. Enable the action with `githubPullRequests.experimental.notificationsMarkPullRequests`. +- Comment reactions are shown as readonly in the pull request webview (previously not shown at all). + +### Fixes + +- Extension fails to detect PR branch when using gh co . https://github.com/microsoft/vscode-pull-request-github/issues/6378 +- Extension fails to detect PR branch when using gh co - v0.109.2025040408. https://github.com/microsoft/vscode-pull-request-github/issues/6761 +- Element with id All Openhttps://github.com/microsoft/vscode/pull/238345 is already registered. https://github.com/microsoft/vscode-pull-request-github/issues/6615 +- Creating a new issue with keyboard only is disrupted by system dialog. https://github.com/microsoft/vscode-pull-request-github/issues/6666 +- GraphQL error invalid email address when merging PRs. https://github.com/microsoft/vscode-pull-request-github/issues/6696 +- Usability of PR Summarization in Chat. https://github.com/microsoft/vscode-pull-request-github/issues/6698 +- deleting branch after squashing PR not working anymore since vscode 1.98.0. https://github.com/microsoft/vscode-pull-request-github/issues/6699 +- Comments sometimes not resolvable. https://github.com/microsoft/vscode-pull-request-github/issues/6702 +- Can't search for full name when assigning issues. https://github.com/microsoft/vscode-pull-request-github/issues/6748 +- removed request for code owners. https://github.com/microsoft/vscode-pull-request-github/issues/6788 + +**_Thank You_** + +* [@kabel (Kevin Abel)](https://github.com/kabel): Fix merge email confirmation when git config fails [PR #6797](https://github.com/microsoft/vscode-pull-request-github/pull/6797) +* [@timrogers (Tim Rogers)](https://github.com/timrogers): When `copilot-swe-agent` is the author of a comment, render with the Copilot identity [PR #6794](https://github.com/microsoft/vscode-pull-request-github/pull/6794) + ## 0.108.0 ### Changes diff --git a/azure-pipeline.nightly.yml b/azure-pipeline.nightly.yml index 6840ab5736..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: @@ -31,6 +31,8 @@ extends: l10nSourcePaths: ./src + nodeVersion: "20.x" + buildSteps: - script: yarn install --frozen-lockfile --check-files displayName: Install dependencies diff --git a/azure-pipeline.pr.yml b/azure-pipeline.pr.yml index dd63669056..6a4422580d 100644 --- a/azure-pipeline.pr.yml +++ b/azure-pipeline.pr.yml @@ -2,13 +2,22 @@ jobs: - job: test_suite displayName: Test suite pool: - vmImage: 'macos-13' + vmImage: 'macos-15' steps: - template: scripts/ci/common-setup.yml - script: yarn run compile displayName: Compile + - script: npm run hygiene + displayName: Run hygiene checks + + - script: npm run lint + displayName: Run lint + + - script: yarn run check:commands + displayName: Verify command registrations + - script: yarn run test displayName: Run test suite env: diff --git a/azure-pipeline.release.yml b/azure-pipeline.release.yml index 8833bff0cb..1f8bb2c0d6 100644 --- a/azure-pipeline.release.yml +++ b/azure-pipeline.release.yml @@ -28,6 +28,8 @@ extends: l10nSourcePaths: ./src + nodeVersion: "20.x" + buildSteps: - script: yarn install --frozen-lockfile --check-files displayName: Install dependencies diff --git a/build/eslint-rules/index.js b/build/eslint-rules/index.js new file mode 100644 index 0000000000..85a144f2e8 --- /dev/null +++ b/build/eslint-rules/index.js @@ -0,0 +1,13 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +exports.rules = { + 'public-methods-well-defined-types': require('./public-methods-well-defined-types'), + 'no-any-except-union-method-signature': require('./no-any-except-union-method-signature'), + 'no-pr-in-user-strings': require('./no-pr-in-user-strings'), + 'no-cast-to-any': require('./no-cast-to-any') +}; \ No newline at end of file diff --git a/build/eslint-rules/no-any-except-union-method-signature.js b/build/eslint-rules/no-any-except-union-method-signature.js new file mode 100644 index 0000000000..8edf8e338f --- /dev/null +++ b/build/eslint-rules/no-any-except-union-method-signature.js @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +module.exports = { + meta: { + type: 'suggestion', + docs: { + description: 'Disallow the use of any except in union types within method signatures', + category: 'Best Practices', + recommended: true, + }, + fixable: null, + schema: [], + messages: { + unexpectedAny: 'Unexpected any. Use a more specific type instead.', + }, + }, + + create(context) { + return { + // Target the 'any' type annotation + TSAnyKeyword(node) { + // Get the parent nodes to determine context + const parent = node.parent; + + if (parent) { + // Check if this type is part of a method signature + let currentNode = parent; + let isMethodSignature = false; + + while (currentNode) { + // Check if we're in a method signature or function type + if ( + currentNode.type === 'TSMethodSignature' || + currentNode.type === 'TSFunctionType' || + currentNode.type === 'FunctionDeclaration' || + currentNode.type === 'FunctionExpression' || + currentNode.type === 'ArrowFunctionExpression' || + currentNode.type === 'MethodDefinition' + ) { + isMethodSignature = true; + break; + } + + currentNode = currentNode.parent; + } + + // If it's part of a method signature, it's allowed + if (isMethodSignature) { + return; + } + } + + // Report any other use of 'any' + context.report({ + node, + messageId: 'unexpectedAny', + }); + } + }; + } +}; diff --git a/build/eslint-rules/no-cast-to-any.js b/build/eslint-rules/no-cast-to-any.js new file mode 100644 index 0000000000..9ffd303b7a --- /dev/null +++ b/build/eslint-rules/no-cast-to-any.js @@ -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. + *--------------------------------------------------------------------------------------------*/ + +module.exports = { + + create(context) { + return { + 'TSTypeAssertion[typeAnnotation.type="TSAnyKeyword"], TSAsExpression[typeAnnotation.type="TSAnyKeyword"]': (node) => { + context.report({ + node, + message: `Avoid casting to 'any' type. Consider using a more specific type or type guards for better type safety.` + }); + } + }; + } +}; \ No newline at end of file diff --git a/build/eslint-rules/no-pr-in-user-strings.js b/build/eslint-rules/no-pr-in-user-strings.js new file mode 100644 index 0000000000..83665ad6fd --- /dev/null +++ b/build/eslint-rules/no-pr-in-user-strings.js @@ -0,0 +1,96 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +/** + * ESLint rule to detect the string "PR" in user-facing strings and suggest using "pull request" instead. + * This rule checks: + * - String literals passed to vscode.l10n.t() calls + * - String literals passed to l10n.t() calls + */ + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Detect "PR" in user-facing strings and suggest using "pull request" instead', + category: 'Best Practices', + recommended: true, + }, + schema: [], + messages: { + noPrInUserString: 'Use "pull request" instead of "PR" in user-facing strings. Found: {{foundText}}', + }, + }, + + create(context) { + /** + * Check if a string contains "PR" as a standalone word + */ + function containsPR(str) { + // Use word boundary regex to match "PR" as a standalone word + const prRegex = /\bPR\b/; + return prRegex.test(str); + } + + /** + * Check if a node is a call to vscode.l10n.t or l10n.t + */ + function isL10nTCall(node) { + if (node.type !== 'CallExpression') { + return false; + } + + const callee = node.callee; + + // Handle l10n.t() calls + if (callee.type === 'MemberExpression' && + callee.property && + callee.property.name === 't') { + + // Check for vscode.l10n.t + if (callee.object.type === 'MemberExpression' && + callee.object.object && + callee.object.object.name === 'vscode' && + callee.object.property && + callee.object.property.name === 'l10n') { + return true; + } + + // Check for l10n.t + if (callee.object.type === 'Identifier' && + callee.object.name === 'l10n') { + return true; + } + } + + return false; + } + + return { + // Check CallExpression nodes for l10n.t calls + CallExpression(node) { + if (isL10nTCall(node)) { + // Check the first argument (string literal) + if (node.arguments && node.arguments.length > 0) { + const firstArg = node.arguments[0]; + if (firstArg.type === 'Literal' && typeof firstArg.value === 'string') { + if (containsPR(firstArg.value)) { + context.report({ + node: firstArg, + messageId: 'noPrInUserString', + data: { + foundText: firstArg.value + } + }); + } + } + } + } + } + }; + } +}; \ No newline at end of file diff --git a/build/eslint-rules/public-methods-well-defined-types.js b/build/eslint-rules/public-methods-well-defined-types.js new file mode 100644 index 0000000000..8f1b18084e --- /dev/null +++ b/build/eslint-rules/public-methods-well-defined-types.js @@ -0,0 +1,172 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +'use strict'; + +/** + * ESLint rule to enforce that public methods in exported classes return well-defined types. + * This rule ensures that no inline type (object literal, anonymous type, etc.) is returned + * from any public method. + */ + +module.exports = { + meta: { + type: 'problem', + docs: { + description: 'Enforce that public methods return well-defined types (no inline types)', + category: 'TypeScript', + recommended: false, + }, + schema: [], + messages: { + inlineReturnType: 'Public method "{{methodName}}" should return a well-defined type, not an inline type. Consider defining an interface or type alias.', + }, + }, + + create(context) { + /** + * Check if a node represents an inline type that should be flagged + */ + function isInlineType(typeNode) { + if (!typeNode) return false; + + switch (typeNode.type) { + // Object type literals: { foo: string, bar: number } + case 'TSTypeLiteral': + return true; + + // Union types with inline object types: string | { foo: bar } + case 'TSUnionType': + return typeNode.types.some(isInlineType); + + // Intersection types with inline object types: Base & { foo: bar } + case 'TSIntersectionType': + return typeNode.types.some(isInlineType); + + // Tuple types: [string, number] + case 'TSTupleType': + return true; + + // Mapped types: { [K in keyof T]: U } + case 'TSMappedType': + return true; + + // Conditional types: T extends U ? X : Y (inline) + case 'TSConditionalType': + return true; + + // Type references with inline type arguments: Promise<{x: string}>, Array<{y: number}> + case 'TSTypeReference': + // ESLint 9 / @typescript-eslint v8 may expose generic instantiations on `typeArguments` instead of `typeParameters`. + // Support both shapes defensively. + const typeArgs = typeNode.typeParameters || typeNode.typeArguments; + if (typeArgs && typeArgs.params) { + return typeArgs.params.some(isInlineType); + } + return false; + + default: + return false; + } + } + + /** + * Check if a method is public (not private or protected) + */ + function isPublicMethod(node) { + // If no accessibility modifier is specified, it's public by default + if (!node.accessibility) return true; + return node.accessibility === 'public'; + } + + /** + * Check if a class is exported + */ + function isExportedClass(node) { + // Check if the class declaration itself is exported + if (node.parent && node.parent.type === 'ExportNamedDeclaration') { + return true; + } + // Check if it's a default export + if (node.parent && node.parent.type === 'ExportDefaultDeclaration') { + return true; + } + return false; + } + + return { + MethodDefinition(node) { + // Only check methods in exported classes + const classNode = node.parent.parent; // MethodDefinition -> ClassBody -> ClassDeclaration + if (!classNode || classNode.type !== 'ClassDeclaration' || !isExportedClass(classNode)) { + return; + } + + // Only check public methods + if (!isPublicMethod(node)) { + return; + } + + // Check if the method has a return type annotation + const functionNode = node.value; + if (!functionNode.returnType) { + return; // No explicit return type, skip + } + + const returnTypeNode = functionNode.returnType.typeAnnotation; + + // Check if the return type is an inline type + if (isInlineType(returnTypeNode)) { + const methodName = node.key.type === 'Identifier' ? node.key.name : ''; + context.report({ + node: functionNode.returnType, + messageId: 'inlineReturnType', + data: { + methodName: methodName, + }, + }); + } + }, + + // Also check arrow function properties that are public methods + PropertyDefinition(node) { + // Only check properties in exported classes + const classNode = node.parent.parent; // PropertyDefinition -> ClassBody -> ClassDeclaration + if (!classNode || classNode.type !== 'ClassDeclaration' || !isExportedClass(classNode)) { + return; + } + + // Only check public methods + if (!isPublicMethod(node)) { + return; + } + + // Check if the property is an arrow function + if (node.value && node.value.type === 'ArrowFunctionExpression') { + const arrowFunction = node.value; + + // Check if the arrow function has a return type annotation + if (!arrowFunction.returnType) { + return; // No explicit return type, skip + } + + const returnTypeNode = arrowFunction.returnType.typeAnnotation; + + // Check if the return type is an inline type + if (isInlineType(returnTypeNode)) { + const methodName = node.key.type === 'Identifier' ? node.key.name : ''; + context.report({ + node: arrowFunction.returnType, + messageId: 'inlineReturnType', + data: { + methodName: methodName, + }, + }); + } + } + } + }; + }, +}; \ No newline at end of file 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/hygiene.js b/build/hygiene.js index 9cd9eb298a..76df48120e 100644 --- a/build/hygiene.js +++ b/build/hygiene.js @@ -56,7 +56,7 @@ function hygiene(some) { const indentation = es.through(function (file) { const lines = file.__lines; - lines.forEach((line, i) => { + lines?.forEach((line, i) => { if (/^\s*$/.test(line)) { // empty or whitespace lines are OK } else if (/^[\t]*[^\s]/.test(line)) { diff --git a/build/update-codicons.ts b/build/update-codicons.ts new file mode 100644 index 0000000000..9e504fb6fa --- /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/main/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/common/sessionParsing.ts b/common/sessionParsing.ts new file mode 100644 index 0000000000..0ba92071da --- /dev/null +++ b/common/sessionParsing.ts @@ -0,0 +1,329 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface SessionResponseLogChunk { + choices: Array<{ + finish_reason?: 'tool_calls' | 'null' | (string & {}); + delta: { + content?: string; + role: 'assistant' | (string & {}); + tool_calls?: Array<{ + function: { + arguments: string; + name: string; + }; + id: string; + type: string; + index: number; + }>; + }; + }>; + created: number; + id: string; + usage: { + completion_tokens: number; + prompt_tokens: number; + prompt_tokens_details: { + cached_tokens: number; + }; + total_tokens: number; + }; + model: string; + object: string; +} + +export interface ParsedToolCall { + type: 'str_replace_editor' | 'think' | 'bash' | 'report_progress' | 'unknown'; + name: string; + // args: any; + content: string; + command?: string; // For str_replace_editor +} + +export interface ParsedChoice { + type: 'assistant_content' | 'tool_call' | 'pr_title'; + content?: string; + toolCall?: ParsedToolCall; + finishReason?: string; +} + +export interface ParsedToolCallDetails { + toolName: string; + invocationMessage: string; + pastTenseMessage?: string; + originMessage?: string; + toolSpecificData?: StrReplaceEditorToolData | BashToolData; +} + +export interface StrReplaceEditorToolData { + command: 'view' | 'edit' | string; + filePath?: string; + fileLabel?: string; + parsedContent?: { content: string; fileA: string | undefined; fileB: string | undefined; }; + viewRange?: { start: number, end: number } +} + +export namespace StrReplaceEditorToolData { + export function is(value: any): value is StrReplaceEditorToolData { + return value && (typeof value.command === 'string'); + } +} + +export interface BashToolData { + commandLine: { + original: string; + }; + language: 'bash'; +} + +/** + * Parse diff content and extract file information + */ +export function parseDiff(content: string): { content: string; fileA: string | undefined; fileB: string | undefined; } | undefined { + const lines = content.split(/\r?\n/g); + let fileA: string | undefined; + let fileB: string | undefined; + + let startDiffLineIndex = -1; + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + if (line.startsWith('diff --git')) { + const match = line.match(/^diff --git a\/(.+?) b\/(.+)$/); + if (match) { + fileA = match[1]; + fileB = match[2]; + } + } else if (line.startsWith('@@ ')) { + startDiffLineIndex = i + 1; + break; + } + } + if (startDiffLineIndex < 0) { + return undefined; + } + + return { + content: lines.slice(startDiffLineIndex).join('\n'), + fileA: typeof fileA === 'string' ? '/' + fileA : undefined, + fileB: typeof fileB === 'string' ? '/' + fileB : undefined + }; +} + +export function parseRange(view_range: unknown): { start: number, end: number } | undefined { + if (!view_range) { + return undefined; + } + + if (!Array.isArray(view_range)) { + return undefined; + } + + if (view_range.length !== 2) { + return undefined; + } + + const start = view_range[0]; + const end = view_range[1]; + + if (typeof start !== 'number' || typeof end !== 'number') { + return undefined; + } + + return { + start, + end + }; +} + + + +/** + * Convert absolute file path to relative file label + * File paths are absolute and look like: `/home/runner/work/repo/repo/` + */ +export function toFileLabel(file: string): string { + const parts = file.split('/'); + return parts.slice(6).join('/'); +} + +/** + * Parse tool call arguments and return normalized tool details + */ +export function parseToolCallDetails( + toolCall: { + function: { name: string; arguments: string }; + id: string; + type: string; + index: number; + }, + content: string +): ParsedToolCallDetails { + // Parse arguments once with graceful fallback + let args: { command?: string, path?: string, prDescription?: string, commitMessage?: string, view_range?: unknown } = {}; + try { args = toolCall.function.arguments ? JSON.parse(toolCall.function.arguments) : {}; } catch { /* ignore */ } + + const name = toolCall.function.name; + + // Small focused helpers to remove duplication while preserving behavior + const buildReadDetails = (filePath: string | undefined, parsedRange: { start: number, end: number } | undefined, opts?: { parsedContent?: { content: string; fileA: string | undefined; fileB: string | undefined; } }): ParsedToolCallDetails => { + const fileLabel = filePath && toFileLabel(filePath); + if (fileLabel === undefined || fileLabel === '') { + return { toolName: 'Read repository', invocationMessage: 'Read repository', pastTenseMessage: 'Read repository' }; + } + const rangeSuffix = parsedRange ? `, lines ${parsedRange.start} to ${parsedRange.end}` : ''; + // Default helper returns bracket variant (used for generic view). Plain variant handled separately for str_replace_editor non-diff. + return { + toolName: 'Read', + invocationMessage: `Read [](${fileLabel})${rangeSuffix}`, + pastTenseMessage: `Read [](${fileLabel})${rangeSuffix}`, + toolSpecificData: { + command: 'view', + filePath: filePath, + fileLabel: fileLabel, + parsedContent: opts?.parsedContent, + viewRange: parsedRange + } + }; + }; + + const buildEditDetails = (filePath: string | undefined, command: string, parsedRange: { start: number, end: number } | undefined, opts?: { defaultName?: string }): ParsedToolCallDetails => { + const fileLabel = filePath && toFileLabel(filePath); + const rangeSuffix = parsedRange ? `, lines ${parsedRange.start} to ${parsedRange.end}` : ''; + let invocationMessage: string; + let pastTenseMessage: string; + if (fileLabel) { + invocationMessage = `Edit [](${fileLabel})${rangeSuffix}`; + pastTenseMessage = `Edit [](${fileLabel})${rangeSuffix}`; + } else { + if (opts?.defaultName === 'Create') { + invocationMessage = pastTenseMessage = `Create File ${filePath}`; + } else { + invocationMessage = pastTenseMessage = (opts?.defaultName || 'Edit'); + } + invocationMessage += rangeSuffix; + pastTenseMessage += rangeSuffix; + } + + return { + toolName: opts?.defaultName || 'Edit', + invocationMessage, + pastTenseMessage, + toolSpecificData: fileLabel ? { + command: command || (opts?.defaultName === 'Create' ? 'create' : (command || 'edit')), + filePath: filePath, + fileLabel: fileLabel, + viewRange: parsedRange + } : undefined + }; + }; + + const buildStrReplaceDetails = (filePath: string | undefined): ParsedToolCallDetails => { + const fileLabel = filePath && toFileLabel(filePath); + const message = fileLabel ? `Edit [](${fileLabel})` : `Edit ${filePath}`; + return { + toolName: 'Edit', + invocationMessage: message, + pastTenseMessage: message, + toolSpecificData: fileLabel ? { command: 'str_replace', filePath, fileLabel } : undefined + }; + }; + + const buildCreateDetails = (filePath: string | undefined): ParsedToolCallDetails => { + const fileLabel = filePath && toFileLabel(filePath); + const message = fileLabel ? `Create [](${fileLabel})` : `Create File ${filePath}`; + return { + toolName: 'Create', + invocationMessage: message, + pastTenseMessage: message, + toolSpecificData: fileLabel ? { command: 'create', filePath, fileLabel } : undefined + }; + }; + + const buildBashDetails = (bashArgs: typeof args, contentStr: string): ParsedToolCallDetails => { + const command = bashArgs.command ? `$ ${bashArgs.command}` : undefined; + const bashContent = [command, contentStr].filter(Boolean).join('\n'); + const details: ParsedToolCallDetails = { toolName: 'Run Bash command', invocationMessage: bashContent || 'Run Bash command' }; + if (bashArgs.command) { details.toolSpecificData = { commandLine: { original: bashArgs.command }, language: 'bash' }; } + return details; + }; + + switch (name) { + case 'str_replace_editor': { + if (args.command === 'view') { + const parsedContent = parseDiff(content); + const parsedRange = parseRange(args.view_range); + if (parsedContent) { + const file = parsedContent.fileA ?? parsedContent.fileB; + const fileLabel = file && toFileLabel(file); + if (fileLabel === '') { + return { toolName: 'Read repository', invocationMessage: 'Read repository', pastTenseMessage: 'Read repository' }; + } else if (fileLabel === undefined) { + return { toolName: 'Read', invocationMessage: 'Read repository', pastTenseMessage: 'Read repository' }; + } else { + const rangeSuffix = parsedRange ? `, lines ${parsedRange.start} to ${parsedRange.end}` : ''; + return { + toolName: 'Read', + invocationMessage: `Read [](${fileLabel})${rangeSuffix}`, + pastTenseMessage: `Read [](${fileLabel})${rangeSuffix}`, + toolSpecificData: { command: 'view', filePath: file, fileLabel, parsedContent, viewRange: parsedRange } + }; + } + } + // No diff parsed: use PLAIN (non-bracket) variant for str_replace_editor views + const plainRange = parseRange(args.view_range); + const fp = args.path; const fl = fp && toFileLabel(fp); + if (fl === undefined || fl === '') { + return { toolName: 'Read repository', invocationMessage: 'Read repository', pastTenseMessage: 'Read repository' }; + } + const suffix = plainRange ? `, lines ${plainRange.start} to ${plainRange.end}` : ''; + return { + toolName: 'Read', + invocationMessage: `Read ${fl}${suffix}`, + pastTenseMessage: `Read ${fl}${suffix}`, + toolSpecificData: { command: 'view', filePath: fp, fileLabel: fl, viewRange: plainRange } + }; + } + return buildEditDetails(args.path, args.command || 'edit', parseRange(args.view_range)); + } + case 'str_replace': + return buildStrReplaceDetails(args.path); + case 'create': + return buildCreateDetails(args.path); + case 'view': + return buildReadDetails(args.path, parseRange(args.view_range)); // generic view always bracket variant + case 'think': { + const thought = (args as unknown as { thought?: string }).thought || content || 'Thought'; + return { toolName: 'think', invocationMessage: thought }; + } + case 'report_progress': { + const details: ParsedToolCallDetails = { toolName: 'Progress Update', invocationMessage: `${args.prDescription}` || content || 'Progress Update' }; + if (args.commitMessage) { details.originMessage = `Commit: ${args.commitMessage}`; } + return details; + } + case 'bash': + return buildBashDetails(args, content); + case 'read_bash': + return { toolName: 'read_bash', invocationMessage: 'Read logs from Bash session' }; + case 'stop_bash': + return { toolName: 'stop_bash', invocationMessage: 'Stop Bash session' }; + default: + return { toolName: name || 'unknown', invocationMessage: content || name || 'unknown' }; + } +} + +/** + * Parse raw session logs text into structured log chunks + */ +export function parseSessionLogs(rawText: string): SessionResponseLogChunk[] { + const parts = rawText + .split(/\r?\n/) + .filter(part => part.startsWith('data: ')) + .map(part => { + const trimmed = part.slice('data: '.length).trim(); + return JSON.parse(trimmed); + }); + + return parts as SessionResponseLogChunk[]; +} diff --git a/common/types.ts b/common/types.ts new file mode 100644 index 0000000000..094d680957 --- /dev/null +++ b/common/types.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export interface RemoteInfo { + owner: string; + repositoryName: string; +} diff --git a/common/views.ts b/common/views.ts index 500e207279..707db9097b 100644 --- a/common/views.ts +++ b/common/views.ts @@ -3,13 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IAccount, ILabel, IMilestone, IProject, ITeam, MergeMethod, MergeMethodsAvailability } from '../src/github/interface'; -import { PreReviewState } from '../src/github/views'; - -export interface RemoteInfo { - owner: string; - repositoryName: string; -} +import { RemoteInfo } from './types'; +import { ClosedEvent, CommentEvent } from '../src/common/timelineEvent'; +import { GithubItemStateEnum, IAccount, ILabel, IMilestone, IProject, ITeam, MergeMethod, MergeMethodsAvailability } from '../src/github/interface'; +import { DisplayLabel, PreReviewState } from '../src/github/views'; export interface CreateParams { availableBaseRemotes: RemoteInfo[]; @@ -107,7 +104,7 @@ export interface CreateParamsNew { compareBranch?: string; isDraftDefault: boolean; isDraft?: boolean; - labels?: ILabel[]; + labels?: DisplayLabel[]; projects?: IProject[]; assignees?: IAccount[]; reviewers?: (IAccount | ITeam)[]; @@ -168,4 +165,14 @@ export interface TitleAndDescriptionResult { description: string | undefined; } +export interface CloseResult { + state: GithubItemStateEnum; + commentEvent?: CommentEvent; + closeEvent: ClosedEvent; +} + +export interface OpenCommitChangesArgs { + commitSha: string; +} + // #endregion \ No newline at end of file diff --git a/documentation/IssueFeatures.md b/documentation/IssueFeatures.md index 5cd6efef5f..72abd5f90e 100644 --- a/documentation/IssueFeatures.md +++ b/documentation/IssueFeatures.md @@ -1,8 +1,15 @@ We've added some experimental GitHub issue features. -# Code actions +# Code actions and CodeLens -Wherever there is a `TODO` comment in your code, the **Create Issue from Comment** code action will show. This takes your text selection, and creates a GitHub issue with the selection as a permalink in the issue body. It also inserts the issue number after the `TODO`. +Wherever there is a `TODO` comment in your code, two actions are available: + +1. **CodeLens**: Clickable actions appear directly above the TODO comment line for quick access +2. **Code actions**: The same actions are available via the lightbulb quick fix menu + +Both provide two options: +- **Create Issue from Comment**: Takes your text selection and creates a GitHub issue with the selection as a permalink in the issue body. It also inserts the issue number after the `TODO`. +- **Delegate to coding agent**: Starts a Copilot coding agent session to work on the TODO task (when available) ![Create Issue from Comment](images/createIssueFromComment.gif) diff --git a/documentation/changelog/0.110.0/already-pr-branch.png b/documentation/changelog/0.110.0/already-pr-branch.png new file mode 100644 index 0000000000000000000000000000000000000000..ad27d0d749961a3109127091576dc1944946601c GIT binary patch literal 31521 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%0A+^Hi7(0KwkRAo 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

AD?FG)hkpH(VV3S-o6qHh&Z} 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 IDbv9J1qWL3i~s-t literal 0 HcmV?d00001 diff --git a/documentation/changelog/0.110.0/copilot-address-comments.png b/documentation/changelog/0.110.0/copilot-address-comments.png new file mode 100644 index 0000000000000000000000000000000000000000..a4b30e1ca09a33121784fc042689190d5b40cf03 GIT binary patch literal 94476 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

#VT2XL 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 J<(@<2zX0fG+d}{V literal 0 HcmV?d00001 diff --git a/documentation/changelog/0.110.0/issue-webview.png b/documentation/changelog/0.110.0/issue-webview.png new file mode 100644 index 0000000000000000000000000000000000000000..f561e7f1537b8853373538f0f7875f1954fbfbb2 GIT binary patch literal 384786 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

+o(F`;AjvHhifOx*( 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%0D+9|?A4(VTX|~F2mI0B;0l8?CEoZn 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%0Dgr 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 literal 0 HcmV?d00001 diff --git a/documentation/changelog/0.112.0/copilot-start-stop.png b/documentation/changelog/0.112.0/copilot-start-stop.png new file mode 100644 index 0000000000000000000000000000000000000000..82c71c2a3f18e81de7e0a5faaf83a2c56bfb5665 GIT binary patch literal 45070 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%0AH)S*UQ~!g)gnzY6&h>W;3C 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

A1K`5h6>JYnMCrK4{Ca4qbqe9O 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 literal 0 HcmV?d00001 diff --git a/documentation/changelog/0.116.0/coding-agent-status.png b/documentation/changelog/0.116.0/coding-agent-status.png new file mode 100644 index 0000000000000000000000000000000000000000..0552153d58131b70b418917b5937264a579f97e9 GIT binary patch literal 20250 zcmeGEWl&ws7B-6F?(S~E3GNas1P$))9)h!Q*9aOMf&_PGA;E$}kcGQ192T;WL*BjL zz2ANApZnvxRrge#s#%j>b9T?!J$iJHF`j2kV$@X?Fww}+U|?V{-zv(!hk=2EzLbBU zAibO$^)51B{=j;?SCE0J8mBmbfuVtUD=V$#V{)ABZ=~h<^laK1&(B&K97{qoj2FXc zF6&VIgAqX7Z7oOg+K!KAks^g}@CO~73FnSnJ__5Kv;+QTjYitVbaz2LI#e zK0+~)u08b>*a`lh>rxC-wG%N?a18%AOq8T_!Ov$uZ%jmtu;TvlkYI#GY}nXGh^1p< z@;@%;W#i_7Ty2r|+}{4Y3oYQ0h-i-x!4YI%gr+A|G|^xv~mpDXDLR-=pR& zOFV+IeonZ$N~1-)D^X*ve*fn$FsPsYJ{r*ADmB3)bgMUZST-GB91)fDV2u0~ac&H1 zt2cy2C`a(7TDaApoEoLcEq#a!KC0~IV1oBwd!~aKHDG%H{ z@}$8b5s$6U(~j~1^d)sp_!g$W$gJ9<&q)MyWdAGWmjQ_&7^IP7ZyVj&9$Q!07h0IE zt_V`Zb^lQMpITHjlf!)yFg$s))Dfv+Wiy3NzO`JG%SBymuKw$(S2n$i2~na@s`QeI z(0}X+-I#EdYT)vKD`&Nw+2{izw=LXsCFZ*_)e}M>0Wl-MoHC%C8M344c~E~kaJ9|& zPx-3QPO|m``DpYR92H82lj)D!1W)q~lrF!l>v|WCFyMHNhzr^1E{6_V81seP|;{uF}K@bW-6?|**K`=917 z9Tfh@4*dVz4hC0&n2G*5DR?v?O=`gwUB^qG=>IWSFI9};UZhC!KVtYlD@6{KJbwy2 z{QH9PNf+l=P0noHzS$D#mQggtAE_bP&Ied^;;yt(wWl1Z+d}^_ii-##Pu^l7gZMwq zb#5R|sC6cWG0XG`rr)B(gXMyvrROgZeSCN2DV*>NEtw6;U(Dg04KCzA*0$Rp6(MP` z*Oz`08i3m542rIl>&{C?K0B-2ID54gf*4ol4i_x9|LlVys#77gtLr&7#6YUnB|w({ zjp(o(t;QT85B}XG#(;`H#t3SwM;KdRJF*!gdD;_hIn@mu5Sr1oxM?G<1=j5N*;QC_ zn8sRk?PS!;{VnkjFQvSj&dZv!T~(pm`>#F=1qEY@w<8wcWwq#Tl14u9Z^N?ZQiN4e z2DfPrx*5!rUke3wpgfN~lC-mlbC@C1Y}~-(&@6qolwxps(8f^~V!BqaEDyKmqS+y% zUT?Pqo$sPqH3jD-wgpiYWAo(u?Cq6ub9=WL^J0YgCtDsR6W5MO`MPq-)Siyb?STFx zRRh8h+Mqxz?59IIFUd=cxpJ-YD?kcE)#=*aM=yDC}L0> z+xu9?8hQw;0QWcyOuwC84 zFPmh`fmLA|JjOV^Mt2%W78HF>SYS{AOnVZ-Ra+&FK{)iYa3{)DbD_^S;I}M+% zV%+mcpJvb!Gpg4mj#!f&9nuDRg^Fne07YRK{6i7tvgfW~Oz&)%lgDSCf)DOS@>_fl z>|X7u>x9|yvlxcFPoY~E4!wHlO352;F&UEpkRww7U$Nwu_|htVp%!x!F{o0AJ`mgmMpm9Rpc-mm8$@9o0p2-bHN zaxmxwEYFT(q7Rw)Iooy+OdGZaeg_Zt8iCdO`PIRKWMi1MuJT$RQ4)z-@siC4)^8O9 zGEN^-zMf!p7z7WCPTEjFbjB9?MuYbwrs^=&gCK({6k#!H-`96>B-2G; zO3P*3SD7B1%XJJ~EVU}}pHn1^ z$8TVdc8i~9t2bQ>v-?&r(xL&p$s18pmMv8qENZKXHU|2E$0U_T2{W8Ru{AC_wXTCJ zzlQzFBt!|I@`nyZ-YD&VTct7CZgJ^F3X8S)GLyK1@CFK&_iVrRO+SYzqykT2 z4XP5SnmYWQ#3!$H^dd$qOi_kO9``Yn<4>%l&e&wS*v5CB@zLjg0qwD*8UVGfw%*GN z<3NMarQ5T2-DC?xqC2dDq`Ck|_9+;hLj?aoz90Cx&0u4thx6YSREihRP3@CXhKESX z@xYvT?1-ACs4zkjM{cyVw&>Y<{ep{j>T4(=142N^^+9>fQYG08aic`7Ya(SF)D@*? zYT|Y67!y=oNM4O1fh8bedDQq;o;UD((frZPVgkv@Ib>AM;K3ytkdHY5^iu?*&$2ve`H4Q*U;uAW#XfrEZBPP=EFtyhD2!wg7Drf< zo>{^Qj^R3tkVZO_S^XusX#CpK_r@GNEB|r%8mQoM2!~Wi)5-BYjnwRmxzB!@c)jX~(+>ALqX`O~m##Aj$W5>0$z3G}e+VWN6E}`yeYbX& z1q(tH4d<5^(7#<+Z8rYfvQ8GGwlG7MGRY_8%Hg{jkgD-lG9p0;9PNX1PahqkT*Mqm zY%tU^!=M%sluwPIScV6CEu8Td-}SKH0k^5TFbOVP0#An`=&&W%0z4DID)M;0kWFQ< z;ZD>1Ws2@jx$dFojBR#3(##$<2_Y2DrBSij=J*?EevaQ8V!C+s=36%Nzud8qVw@KR z5daXIR8Pe4ns&F8kJKJ>aaoj)pn6*ArVLZ2x3Mh2M*)5e`ZTg^zc~)B!mr1BYdu36 zyKK2c;Mlb4xN@~HkEMwF5D4?lt1eSh!^PeJp!d~);wajpTo7r@qcpX1CX>h=tOYiV zb2x`#6OA(BM|1K2@%q>b;@*~rTh&fjl2(dM>5-iHTR*sJtF5Je&5s%bO!}A4CUuGb zr+-NxT`Z``&3Rn5Wy588XX z{S*6)zX`Y}mJ1Z8ds(x2>y#9W-^UeBhs05WS!U3Y4OLefk2A~B5watq>wlJ6nV$;T zNS~D7e>9CMxpf#86+gdubEUp>*}P%4WwNad7R_t7Ywepb5>HA(jwIc!#RnRN zGmA39*s*{KjCsadF1{aZX3qVs^ti{qwbctn#IhH}^A_eZ=@JaiVxjo3rz~SOmhg#| zq+kxavmsM=$LM2I=Ouz+A+N34L8C6tiBak~nScAjmDy6y$)xFlN3%G14SEui6mL%A zM%In2+F8RBesg2L`&sY{e)2lt#fLJEF|rMn{Ew*uJ>P5ILM67Gg*gunNnR|~kHDq` zKZ9(LD>;bs4OitV1~*{Gx&|hb;cwN!znx`Zzw@B*%i`*3!X&S4L?LO+WcI@KC#e}>xTofu*aDQb{@d~&oBc@LGRE?J;=0uKIyUXy|zOu)GAW?w7P>`e;j z5iAcrgR%Ai`#kKd;-VbGt(S;oRaMB0WmpP@n4n-@y-CW0Nf$*4oE)s#8Y3LKx|Pe#*e1 zC3h7-l>j=hi}S7#9?q14y*pUi{n+d~1>IjOquZMP(=CEYCp`u6Y=*_?n=?>B3`saw zjR1g3u~^Qy}5O1j`j#lt3lF8}RA zFrTqNgxihiUwm*fu2iP~v$s$DMUbdeVDDf z?m)ahkX(SJUG;w=LA00na2)Y(|NE~YoV|bPi;08Y{|!5P87uOC9O6HP?stDA_QPD5 z{ePP0f%Yi=b{zk8gNyM+LPe1~HUHB**do{CZ)5ha8y(3o{ZBi574``4iNDhv_L^9+ z56jkq(*DA1PF?_>v$2Aa1<|HsxR5^>4}TL`>%mCvO2iQbVWXk#y1H_UD}{xBCTy~W z;r)kg-%=HwH-C?H-56o986i|CeL?Q|UyP|Sd3u>amEP>E%wX##VsQP=8hehQo z%gg%XK{dYe+~f=07EnbsOT^5!A5r3CWkzQtVMRzfsJGZ)3A|-1U!9IESNUsEB z9oVf<7IZw%8KF|mq{FhgE=yVEWl0SG3xdcq#SSz_9`7ZG#K6+g0X9R1Oh$qG% zR&L0v?~-tBw*`aRjGzYD1!vb^!qZpTkLw!t>3qZCoodx~08t=OE%v$(ClT;dm_fUb z3JGLz?-PYRoEQg0WoKyZ5z%1-{`r$)ctk|^8ph0Sb#HI8aZus3g@vRA-LAxIg0sm? z>R7P4X}QUko? zR`ai5itUusvm^C9Q0r`~x~YhfmnlX)k0=BPCsqk#Vx2W>r{T#e#A9^nnTkNJ0ZW!A zRs&I|&rud@Lw)9?e^kt_AAz$PbSjC$V~f2h)rcN`zZPwNOW+T5-ykb52qd}O4`2wN z&F8k15FiwGr+05AZLW5twxE_nRwt(n%ZaK#yDD%0NNkw@>XOG=Q9L$Il$NzDqhBJJ zC#gzGM(>AV;0*^8M4`k|-@-Im1KbXX5^kH!0J)~h8@??H3^aCs4zowWz-YLNw&+SB ziCX!i{vk{_E%6r5OvylxA~%SH{b z=)7ncr~8@oU%5A`@}tjg!qkvqBxeho+7RxZ^`e&P%Mim@1|Bq8Y$PNOpfF5Wap6I> zU-f`(9WbJ=c|QlqAGCla0|k(IO@5_xHPAy;q@&e<$c4x^Awdc~m54YGqim2pI$lRE z*#HM(QvS@s!~5&Jj7djDwD&FvZ))(`)lHKQ(VT-|<4Uvdh#XD>VPfFJ%!t}NexUWA zR`(8MtFC?-0uU^#H$f$Wx1*JuOj|Ho_xK)PCv^Wr*POhuBNCWnqT$yeg6c{{+cNcc zYki4Ws6L)H+KvjhkK-w}MXlR=nc5Z4tWl%(`XnI9vzEwa&~ijts#U&>c74!;tTF|8 z)a1$QoBATq^OK*`cy(R!hrS)RRX?i^-Lh|vHnI$=1Gg=Tx6ognCZ&BnS=ylQx+Nr6 zY-4fim3LW-bLT3(*#Jk0V|_52$Hvzc>W!l>)qQ$c8*3l8-1nj)=l`HrQb)9Ir2P_; z2aBlb;?4xUoFq68v6F+%xAPZ8An@)u&~=((lya7^KhuWh=ek2r&_|JCW=KR5jol6L zx(~-Xz7HnS;8lcc6xop=LLG>UvqE0BfeUM72k>T-TZk1#Vh`<8?ML`nIa&vGaApC| z(=Wb0Z)ArgBD0{kgk9xm~q*A#7Fyqc^e9+>GpY zM`5>#l9w_(nPgYQk$;@SN~yd;L+_HgX6&9`dsj4M45V1vL5yW^;3@#y!GwwihNxj_ z7vx*wAYmm)S0{Q3HMk=QP`PpUXj=rCOJ|W(7V8&JX< z+zs8NpN2Vo4!&|5@$b%yg@;%^=~7>ubo8eZ1@aG>@*-#M&{!QcBhp59iX(}KZE-V+ zz|6|EvJmJnU^zQFB1?}(hDYi@eR@7H!65sXtrA>C_kf^_wd0XjGszn59WX3=g^JqK zwKgU{ymBM;%Fx7XhN+u>x2;E!Dh-bMD*;l_?kH&o(2LN)VSMT1`vvo=cAuWIoX^ei z1C^G~6T^+NulzeYmZt@avfjJD0l?Qa+iZ02K{=dX6_}j8B|fZZ*NeI)t)@LlbhjX7 zl+x{7;sAeOqtm0DteCkz0E+8a&RpOf&ThHwUcC*ExW-?qF2y<3TS;0SZ%=Jh51MxU z^7+z5z=h)VsD-0-K;ogUU>VU90km=tIa8eWkMZ$C zRJ@`xM-SqF)wE%vy$Ut>K7vTN_Kfad`$2)%$QzpT^h%vnonOO>yE?Jb~o>cJ-#fgGl8>eenG>lcBdDv*qws3!*EKS!Bd9m`+$-`^;4ej zgQf=~5pl%q0@_?mMjMsRUoFzxcq@D&qHx63!h9}+arv%(i*ooOpM{!qtC^`|RUpGz2HLbm}tKw#+^&mGaMa+>Z_b6&Jj09t_!m|r<>>i9E8?;k&Wk&5OHT87K>-<4A5WvKZ{!7O3!n zQu4U}Fd}2K6{ad0z{ApLiz^`Bh!!D&(hx@q^<27)nAa-9g{C%>OR~n1E}R0FV4VW| zII#2w3b7{n%|Y}Nw8EQ0A8a`Xy*3i0;_Z7AX}v+13hexr|{?*mb1X5+ec=I>mACpb|Z2nXb4iTFKb~`AMNm z37t>-F~*g*x&uGl@SduhNotrODH3*wZFSu^M0BC#UspHfPdOr~yv3h=j|+Np9<{cG)C5pCkH(-UdBuWGmx&MTD|C0~OJFd%>G0-L#e=+5z#Cls z6B=&a+NwginCpTdJ?g)60Z!UxT*PC&3D$3u{OSv_6mFaq&V?1w*`57iNqDWX@Oo7) z?yypX^t~`<8>LCo3BpXqXduUT-RShwvAPPP-6zqHrt&8A_kByy{kujs>)B0!lMLMh z{6jvt1zqCaU*cCi8eJpv`tgfU;f!PKCr4zrsz6DU0iCmkxcu0*7Hgl?OF-Tb{K2k& zg-#8eFDT=KE}YPwc|O;oDAHFVvFH${qi#)q&7O2}HE~D>H}!&yQ+9uefo)W);p^$W zMCoA0ukpCGuA>RedMVihDcOKGE@C|iJvBLN3&a8IDWjqbLvnO@KNIz0yo_tBttML2 z<09A`xXsPc?g>QEMm>$e$ZP!6N!UkLEa^+!#Iu5Cp?O#PX|) zUXTPazm$lO>^nIV=SR;oFk)gBVv z=stcU0eS^ujY~Ajz{@kNM`ug3(nsFU0VVEm*=~I=a=^FHB({`kNMenP!>z@}=x`-v z!n|;&$aDNkDo?1eZkISBy3M2j+s3HxbI<>Z7-+8HSgk@XNcAI+!00F{(hvWTzrffO z%r-wm@49zGB0N0*)~t%R)>Woj)P@FNg`+y`BKGNb!#(A1g#E+#G43>t1HIH4VnS-= zpYKlj$3+=^+FO@>z{gaMVeXWz^ZR#T^aY;=ea{(Z)Y5Q*#6vK@Jawy*X(H7lQ4c`o zY;FL9$1njeNL|AngHqJ10RcnQk1c<%{!|gn<|1v-b6N#Doi2*wMDC<9wYds_ zq&SHkwjj2hlVp3cEBp!hRu%F*V;^nyoMh~#@fFDl;b?KqRx&7 zaTe6W1!&r!%i#d4a@EtI3}L{zisD|4Sd+=_ghDfmXJ;t8YFB z=)zSUY~OKXe3t%2610bM)kquQe(=@(gk1D=}X=r*rvc5>@_MFIS=LfbE9vZS4 z=*{m>1Ut+eHdrfaedaS?1W9 zn-%5lU&v*xz5SAbuTO{{E+c-ZP@v49N&=M(?V`zvs9TGe}&66V8{BSP592UD)W4vpIC2FBZS^jDG| zs|ybGZ%#`&J&|bgIi1(S(-AicRd~w=dxsW~U(lZu!7TW{ked>O#YT{~(qJ}RD*nek zv#$X{Uqolm$mkKaE5yjCgXneZFi;za;1Vq`4c<{NVE+ZOgfzW-$q4w}R0z7^(|}(8 z;^bMO%t>GRuwwp>VVxCL;pw(&uK(iSpj)28pSqW!FDKdKIa_ewGSLwhIUQ^i!pS=T zNdy$V?-DjS=`(y|bjrVTBEHGIaNw`5uBwXuJYNN1$^DcBEQQjN31XtjWHSl1W{}%n zGpq~1R(1i^r!shLo$5$iS75mQ4S>Q@zCcPjIYcifCI@;*3A>tnDGtaWE1|1$A>z=+ z<@4-+!7wSq7r1Cmhhs4Czt{d`9{fSbvgrN;8~!Q$pXJ+Mp7{Thw2BpOr=)JFt7Gz5 z2|(!7Ml9Re&!Ts?6=ssE*&-ZsLVFOU`R=$ybTa=9a^`E%ONoJdOxKN) z+`td{+C=}GWpaa}4sP#0uKkfjb8@eD9-A9@PpU97eD-)FNy9x!-RshIuc@gSw?)Wo z20Ua9k2G<{ea_t_HrrX#ZJ%U~`)9h;r(l1C#TJkty^A6N5TqL&capGnrX6N!dDdqc zL^R^+=g;dEc*S#gc*s8g%=pOj;Go`c`#gb7(RV?ttM@e5>cJdO%8&PIYcOUFH3)zB z%%c@0og1A00$1sWJ$PQ$w%&fYF63w;55D#P{fPu0!2>yu-Ii77#3{yI*0Q&QuP956LqC9pjugA%l zJ1+{LRVin-f*n!agt|r{u>`5v|%l{h4+EUAn`!nvKbukA|9rDr!LYU#bn(bQ%P ziKD;8w_^AA=z8e?EMnCHPsrKKnShcw&)#fa^^+`+vn&Z<&76All!5qM!CYeA-KbFzP2AT3;eBfM-W(Uj>#uIM>56+!d#qB)cpH=+yfhFmv$6O5 z6P>H_CqLPA2V`gKoICNxITL)imPDDjOfc6% zt*-D-Ii6DNT!GtBWfQfXpv$6~`mO(Bq=2EL>$X_U|Ztcgj#v*UA{!+s+6s6C6I z$o^2r&6`3WbVDoDr{oNyG#w0@836JVvgWw9o` z6ftNdv!;X|WoC5I@hdUZ=4^plWnWjWgmibjL-XD0eW1&$J_cBnHrK4LwoZKwSC`eTbB`EKEJ(QTAPO2~dhevk@aq;^9eoVVWH_7Uk^EcLuah zC(sFxPlEsaZ9ncoch~Y#!&&$a8E!x>UnubeYnXf$)3ft9wPi|dP5yUa_{z})idY99 zA}--c+caO%gi7mN=aj&bcI4iN`s?lyR@Lt0owxfZ9~D}5qE@{6fZ~xkou?EMdkN8x z=9=eV`B7AL1jiuP_3@9>5@4@X39OV}TP?cU= zf1dup-yuzywOkxIX6u-)zhn3+kxucUvFXotd&oL6#v^ZWu3jJnXCnCCP=eMGX*BN554OI`PIew0_^Z&y&4+p+{i=i|{F;oijjdKXkuVZ3YY=fQ7V@M8VDG|tALO>MyDd)QXNg5m3k;ap`|^bV8S+p( zc|;VD=p>4&h;0h2b?w}mBmPxC&BI=U=lhPxGPD0_`ZY51k1EgD? z6NRo`qq~#%#fkMBD~nodyC%a$Sv>S1U|^vJCw=mSyeAyiI;U2JvI~8^ri$H8ez}1a zoU|UobOlw0txL7q{Dg=6K>)eF-r>`S2_cL_XQD?>^6^xf(@O0j@d9GWg5&zi%5Wf6 zU=XFwDLXO&ZB8xHEIn245Q^mO42x@4)`Gsv$P6*+&5G^V+G{Mnrdk_ZiKAzl0qmsk zF0+z*ku0$W!s@OcF&Uqa$mb+sY^Hdtv1Zr6GQ0N_$v+~&)RMUPgATh1uQ?Ue$TRQY z_-3>=&PtAIHF7+rcM?L+?jGD8HoOaQr;I-kR)}f@S9Do#s#}i3WfPI_WmRIveQI~1 zyx=>R8%~$$_d+mCvv`Ic)#`UtgU3e52wWa1%(vvbUnr8x8SK}Zp)GH5{cMO$48BC5 z6{Qt3hynRvSG0G=KEF+On^%{;g}(K;FY%kvNuznDKC0OJ7A6dz4P%_U|0_L(40#Qm zaVXFl5t0QP(EFA9;HInzft(l^QRGTJ*|=OI2@KvIV%_F2ig(^4i&MPGXg$~GdVYKV zRG<}D;~CrnH0{Kr-Wjr~TK0ag@U5->^e;d?O0VmO*Fl=rG}_``<1DWfi^Xo~N@7&2a6Z*XV>XnOy`b3o- zri|%+9B~ZbfXRikr}}njDbrys=vh1mdh|}{VXWEWGZtl))cWkoWW6g)+s96{l%iQ7 z-|uLiYme)5PR_BQ=7nF)&GoLa1K}C=(fkK7@9_u3?&0trAQn3X%E_pk$^_3FEZRi4 zsh=D9Yq?nVW746M>gn(5&aNEWWi^iR(;{2(3pAQ-g;QOofSu|Y8iC~R{TpDQ@5&}8 zc~Ib^2J&s z{;+GgGfs9ZpVU+H$)v^WRvjV8H2fIxJx(YNI&B|)v}7qQKa1!YDNy1!Ol}^@G9L?y z#2$ps(!3b#$hEXT$eJa~C}Gl!b+;(xGK<5wwp5PmZ^-vB!RoBDx$cixilW?|e8mu6 zfYuL1ck3WJ!=#SAMUmti5*t&!cfysD+hNKzQJ!1>04o`D3`EBEWe)m|Iw zMGj8Hu{HJ^tOO!6P}U$*bklA@1}yVEt031q) z&rO%X?Io#2SStqUHeU_wxvvj|eMgBX%Gz|Kmvp!ZN02%%wM|7Hr7oE_H*73J1RYcg zF?kZYGqn!@;?m<#yC5Yl#cgOXeG%IOFHYXKZb^zz{>F#GmHE?x2VVHX^1%%bHH+&U zztX*;1-vk=s6r^Pr~B1G^mD`jzq%LvxpYyZI#CyPkGb#HH}*?=@3*puj!AwK11TNU z(|M>fW4C|pnvL77_`EC(o}CRI6!}*l(zph_%+D!PLo<>RgM(C1FZx^8;h%@dx%W$q z(>vcwUM63G6J#T_zl7s3OneJ550T2ArBiqU28t3%{jR_fH)~5w_h847 z_Ii@30mRb5X$lN+jS{&tFa4@c*+hJohvXtw} z)dRuI-JHg*@+B8|3m$3L0miD8y9B%N(cL5W-0XeWt_;LoYH0ty@&XMmBN!EVE#Vr4 z6BXy3PO*zUmN9%PN(ze?N;Q7($VcV9H8q0p-C=1iZ7#gjf3MsGcf_(~hNTp&`dtJ6 zvmY%n>4i(7`Z%)$rpIZbd{}SCzgWAjFYKd%&bpS`b+WB25eQ?Tc#b`}MOhUEcRoAt3MPyjwtQa@w zh*c!e&?@{!UH6!e+m0f&d38s!(5__d*m<`VirLO+HayCJkUQFc$+gR8gNz>z0`v8m zygwa1YR^*y&s<{Tc=tUzs#B5#pK8en3wV+;$)?i#@1lV}FJX1&)h79H4WloiCuCB^ zieK->&w32hvFM-zU~s*;27aEauVmth)-O1vtS-csK19;#J&NJx^3229j-H+R?dBcuN3(CTc_O5#*ioFLT|F$)>Md~bm=7V&zDwxHv+Jdvl~`ZUJpmkmIZ4= zyxsfadqXE)xt?A@vfm)c(j=z%gc0wquYDXXBlnpK!8P6|Gx`J%y$UGt9Cu|P2k+IP zs&*Q1d3hPfyge~;?=w8SzX1`EO!nKZxANod3qlvif}4gRZE?gHi0cl}%9aNd=c|*I zL3JFr-*#<5{QW>Ec)4Xd9`>f#^1d=(2zrjLXu9V&j^kM<*oQe#s5#AUx-imENI;T< z7neC`x>vJio0$F6aN+@(*byiw24so>=-YkcvoOu{tFICw=c-;ybf9#`378KMlfV$@ zRrkv_iaAD4qn*DHS&$aUX`{#C<+77Maj&gTOHi0?a#?@DaX6g@2k7U`r@4jP*W9fn zD!CzVdK>ylerpnn!Y&Z9f)y>rB3x4ZT$PNjP#a_V-5!tQHV4di>bcNSB`e#*OOKf? zT^uOJ4UEFBpkQ7R$_XY&&qn3T9_ZrVJ0l&mHGMW3ZkN@x70N)Xy=qk|`;E#mL$e?G z_t~_+{BkItS1NIT&mLOM`5orH3y0Uzr{9ZSI$ni4%@TXtl=_}Q{OCTv2uG2vso8&) z-GpZY;Hek9VExd^H$LoOnmwVh2ekKX4&Xa9?k6vP&cS9P?{Qc`M;1sX?kur70lxL% z_SVuh_Lf=lpHEaz@Wq)>j+Gu87uL(6x1#*&NtJ;@{CSy+01z%9@V!zH95!`bF^3Qa&ph&}oGpc_Mlw?IQ~w(T;V|=^{^H z>FGZzVWk*02uUczBsL6?&w(8{6_x`ZNwOIQNRYP}Qxf9#1-_7k0y28P0t!(A#JhZL zfmceDx!@&kWfuOj~wy-u~EB?JEC61HN{##x?smH7Wa@(^OTE zD(y4>PN;l-&_);e*>-I4-fEE7lvudY%HStZ60lYBB>i>o2~4=AH~C?Z>>wM!?V()L z$8xm`le%h>p_WTBnM2agK#WN9>j$tDw8-VBO$H%@m3M{`Qr?pe5 zwNh#v`WJk_*1yz^yu-hn&5kd7v!@FDe-d_Ia;sU>XXR&x+dagws`@*w6M(I|F6T#?dMN`Tb3rDTWCzq{)p_Qv8%g|^8HB|QKOf5YwB=SyP2^$h=H6}2kpR^5}{WF1FU=ak=vGEpn-U63M6`+K@PcR zL~qs}YDzdgiEzj>}A z^1h&5(q8_A`u_N;ogT6RJpHBye#ehKu_`OZ?TZmAD$wSAu*Nej=Kg@;wvpp;TVeN- zvzumX%J!+T{Z>BUe74tkLQcV~(sRi^7DViI7LjB^rfQ3hZ`)-ycI}O{FF>rn;-@Y1 ze2M9EQB?fGHfLaxmQUt~dXfGPU-5N(Z+YPW`3)60PyNKCso4MUdKlNLX$k< z>nVs8c-2U&q=E~bgUy4$eN3U5WnEfv@{Jl5VXG=$vYkbTo%c=<2iBij<(msK=78Hz zr%&3|x97DgOeOqN4 z?{nyE2~#C1DG%un1h{je9cSTR0PZ-q2~kDM6Z@Wg6VPK*o89yG_oG(r{p8y-J{Z;1 zuMg}HC-8Ks$OmZQgj>>)C2p_ZrL=w#jSX`W^Rtk=^?|(r*6?CaP)NCiz^%rwZ)}|{ zWmdnj%hO*z9yP6|=OmP>Gy%^S`Y6CAK9|~R^l+J;y&`_H3BsjATREdI@K3E8W&z!u9x#ag3Zw$`AFj#~K7%P+*&df}ZNR%*u=w$=%d zzapD-*L8IEqVmT^26%-ZUOn^EN()f2xzyAeGEX~Twx-V)Ta zNBEe4M?!xx9mSkv=dIcPLX=J67>;o4FNlgbe@zJVvu~7S4JP&*iA$g1<@3Xj; ze2B*CYJuy*c@zq+PvIX*zr$*~x$6Jk|MraM?cJ?o8RxodP@Q;!?nG==9Dp`rOLp1c zmcq{-p87bpaKKD~@iH#GB*+|%jv|J^caNILJGqGkLJt42A4A0Udv-z2B8+sUyL^$Q z{(DoqM}fz$yJowkG;D%Y=kGIS55)batU>DKG}0i{lZFe`Z-}B30qth!7l-xCf|T$M&#|uTsuVKJwc;(R)mRL?aEpN$blg3N?JAw zJRpm@$t!_37{^6d7L;&J;YI^-CKb2_)}MI3jHGa&$lT1HsSaJ+#l66IJNr>H@wE)8 zFA_=D9z|zJ^FP`XG?X32?O>6m#||yZ zE)@x+`t|bVz0gnnOM;gdM%?73NF!IU)N)-uEpCfo;SWj?x{e?Z4$4o z?tsOuD`gSA`eKId1|Ba&f!B_epgrDRceyS_#R%>igVp*XM77*XC828v@39l`kj-yT zRLzsW3x&BgY&{>cV631bL=-*SiEz7}tCDhI{$+xxbAYzB9`pn0DFpUU!pbTci|@}_ zd)|nv5Zb9|6fwHpUunJD*U%1M=Df5+_4E<=!ci9Wl2xZz(GXVWTt=0$$%*JAoPcSO zYn%Kz#-F9x5&lO&#Vm1#B5}kAnxZ(Z>^l7`+bVOd0TEG7IT^J_fu z5#HJ>XP(Mr)_SdkD@d&e&S-?=XAV^n>v4NalkMQS4Olzw*_actOs!T5isDtLn(^W- z#jAxDbc0mEfMKy;!F|3mEF&jr1UL$4V+vaP*hTA(IslXx?n@7{L3aj(OtE*D^W z)54I#TTS1WB-FTc9ofg1OhYGxrW*g%LIt&Quz2F)RND>;-1A)RGqcHi6EA}2p@27| zE%+oPEP*BBw7R#d2T5nH$2|rzfO%O=9OEzw>n#o22V4O<&A-2Q_!4A!&p`ip&a?0N zLDa-g!cauv&AA@Sc<*n=8GZPKPo74*v20b{m_1KI>b`Ry9I86m5PGQbXDD;Ewa61^ zo&2!L9sdxOgW0_2%PoN|Zf^LyoNK7JG>j`Yj7cR|$!+K}1#@wpv1DNhZcgK7hhp+SMR2f_LO$+!3(v{=zwri!1=W=4oL|F%;> zxB{hA)B{Fz?o{sVr-)cY-l)PC+*uVRX>Yp`45ww{zvy8hZbgaUA4t}@PxwdUYq7Q! zs`6fgB{NT`ZPT!_^fq_n)BgkmLELo38Q$uwR*%OKvTWYP&cboDmh`eyWE z)s|jKj7Aub3N3w1^Ycy}53$6wO`?D9CNW9U5PeH=CwMjebwv_wfQ9yjTb8 zt0ZEm+XuX_;o0eaJyV!pj1-05Ob5+|FCd9mQdYP)lQ=tNq5i*$x$}Rh^EeLR#yT4z zX-8PcC}WW$IU-k%Bt{ys9mq^IB|}DzG6yk@Gb2SgMp8J@erBsIDEO*FMT zQ*zTt81cSx;a$HjkW|{ps&5q}iJyn%q%RcN&BsHh@TQfi4{7gQ8c7jg!;tuL|HPM7N)iL#LQGMrMm;!Pe-t0UoMcU-xw=HFOMk0f0fZzJy>2 z(-azGw})-qq-d@7voA2Xvgu7}#G`J+%G}^G&lz0>770}$<|SQG*YM#26ctU`2g|WV z0wGS(boJJeP}1NNCWIj6IBYH=ZWW-CSN1gQ&&A+HSI#F{Pi*6X)-vBk9_VIzdfp*Z zqXPOuWZO>b{?#}c(^1qapP|6xPZ|acl?7;~%vKMDMs9Z1jzqG%8N-KL@`(3Mr6{Nk zp%gs|+>}n;yDuJK5 zWhDY>gi4m)^c>q5F9i*fFn(Q_OI+H1;nk|@W9?Yoot@Swd21SpUL8Y6xy4KJ)IFup_*EJ|MBU#8?(6Kf1Rq*2NM8 z0-52DV>w^J<4M82j_PLj$blX6>;Z1fd@iRRj^fQ{F)e-m=40KRl3Gx9^M`yT8&>D% z2*E6%+ZtYaU7`ucH2WmZ+P>&9Dz<3%E^REV@b@4fNcg?t?k7Uaw;w-qYvUB_jUlI8 z0SRXG&+?Thk?#_Wo}rlD^zSMZfU2ztwZD1U|4P;V;{!g~4Ri(!QM7R7WwMYL^0Z#R zWO#FhJyE{&PK+x9ekhV21?Rhe({uC>Fvj*~pq`DLIGv11t{Z>Wj4P5o#`KxuRft8YUU7)kn3_;q%!~rUlfTWK zO`W{T{s~!Es|6u#YZT;vM6757L$wxgP_b;13tlMSRTVXSL2MY`f@C9~T?dW#S}XfC zfDFPP*{)>nW#aY@PbgGLG3f3`))SxK|2)S=2Uv-uj6#-&cPzNmHi6k5lIIY6G}~S5 z0;;j=Wa(r!N=24`hE;$Zkv{rtf9~0b>iuQ%tXCNtT@1ad6;97>1O3hA=nw<$t&?MK zZ!zfoVOBk9hC0U5qie(Vj4xO8yd)C@Z4T5=KU8g-$niX85TRkF52+f}^~q><3S(>b z{EVXKOmHxK51A>h#uISo7*JYVLluX*g>NHVpzF4O+&v~yY{2|u%_|@khic7Sw{=c; z??^s5Qm!w#LgS-3ZW=!YYX|N6`T2CsUGF9&TRA>95(cIF3gIlA(|?~K20GevPA8=# z#@6K?)W#KVBC~zTeHLw?+dpL2#qh1Tn{{I)2d)ifynRHesI<_1s>AQh7;$0znUm1n z5B|MxOJRrhf<83pJu(0>Y;t=E`ku}=C?*QE3s)?edk`zZcP2v)dzMjoyID#a@7c$6 z<7&5xwk*}h2B&elX?A+?Bh!P`zx=Fe9^DmfXGlN_i;+d@AI6-ntPs?+v=1iwKLBH` zmBwr>CpzWTE*#WUeCd#s+kx@_5@zS!Nd45Pi5$(hi(UgS2IyEf?roFo?a8gx_KgD` z8v2;@2=RFz6CrMHOLSUd=#%-pGj1lKaP~ zJ?I72QBhnNrhJriANl}qDkLKW15*`^`e=XveMYhSs^JI&gVFux2is>`XaocEk|!Z5 zr0k}9wBqijJagA^W*eLQKF*tpe&H)Jr2(pcB}zXrJ(Idnu;S_NWHCb7x0oiy-X`VW zVJe5Jd3dGY!tvw%{QQpmE8koy8PCZr#fNLvZY?!}#m^mT8h6!hbT0+wa(-PqHZBE}u?itm24Q8>x<4gQCqCZu zSc=`tMEd^JxBw61;3%U~2ATCO6%ZKO5RRdPX@0kbpb>=tfk4(0tWDze%>%^GDk^yn z6V5{op-tOJv)gE;)P-+^kqxq4A@U|q2fCmLV8-(Nfwkr4QC_LCI!iP?W57$O33{AuhIZbw%92vq?AgUF@QXzg z$t{m>q;cCvi~y_}%zRws}@gJl%!79|ti z(h)`+*K@c!%vW$RGiRAFu*n+t*I1h=EI(6shrfa5VYUP4$LND5ey}C+nZChX21GYH zny}E@!)NIZMkI#DCGyFFPz>Qt5zHpD%jUAJU!78#mD3D@{ra?nw{GfCK38JZrd!!WSOvw3z+<=h(JS3%-?!vpmnj&F8+&W*Bi$nL zYVgns*VOK&xOVaO=P>t+{*2LbA{Oa1a(_pmLHxZU&ihi-s>ic^ceESp&BrWT_uaf3 z^;W8cIMcx*%Mm@fyJTq$$XyJC3z++{keQ__kO)xBMa>sGFp*OFI4L@;zn@(cv?#KW zHPg~8A|ovgY0P|!GJ4N*3@zFl-K_8Q=dag{DQe6-=wxfKF8Yr)Ro%}N=MGK zg^ocZtC95IY0Qh=(TzFw7L@Z#X^p{fL$AHrZOl=IckS`Y{P_{$G}yA4RPTluGsvH) zufB;aOy<`GYLhn)q#UPLf6tI3F17uRyF&6Ei9oio}Nty16!%lyR&DDQycM?+77Z3lFA`fcYduTcRqN&&ER&98&$hgZhRHnceyXuGQbs*MJ{W&>tKj1 z?-H*PXE;6? z-L=zEo%`N!wdame$a5FN_5+N1e|z0aNBbN4DRZ5CFUV%X;Fw~IDiizQpRa?q@3aNS z^p=ApU%;Q4>pH58NYCcHIej2EA79ezdZ{z~DY+c1c(i?7q4+^WW^d~ud8V1V^_L-1 z6fww!L@AdqawCQhX;YLZwEa2Bn&8l$VD4vGORER-?wNrTt$(dxCb>WJmm|$5!PI!7 zQo9lOnZLT-DWti%vKoSFTiZrdc3R-W4N*#p^9QM;v?2md)OHEIXPZAND~!N`$1-Y$ z-58-`y2p77o@f`Xn=Vb5P{93_5{;bx;1>M1cnv=1sF@duJn5BC1GXjux{mm(N4x4b z)0EQg`XS$REnE>v^6yd;=AS`L6E||El}OWR%Y$l2OJyKPa2loFQul8D@M&TW>vzGg5(pbV#^w_ zI6r%2{P|pS4~7Arqx9AuNO~pSOATCVLgctb>Ml{p_fRf;ZuFa<@8|PoJWDwEJt4Nkqj-}mf4f0(^gaUy1ekvFuN$EttR9{^@9`&C zj=7$A57(#Q+&`e-Y<4r z%`UTPqQk})Lb-|jGP=#jM#nTsf&P3lzJZSuyNyrD?JPMJDJe&IF6ngLY!2_cyV%Xx z=2&0yAce4Yul48WSt5w=WIoHo2$A2r-|?mpkFt6vv2w)_PHMD&qc6ad0&d#wbYf>2 zL7pOP7q6mj3o$_MpbMR`B%kpWubn-ua$_c{L#$7?_eF-u+0fA4ZEBq#vDi4*XY{LmubM4y)l%(| zC8=4*Qu686oW?3@tr4EsxY^TdyDj5s;LU5xY0}#?0xgeHNW&x4p2G5E2VgX6 z1QU;RzIB{FuWj_;C13sBYdx&|;UQ_UFFZ;KRb!RU74txaNGWAuBrd+!P)jC;KjXMR zY`g(gL~ap!s4g+HdFyzLS*oSIZAVT+95vZzI5fS%+SHr{2Ocn$NN(ulkPt2DuEmuN zd#b&9(k|wyIxLyZooQw*bolWt-arid&jQJ5>jMPv!V@|)9Yn))#9n!_T^vyC;1G}6 z^ww!`?e?<5_uD=Nxyi|~A@Y@lMs;F0PwhI^$gvc(C{2UE&WM-=L*9`$fkLaA2jLTg zJtF3#4@*Vc8E3{S;4~KGMlz&##zSA1`?JNE1LB)YYI8y6d#BGh@Sv!E(YfT6qwaB( z6RlCT{UOw~bms{(9)dsniRk%k?b@)EIn@cO)+L7*?(HoP0bbaR*F%501)cZG(F z;SU}JDm8Ij>22!z0s(D479V)}D7W9CZjo~LSFwGWHk-N6Vy?26ZZsrkezfthly5P= z^1w*+Q&K1W<9gJS5G57WYrPB)v7$R~1Un6D@>zYpQUrLh7uMFM0pdoV;bx_&T~cJ| zS9h)1z(ueY7+!r#kXBn0evXhErrk&zQaQ$WF6y?yzBNL0gvJPe=V;7gJ(-cc>b#%(AB;e%V^di(RS zA%oYojNr-ofS<7KC1hT zyeTSML*8kHu{?Qgw;hVG9F7#u4`4t=MIlq zZAq@$AK3hyY#5low;oSXO2&TYT7Lgw`%;>%$&))jVP@Ju=_6BxjG*JEI%@`aYYy?3`nYW2p_Q z>T1-5Z~)<_887{>G*y{BYqThoRxc=?-~9<};D#vbCd<-5r}LBUXcvhUnc4BX;qB&G z+83GaXx|K>2$3OH#ydXMs(hbU(b5<~Wh2H@sYB}aEf+9Hn$s(oz7qhi0)ImQDLavk zQrrcV<#2>JCNUpSA`2u!|B6tMhxmbh#irsDpPcO)^`Ph<|8gn%-%|GV;dN3JOV513(1b3tgyu$TGud0dJFf;^fnKlhMNA3Hu zdCjriIy1%2$%J?0`ThD=9@=`F^@3qKG3AWqLLr5hHr<`ekOS^K^M@}T%f5g_Aj1wN>=*J-i^D7e04_2Kc9TiLOM#Y zU2PqD2JU92pk;Rb@X>ti%_zY3?Z9MHLJv5*+;a?{{KSm@0LH%Uj9Uc|Cf>W{}Z@dK{HqNdmGGG zSI&d)|7ZrS`=2$S83BR+AAU&|FbqfYr`B2aAKq1f4Po{Dwn@u@1OQ{sQ$mo zg;ehY-mYfv#?JlGqzB)`&@d1PB-Y~Nv$D480Es1k`P=;u99&b=%hF1QRFRRIDXEOF zeVau+iYs6_IXUM`$n5N#g!5DlEiLI`|Mfl3%Br%q_Dh~BczrLZbrCkzCMxfJ;mL-w z3mqvbuu`QcKKw6)J=B#d`(XM6ots`Y9OWB*x_PbA~&zhhz#5qhso=V^RS9h2D< zs<==jUoo3p7~ToY%(0MYX&sRP<%`ImIkf~oP3=A_=*^crmyXqmf$>$S^mlskHurKv z3r}$rrk=5=$TCLg?O!gN&^NS&aGZ4tN|SNKz90IN z!a=nze~(gJp>&^$6MTCYYxo%&swKP^x^k9!_4>?_w$JZxBmcW2*lT>u$QgS??|D2w zn-vrfO`ZL;HxX^8X4Lv?|Hd4>%^(E6841P14Gs?Cjg9n0m(nNwo&{W_=6+U>n}Ax* zsBlwhwqL@~RFdKFLj&ylK*!AF9dtH`Fc-eQq;a}g!teXSt*ser;~qee$h5s$RUS4x z7xsgHKA5+ST>IwuH|qPsXQHX)djR6fQ~XASJ3Rky|MjdeX|=eV6e(ZzLj0qU^-uSV zT_;Zza1<*YZ*$a!EA)DFliQokwaYg3Wc??R!N>kY0el$J7p8HEw=7VnDTIF*?aY-S!XPV=K zi{ek9xP$?UOx<-NV7<&EoDH(pGclH4pxb!laPdN|yQ(s+>SsFLn&_)nmPvg7CI<5v zoohS5a>fq|(H8D9d7if2)yy`;bSYYE{u=BPwVPHj$`d5i?iNIQaxlmAflg-aTgu!- zcp(YJ#t%M*{YH`wM$|V|>n>8~E`j9QI{;K7y^Gn4aQCygW98cS-WR-MfCq+*<3eSl zlP15amp-r@pr3F%z{Wm!sknjWtNc?92RyB7;m1eS7t^mWvgpCncpooxY+LLuuew&X zU27h|@N3TWs~FU2Yv;t`fE1b0l)lEwUFy*nh|uVxC+B_!bKAN934ZPmiU6gnj}A%~ z9j2cIp9e_=LV>oQWK-x*sdgpLUS}tUhNN?2XO?p(K(oz87npiA_c>hdPh&68uW5g`K2g2x^F0xTL_cok+O4zvnfMZ9pUC z7#+-&+JvV5mO`-;cafWIBM=gYPzUao_y4ZFzpMvsxHUBCo1(IF$a}ibYe2^e z8H`UM;$u{%fryT3W^ROgiZEPC%l|J{MR1xW;e z;nl;092O3Vf1Fr`qF!a(a^PstGCzF8;P09Bc5J@YxC_0}9btb8ruN4)!PP#Vy9uy~ zk5z5OMaVfgO-Ie|p@L>zL%-$NZ>6k3F7=DwL|c6c#DIdu>^CjKi)UDMM{f{@jU@?w zUptNIkj2xd%u2O=*QKA=T=sMT+`Uv{9yaI^rK&hiM<7 zKdIx2L$MPa-nzl|T1i6l3IQpMWqF=GVZF|EPlf#3wx!-r*h~zA$x=_~LN+*uczjkd zr8bqLge$f{rA^^F=&!(q+6+a24oI6RJIW97=spMA0Aydbw7isbz+bDs@ZdAhLEo+gy~) zUvp+-@gY|BD`$uU>OBRH@Z8s6J1HfnPL#K@?|&Fs7)`++^!OBiSBcCIRQ8K1Zh7)* zR_ZRh`OQ2wdxhwpRgG{U$=!C$HrmJEdAqL733wo1LUY$z)bSLDRBe|&mnf&rk!uu%s66j zqIxc`b2=Dx-11~QF4VaTEUyllLTXzNr*J@DBl$fWSYYq|+opS{)Vv5;kd-CpqHeKH4nm5yoxTs=183R7 zo@)n@r1OGwH9sRodGTg+wYtoM&~puLWYV;o zW4l@{i?|-At4Xf0p9;`G3*}*xmjZaErb-GXfuNe?!5l5~9?Z}ITWlcKkH8a#U~{C8 zBSZQ56R2~mF}FQY1t(ke( z>u3YCs}IZKqS;J>U(Y^16)C+u^A6tlEb*ds+suoL$`M6`PjITva<7wOg}5H!rHt9; zG?k|0gLK*B6GbZCl9oirxpF6jJzCK@x?~@EGPlAVeU#9ZPRJ9{U#6gSTV;4`IX3I( z`z$1eh`&2jb%xQ$#t+LirYVWG6uAPt9rgbHR*&z%5#4=ArQtH;Gqaqh)iH4)?-})s zF$@bgW@KL;Wn9;9fe=i#_~223u?A$PQlp)^3#)XzE0(A3p;9+?2n z+GIR(L1~R=gcP_87b2=zVdny#_&0^;1sI`Bd)Js9LdW-~4r+gz}p zo}UYnu<~=pPl@h0%7cQ$v2ek%&*fmeDZkOW6jbMvpu3JnI6m&Am@lLIPLmg5=JGd& zAc$}G_qPo{>rw;z*aj+YGUYB|LNJ|nrqgkilTK2K?Atfaeqghw1G=8$4UM+dz{I4o z8L)wNd^Lf%Gj z^@;kpJEEQJ-kkSFN%I!RgotTbjB>4$?*z&``?!$v3&~!a%bIM-Yugs0PG1tSk&ZpK zl1dM~ZwDo>c6MIwQ;43Wi{KrEFW5>hU}nSCrw8O+=!Atu9S&3Pdm`% z87~F?h$8Dc{b}Js&YJ^36bWUc6c&k_!$NEzfa*Z!&M|t4fk90NLS4!%dM~DG?=}W2 zyQ2nP=1UWVy9awY&=N65j(fht6!ako^qNsNCD&h@z}?zP%(+G_kZa%QdDyDrDpP-P z55fhoyo$7UC#=glJ;jK^hYPYBGK~I2^R+KI9!ScQ{KY4jcx{y_-g+ugijSAkOzNZB z1MZ%x1Sq;$Vr2B)?v{P^YdWEo=p_ozAQbEq_H+(Xf`u=8E!}&#EPfl_soV~4!=svT z_-so;yeHk+zA3}c^tm{08NKP{^bdkZz+LG}7kQ8+o-KQKP+oA`j_p662IyK1?CT$_ z=oR1(NeZIO-?oU#Dm@BB1gBpi_&%J{a~@0QB{5Am=Iw?u{hpfjm~m$_`$@Ztd9tgn z%Q5QT8FWeFS7415hx?%5>_!_ZyQ6T3BW9KXE81*Sxf`^hR$sd-K)>&l_@)*bd37hD zhcFB69wz%e6rL51ddo<-aNaKec)>zX0C>w-M=Ef>5d3KPh!|Qdl6HG(ASnmQJsFlg zsBX*I+EXQl?MkOQ53#d)9n0NzZeHwgAUBZ7b~scgpz~;JMMy^5k)Gxug?A6lL-i*=E${9;Ri3 z`^wa1lF*A*a1|fV`w7q3)TIu!aG#-2HTv6$2-oIZ($LPf?RmyloDp#J{h=})k}K53 zWl|TqR8T)%v{zk~QhtN8%jmB#MenWEM;%(bOA#qx8{qX$6L~m4jf$q_{VH+~^+vF* zA^{Q)=VY@)f>alKKE)o``kucJ+KY1!n-Q4k$;mk zgA-S6>k6}`MLV1y!Crbsi~iBoY=Iiedzg!cLbo`4?<+pdFBE?bil$z7iu2sW8dMpG-zn`&>$6qyRwcDrr>?zCKTJ2l~j zy5NaAT2%!h*$8<3ZM_GXw3@5>e90Ty=d6bGx~5a7{BIYjE$t^?Ej4~2M!loyKdO7r%B=_G?V%z9Pq7xRm) z6BppaP$nd3r%_4&aEn~wY%G%d6+*T27DZM(?D`Nv~e~o0s9!w$D)-Zc) zdUswS!STD+QILud4@WY|lrnYAH#(PHgs&LWR9YE}Nv3bXF?-Usl&XFbTs;^hN`>cN$f5-Qm!Cy4UR~$E3W5ZPHLTb?<$! zZ6bAlFsELtw1y}L1Oh_sU2H!I%` zv_g%NCeb1~{c%QZeWfUjNQOMZFA((SJimYImw=-d*HzkL&ULw=w17#>d-o_uX`d@m1}z1zBENY%_R&9>m50@v1~u(0*tZ$#hZC@$rK09 z%Oh*_2;Rd*%P_X_%?z^EQYz{S_vwCzH^i%;C_%lBLoDu_!<%1T*PXsKN)%O8jmMev zoBGoDd~IbbD|@VCBFtDPzFkiNk@{$u8?(@!J*@f5*z=IUumJ-Zg_$aJ3 zMg?sAS+jQeq*LT5HHxWYRZw=WBm)3Y0c)H;>(F>c4(T>$v|RY5Rv3I$^ z+SB>IkTDOmoY|YLcmRvU)s3?F>Y8cvGbKmzV25f&TOpoZ=h-{G+!0u~5s#IWd|I zjILZCAaJ{1$ZO%AndCeP24SC)%=xtK5zyf1E9jSYQCr~EkNE^45io4-o9eCK5sXC8 zo`^2Rd;H0rag}-Z6bCUF4McT&EP}RtmltwxgS)=}+L@qV@evD7o|F{pB$!^_gJx#9 zQ7E+f;?8YSjFnCWe(e^14AA_Yr=3u^qZi{ffQ#MF@KWG-@gUss5*#Hn7$wQHMOO!s z2Q?w~<{_4~6{v6PH`sTdYV6z~^>fR8T;Fu8;rX^b%$PuM~Xyy8pn z**x~5i<$zYyYW9FSPSMQ_=z*$e@Ju|Qn8nX?(i0uO}$jI4Hs|im%@4xT_A0{8+`-^ z!l*I7!aFpph($HU%b3pNtzUl}?{!(s_9mKEESQOoGWbB7%K%)B=> zr0>mG>xpr-+1%?CE)?wf0ln+z3+GWnyCR~n-y(ZDKgz(UZI#hlJMrxBK>rPywV9e9 z4^CgQnUq)mO2yO=q}R5&egLu}>}6bT)6&5A#N3SK> zhJTZFTkgix3?2!5kBWF{9mvpoKw64Q@O#0c8SxG&ph){(z@dRiOkD4A8Ksc-!a%YXylV~v*qqjI`0q}De9)^6A@+#=J8+ZoK`@Z+ODyZZZ zj+eAymYqdwXv>W5h#m?YSAI3NNNkXhyAAkhJc?JvW)|>B5W*Tqx+NFh=hnlNRG3|u zw564M3^US2^sMpA=#F$cNyTCKmWR4ny>S>Dk<%a3ySDo?uB@&)lf7T6e4g5t7L=9h zb~z+RTBAtNc^ZZ34u9Y8uW!anG35BRBaZ$xs0-^LrN!ijfMa@|Y6lcwA4?vJvfF@M z3eM{KerbXVjaswX%<3|I+h(X;c}dg>ITEn5=!&Br0|760&Z{qb6s31QRH-*2<1RMsQv`Jxo#n)<^k=vTC3J0jc2A4K?d9}}yS*~N+3uK&W7b6Y?UJbzzjVhb zQA+;B@lw=AE!)a4W}uXUx)9^~f~VhIQ`C7c?gvII-aQ+S9n<$>kAc<5?=c>#Fo>HTes%VrC*NEq=`?v zT+@>IH=@CZn2~p((tX&Q0dtv1_^>9gbVVL=vV$hUzs9gvy!Ht3{nQ@8+#k3)p6tE4 zbuu6Ewg-s>v>)-JN%Ukk{P~GzI)hfPxRzT+koL=p+wxkFk38f$xU`eGU~k}`)E%GN z9;1s<{ciLh4Wu|1P# zGi4!NX-m@NneDMzSQNAxw=V1Wzm{Z+Y|m~~rbWwldTiE?A8gmT|) zAF>mJ&JRo>W|=}9@a;)%#K6$?c|Pon9-iAq5o?g-D5*tqLRY{7fY?QGftwr}U0#MY z6D4T9rvp53hDZ~JNjZ}9=m`i}AFjAl7v8TziHHQ(Hk9-5BcPdGK8sP<-D`z%@<{Qo zpL7FHI@mn;F>zy>RGTXrjx z9Gsy>mDj9W@Ro|AFu%lDBg@p|)(RZHMz|E4*w|B*;iahd6)Ql0K9(s><=yuIN*;WYvV>YoE?my8{jw;KhfQ z9B>i;ilS}^KiSB$f@f@8(}$40Ys3D%EO>YBDSGYzK{#>yA!{mm61gI^#DQ73Fg+kX z?Z-2s(VuM-5ya%d^{KS52H2PTTxl%r4L}K@-Gw+^=^{B_P!QsT=hsil%on)#9|xr4zmaf;__qfB zIM@x06Mt*)BxXOf0=6qF7XynPTH(v^;5YR4VZR~n`##&!oy`BF+EqL@v3_EroZh>& zD8}-RE=lmr`q*w9-POTVm@Sz4q=Bz~r1;a$bKawc_9tB?XwPwlru(&s@HeEp2O>*( z6vsP1_&Kmjd3}!#e`nEy{LZs+lL$*TL-6T)`-ZAbGGTwPK;P9vX_|Y^m(b(SfT2K7 zOFoOYhfYcl*Ne8?agdfc%GQ+rrCY8x?nTy!4(JScXL{O0RgVjvt6mWy1W=L#LN7=k z9jKCl5nn&tkoVsr>v_R>$v1|6^09*nopk&l$=t!mlwLT`f(AJZ3^#pi;2rX5C$T08 zt2pbRfA`TF9E>qo^F`cxW>~ORo|H<)v^oodoSm~t#P&O{^RwRVNCH+u|?z7Km+{vf$62 zSCIplv28xI)U`}*=;OXV5M()GM^^`jnZWo|?R3Um{(Y^mk|X;=fd6`DXQ3vzT$tD= zrLW4@d!(_3nw<{;OvVwi9>j7RiU8g>iLR@zJnj>})7mP#Ph&GBuEdmm%MESPUSxca zm8Zt%qdSPr+lYB*E2RuCF;HuoFaVbZYaNqXOK;Z;3A4MwNzZZPqCF-~=arzy9`KX@ zD%?BDLrYl*TU!!0eDo(k=Fcz@G4S5fT}o^zPGxup+bAZtXm3hhn&e215U$YYF8Bbj z;duPf*7RJTnJXQgZ+HuH`;N8{90p~GgJjZYW?xf_sk#s4-<*Wb#nVR1yJk(1J5dbn zoc9(}B~`ZHNVZZIL@6(Q2s zWYc=9<%_q>Z9ER-Ku5r)ZCd{=%S7Q++xionNganFy5$Y9V3Z?(g-iB5NK1W@!yBM}VQ*i`E*{g|DEa6!aq^ z8Z+1IkgwEsZr>xo_W7QCiB+;=?x(98a;|T;xYkm%a@pb;`Or&=Z78EN-l4ZYFvIt2 z$T$7##!_ty9jeO*XRxT4XmS5aW9*F@`GFI6G}9C6>YWuTQ6AkZ#K@`oEc=`v=Vjo_ zwx>70Zb{>ZD;7HToE#|r_T!|ndP)HKdMSw>$TR7qNvQ(6~ z-y@4!rNre?ae(D16c!)Fyq(Q zCmDNd1>@9t-(##Djpa=ajO#+-PU_6!egiSJO<`@{W+=X3N~36+lx+^?*ljKO3Z1## zPHVXMz|JG&)sm7N+IL~}?;lm}S9pW+4Sm5|m|?VnE=bJ<$WJTq%VRY5Mv?nd=oKv{ z2bN67Me0!2PUkQ#e()YF)76i#NhNQfg-!Kdee7+rLU8Ch%ru^Eh##^3XJn=_E;C&n z2Q=h~ZXz4)>DiADUxFJrCFG&`KQFe zi#&EXItXD4lr2$NmhE<^cpj}e73W3U>LY-M@olTL1W|3=szW_g*YrSkRQ2D=Dg4VQc-%)9b<4mm#GQa>k^>Ed9s%;{A^qmXexkKDi%1 z@Q)nEYB9NQ_G^P;j-aeuDEa+gMjI2AC72nd*fVoy$HWQ!>OZ=gJ<%TVLD@l`SinKL z)Lfwmty~E1pE?>!oirJn#12DOCFNHF_npuu4gVbyO@_)(Xz(26K+jkTq?Id(d+S6l zw|w~1#Dq+W)BSBRpGTqAS9x!AGaH+HA+3rEud-kH`7|Zzw^V479YgBAB&TiJ;)U0SgNYGb^i}fdPre{%`+NKwKONTAnZk8a8|!llKdy*;{4vT}Fu19190%KC~HFl>n`M#vhWZ&w&{QItC0%bwrU ztmLvM^?a@;^7-k3z+K;p;vG1YRl4fHa;Z1)Od^_T_sTd-&GM5k<0R5WMf;~{v+e8> zN)PfeM$cpoH9N!q_{@1l3j>~O`8mhla5tmp(31zul^+Sx1X|FE0?zFG7tfva1yZl^ zHu#M=<;IBm7@BlzU{U7(=NJvDj^=87Ype%4bbEWOVW6C=l|HMv9J z$x&A^Uq^%n9XW^95x?2ttx-qqGT3L|NK)Vrb2Vvu_fTr%?qYE38cOy*b#!G}mO`kr zn8vF*tSN8@7^BIx2}XBfG#UR$t zraBscS^JzEY` zNV8aIn{8L?T*1ViJICq_GZK87+Rd#mN6Fr88TKwC8ZK>Cd3TW*nRA?l~T+ zBj#+F0(m2>`CR&y<|A8JPXkU|lk%?$2{Mh!r{h8NejQs4+tyy|RkUW^Hh-!3A3N-v z2meU74N;BUd`dbxWzsfssp985=w3A^d~FL}ICEG8veqt}JG-WA%Z>3DGq7kx69mnV zhNOIYy$f+gX)dI_hNuvuN=L!eRUILVV23<08m6r)&u<^J+x-$>o&{1ledN+@@odB~ z;=VxjeM-MQcV_S~=Yf`w$Ed zj30k55kgXjuBixM^bl9M(JCglFrH_TQo{4y9glKo-7Qsy>ZCV!%;Qc6R)i0pSF9G3 z!p8~?8b}}fc2{qgE75;QH`9lm1BVt8^XA)K7?uk5c$jO<(n8{Atp}*Tomgd)WoC6$ zjtGhnQb1$|IvI1{7~Jxujo*HFtxC9>csSHQ(f!?;G4z*T%IKUmmcXu{jnnA*Lj|Ui zaxRtGv=JgweVNo4mi#W^aoma#Ir%P9d`zP~mqt>_Ar0b4s?4{cIw<@ap6KRhOl4P2 zXRLEI4I`DAVv;7o z6HS-dyAf2m?~>&rZ_I~SV;v$`v4={l41U&(7M91kxq7=a^B#CTRABs@_7mP|n{Mlk zzx?i11esDnQ;tZRY0U{Y0QGLyC7;bcYb}k70n|sA=aSM1*Cv-?tcY|;PI8-ZVT$pX zxJ92rArIKR25g>TGsLK{Y!^8@X}Qr%9A+=ZA0Qfxx4D7V1I$H!qTwT{KNJ0on zuq3!A5ZtwJg1Zx3g1fszu;9Td913@L3GQwI3U_yhqEDXZ{qEO4di1z=+`jz}Mx8o! zWbeJ!nsd#$e`lw)+{|H+h`n~W<}X&Svh!RRYCa3ip)E@Odl>p{HTD6W=|46HaI6GIZM@Bq+Ous8gctPg@yMd zKfY7xUJwv?F#G&z;8Ty*8kPGt$VNJE(h7$q;#yPJTJ(hJ@zgfLqyMsUU)0Eesq5Ck zf!~)N8=U`-^2Au%b95Qbn9eOBrOTY-ap0n5;-h>bPLO8}YO=G*V5bR~`+elogV!=2 zf5Hwip8rW>#N*u^TPm}5q1)hHhRpW2*)a+$)?E8$w-C+kpRgLk)CuU2=!KL&*sU<$ z2j8)Y?scyN9nDUnHE66heUDw&Gs6@_;ebFzY}NN5Ahlh{PAHp|AvosPW;*3$5?jL< z4;w;0kR|E`f~>!@g9<*l;n!TgTWx$L_%6R)-?@3rO0})`YC7-;ly0)9H4k<;V_#wV zcb4eQ_P~eZ(mq*Idp;w=JU5Ngz4ZkbarT{EKGz}42CLF;@g8X{H}MA;oILr}-%wpR zx0ms`iX=fYEbx%vm8FEo+`tbc%nQ$yjgs`s+uZ*;QM|DWId71Eowbp%RZBts-gT65 z6QcbUMSe8T=%o6d13CSHgkkAp2s&FFb-ookud0~Rk#<#N{z5a>e`ZcKkPH5mDUsvP z4=oPO{Yur9RT#vBJwBR8f>8=IQ@5MvmQAkO9HwW}4dy~Y>U#6E7VGa)E(`V=?EVJj znhIyMw#p>1-9#*r*G~@6mXLKB5#YlDNOSOh>m~fB{887@Nmo`;$(k{b4=dvY3Py|Y zKGOOMN3ANipuRYK3NU_{H)i3Cxt6vzC6(FDPy8_Pe~YMUAgPxx{uY-=laWo*ebu+L zgi@*^CL@!dsL2Hset#NR+<+ZfD1QKrO z(vp1f3^uL-U3YC)Z}00`jR+v8kBmfB(T$G&H_LCJ1inl_NGLyF_@x4CF7R*d6ajGh zK*^M4{rSI@PyZhM{{Lr>nEzI;{jV2E|9$QMryOklH?Gcb%IB+RHSA>iVJHl5kr^(UVB4x^8}!~l98d~N{ie! z4rBjUWcVfwP-Fc+lye6PUHrg6C@3z@2S!Dy%6|#P8fbxuFkxN`)OYf#s!`3&Da=xd zmr0om0|Nu&LRMZk&A?Ct^5%r1!osY|$`8Oe*&+$M0uGp%@JmkC_>WZVeWEsSCBT>( zH2nLahEcv?{uI@JRCf)Bj$DM)-0{rw`n==?b(NLTOn}wV-`ceZD4y%gXB&yo|0w|h z|D)3XV@^X7@+bwJkT(i6N+KqYk5B5Se~6Qu4;;6$hqAXWwjb~LfQra`wEP56YWw^i zTOs!4i&o&dza?&XNark=p*C{mE&Z>irf+!``}M!#Eib2>K1 zK#6@r`B53Zsixf3Blp#;pq3dC5m7^<$9EHod=lX3cc944xv7`(@q{KbVSR;)^e1|o zDZ^i1TKv6_pG4U}{37kLE@}>3w<6zPH9k*_?lcp*hz7qfnxe#pFx7 zYX=9M1de!Cc+xd$7@;0?ccXqf!>*69Tjw}v*DltAQZrV7HYlV!`9HmnmlOenijAC{ zp7k*NqT;Le8I9Rmj~(ah!)3E`m|4ASzFbC|=iP~qqME#KMmxXD(vt;17Yu}K^d&L6 zQ4NM=qv-R$`jLn#GS9rX3I6hnw!Of51MV; zd{I`)cRck34w@`2EpfcMID(Kv`Ud8~03st98a7{dg$SJAD8}aWS5`O<=R1c$@U z>dx15zTOoMFyK3;0=t-+eI(qoTs$y8(BP-g>!#FNf>r_=%KRw>+hf`kA93b+=kR?} zM#eXQHq+d^NMgA;xWB-(rKM#g!9nUNl7KBp!JxO&uFK{2)|G{Wo(K_948TeD%nB`` zw}dh>GNl-2>83DKm+; z5-UDZIy%-CoSfzaGIjYiIROkR<(WlQlV1RlefQW;2Zp>hH>5M7Zv?K-H0-c~VcR=3 z^r(gZR%Dzt_OHtg_z*laNGkN>MQ^ZbD*@U^2%nr>Z_P0Gz($lXS ziZB(k(aG)sEvc4TT2LHD(>La|q5DPDkpvP<^QyUF zR?RBKTiBqL!Tt$@33gKnDQ-#GS+iubJeQO2*$@K`k85O1OiVbSdwn~S`=ocI%YQf) zH+5KCY;pW|rht&Gol|aMVWBo>VnRaAtj)>ldqKg@)5}X>PWaYLLpFCZI;Of}SH~F- zEx&DU8opRPJ{}Ek_73#<`U!YG_B?d)=Y#L=uMVQ3qR@a4V3-$x(tCQ2|{1sbm?QV?*9=?;StE=s!8XoX0f5oPZG(Pgg65|3ZvbQX2H$!V$ zC2VA~M0aSxhTi6}&`^elTA&t}+|NlF{^8naB6HX=g!XOG5g~!Ci^E1rt**nP8g*mi ze5yFVDByv3c709GO%qmBG${41h)q`3@=lMm+1c7g+s4)P)iNI+1_p+rhN86LinnwU zV@T-vJC)}HmPy5^Unwav+XKjoQABxJS!>w`7w6Xi6Z9gNtOr=^~Su!g2aJK8HsKYg)YhSo|I@Fc z!9iJnbE!IBa%VVF(WT$>k1Qn`cWAO{JOb9%?0t$+iV6w>D+S)J?yh#UN0h*j(1m(( zTOS`=O5UdfCX6#DJ0Tl!3Gp9s=M%S$7vig#3s4Wx1-!g?b-gQN(D(WV2KYt-k#_bL z>xoB4>2vb)v#0iwc&@O4aiTYto@RM_yf|yc@4+5bPyqWgKd;SNaD9C&VlB%qfj=7^ z3t_M*t@Hc&Q#{!(9KDZwseG`A`d(Og5;_fS(oLAvlafE$J|69WfB4g&*XrHc7qxb< zBlq?D`}i%)&27WjjEvysW<-IGtS=`kU4ENh7^&R<1oy{q1iB`F-Chnw=T&alnNFkq znUM+bG7XBj74tLdM7DNAi|yx)RKBT8C{DVRcJaXB;o(tIb48^|2IePvt99p6Hrb{d zn2BjTTbr7$LK{6Ttq#XI_8EF^!j93{>nvC50?*G!@oO3Zdfw2$z#t6b&kOf--#0n^ zr$0K*pA}Re!OrDC9#u7|2wma<<^~yU3mRSox>zIu>m3wGF;R(Sv)Yc=g?`b_Uwc0p zm|0tsyFKgR;Ys_@6H0M+-d!MEo?k#z8FW28FQ3zg_zkKuaDGf>XOqO%j1ust8&}B$ z2JQf}`)jU>Ub+46o|$-G-%o>*VVtht+XpVn2J`t>_G?8wJt)o}UlI?_KWat=>61J@Fnp zA=VX^KKyD_M^8gX$MiN7j+%zLLTP@@{7oniZ%aXO=}`@+M#Rlcz4>ZpZYCRm;2j(g zf2(hVxBA1ojKSD^)E>1C4hHhm$v!c@iRgW6Vq(I>%R9l&D(^b#$iwng$$>O9ifJgu z%4L?rCF!q0prjkJnc0~i?TyJj>pBD)jHUu7iXkR(qJ-I>~VIpcj+&hZU zD6MmCr!DQms-0m|R!)W$6c(1|0Xd=EnAEcZK#iP03PT*|_?d`t|iS)M>1| z@uE!q6#{~iuCAQzCX=teZmY&IWVueec(T z%S*w0!_k=wKPA3C?eD0PWdhl$-syPOE*}2fd04zBEDBBiA*1}(tS?IW8#v=b$JEr& zrVUW%L3u9|?-)c{>xh8Y;`#U(7e+4u{1cxRI+ZVbJ{3v4*m`}uBw{6lf`W4OXR-L~ z%Idih^*Zv;g3hBvV1lKGlNDPe9!jhG9W{1sg0_$sp@_&%VX;kSV`JeJ(`N6qIc6O- zH{iTrVQroVCI{Z>2;Vw%xkCUvEo- z56{8L39)m`Zfi?Fuc{l`0Wz&uudmq$G{33;&E;A>pDOC-qI6#ap&gPa_uxCBNFkSF zU|}%y^8-YB)}yehnpt6Sk%iKllAWCpvG>m8+|<&X+{xAWEg7F)fQUdY*`hb~ z#;ZDZc4U0MZ0+=uX1>{cu7x|C}?O% zyQ$+YFY^gsv3PZU&-mFE@G3Yt2fVSr9weEYozw8gP))2I#>9aWVMNc7C}&l>_4l3Y zyo;9%EDlRyRtHj_tIbV2Sp69RR}>6dSCGgz(ul2;K_F4k6k6j`+MKPlqm9b^(1(dy zplN()YHr>+GLrUdykb^W-j>lQHy60dkJH>qfY=TQoRjsstU#Wd-B|dcwpHlh5RHxP z+pFLJ^yy~MhYt!3jg9Dp+`bAB>&(2o*R>06Q|eS2KpgnYN}CEzr5#NjfK*h#G1%CU zkU)M$5C)_KQwRvO;$nc}ZuQ<8nq*j~DUQWlVQO>80oely$^FMA{?{NEv>*OLyDR)b zZ)5YaRTg7?fJL?HqdQ{o2Lb{?Wo2X_R0Ray=uK`GnzBX~94Z5}pCi{pFxg_d5BZp7 z`Ugngdt(07&~y8?r)>+Msy8(?{oOI)cX}!g)E292TOz(bpGZZcK9f`tF9f+C9xA6L zH~>HYLr8FBbb-m#mH=3_&CJArr8YAw%dfvpLHIhNb8Ie3r}Yu%BevY*>8qk5YP)IE z;j;WdfMfJ}xF)46a5<=p#N|$3ZuJMU=-j-fWKuE3g7Wgd<&Nd`r>YuMP2=bBJ;$T{ z0q8IYxPi-%-LU#75D-6sfS;MUs@04y{?}gP0S|RYk&t-k_n+kk%&qn7-&UncJEeD3 zD&VK03UZ?Obn_iMsL7iG)fPK9cyognegffmEVG+z_R}c0h_5QlHeOfBW%SO?MSXzu zjx#jzBVHeENqe^qaMfrK9>8vh|9-l-m;!wI9(N)!!+Swgpd)uhXC4Y2zBzdAOV{QG zY-z~&yg4Af;bgQWXg%2U^15@iK`Z0qzZagpd!oCjIGP)NG3sLg2juEX(;6A&rgAyG zM1HtfDQ{oh&B934P8G*t3T?>hqNJlUy2$&2mqPEYnn|{}4E%=6L7Uz~lqQ_U#j97Z zV!R)5bW1e!n5FuT)A-N2dQX6QU2i<|b2tv$Yitu$b}KbGrF z!5_T1z|#j1*&e6;*+m}-;T3Ps_G5h69AYKP)p^ zTZ3$=YocYJA`>7O0O<$sj4!>+OY@0pv((hnt1GKkzjOQjnIUYTfG8?1T9gT+`1`e^ zKHe6e8~$4x3l3;YB-ieF&sSdUNeR2&{b z`^k8RgoJzOM1J{+*ntQ;w_|h9c}0_ttdT%Y+lrt$U2E&AlNv`F72NU?vRPX|));uKALiPSGAA9K zf6rR>1y}&f6!N9()S})N9qjMh*-!qL5mLipYAP$h-_yj>%^LjJPX?aA;i0X)y(0a` zf03CgJA3=i(a}gensjm~aAM!{^LGG1XzSu4RH`zB^ACvd#!mfj?|@3Nor_Bs00#kw z7$wl}F-1{xh^&Pti6YX@-rYi`@9 z*7{S+(p#MtQH|$m0A{|k@>_)k7>4vMEL30#nIEW9U&3lK_TZ5Q* z3Z=0*zpvW5pVM95lpCWrob9G$rNd=2pU$&6f_4YCwEfeLfL#-ke!*PM0JI8B43eq( zkta>CU{~EDKYbwraps?~2FN%EqQyGkpQpqGYqRtqxbD(sDc zm<8WdSI1d2+h&ncPv3{>YW;QvY<`x%=kYfN1hUZG38;6}-DG^9AWowpZg*_DinO5Wv^YOtx{Pn{nQ9^>yQwgI@IcnkIRbNFSi4cv5ZXU>%B!97p; zFnqs`WmAH(uj6ju0{xw{O{l_cbYERN!}?w>!^N>!MzwbrwMt8KxXX%K=hO)W)r#|o zEz-N*p?MhBqsXz`37kzTLEIjIl5#p;rB2N}t}l=|SrroDDK|4U=`wESRMF9al`zda zUq@E)*oLML40C`SIS*VnN^4V#z8S-rvxvLj7}QZsmD5xXr!pbW1zE-@WQ=8ANQUVU7UUbqC$l6lV0s1MJgdtgfVeGd(Mxlq_Au z9M4PS;wiHmT=Ksy0%kg*ZU*41PNDAjGJ}btcgDlAhNcxDYR&O)*be4%CMADk*R zan}>uL1tfhj7E>WIv?fUEEpIQ!49+u^Y=lvy0?#)4DZ~`pqkKG)s zW*HZq`2(USGKR3?`uB=8Y3*(2m?oN2HfuNK3S(bLc#Qi9gWjjaF^5lfa#bO1W zbRHMPWDMP7S&Z~C50hAWd=Utrp{HnCCz0mTJ!#N1bY&xYaQ)gHqIkexym^VG;$`W{ z$Cz)i!{cDpw&bocijX!V)O?d-K5{ci?|IVd-vAde;MN;sKS-nhV+-5sgo$Igg6&Xe9wDLwwot&lIV1E)1qA43>e2`(8^>& zWWu15=B-wvjW%W9wN=-!-S=r7nKpQkcW1EX#e$0Q=DO2T;Kk6GZ>fe=RBSaq9)-zP zRWZ4{C9Tc^t>Vo-4XKOnYqD49ASyNROn8SdV&R$=-Xa}`Eq6>tRp3a9vqK#grHlcP z60+M!>YD^H?-&{HjFkkNOlHtJTg&QS)ESO$$Xv#WxtV6yGuMasDRdN2etM!9f;TS@ zL2^5;4-sxQs<&O#pU^=;64G6yW1?0nkICwrsH0c&I?ISM^lE;~s# z_Oee@-;E3_yo%}Qkdd%lu1V1<1w|_nZDqX`<DsZ$?TD$78|UQnnuzobgW*aPa4P2{!IqG=t+&Q#vpXDZ zG$A7?law#a$8h+3rmz&>VHEF6?@FhqW{B5#0jgU3{jCzLdL7tcs_~tSSHeS3N1;`a zz4`bm==_B@_ef2uG0zn;i)c_5pG%TekQe{ABhmi;d7it1EZD`3LlFjR2M{W%xp_jr z!evmd^Mp@zo#iH^k{-dE`+AwF@19b47TC?dMkhRcUGO%PXqN8@cvnMB6(hY+(K8<< zw!_;+bXtu+eB%=rh2Ws(@A>n(CwmS8lkcv(s@%wZt10hwD@0r?Vs-iQSPf z9OCbB<>=b{d2}T{)%xJOl=&~Oeiy>uk1ZrZx@efAgT1*6pQ64ylyK~5^OYgD^M4qT zvpK))M6h~Mo7>mc=q_FSr2y&5G~K&kY?WV9^`KMA=73V%0(DGm43~n7W*EEvFKN+?_b4 zh|&u9v9;p@x0VE1e1ngP5hyOtx(lgMyh(TLo~@?bc0uTNyw%$6HtJkjr08zDZFZun zo6QgcnL<&9@%p(YgG1JuxyyGHEkC&U%DBr9q`2F)#xxG(tWMs1wUVhlJ1_pu_*TJs z*FNUY+&Mgj33k4uSoF9L?dK8_)iYG3We&5<>r32X>z~Eai_(f3v^u1sAJn2nCSPB) z>j>r1?Od4+DjTRQXHI)CdZ*}Y=b~8o*-hSh%8N7y)AmU)luFcm-6nBt?puE1cvlv>*CH4LN?ZkX_Vq7=qAk2R}lhoz-g`u~5ua24b1A8*Q&Ytz+OO(2SGQ587825bS zwI?809-ZHGN+pYKQ_@$^<&d6y-SZ6vNjQ?=2R3F-iTY@ALcNXJkcP|yL-IBBjicQ5 z6n}lKUxNKRf;jP2_a~U}3UA7*vFk0s%bqf49Fo9#S) zc2*IY6X*7L#>b*+9!I~IR96DyR!pe|yFAy<7W7$xYUSwkJY9JpGbs{HYJJ~Kc3KLn zaa)Zzu`(2!B0#A+z4;#D+0tQAUZ9UV%lg{9&R^WwSvD?@j^pF4j_A$i=pJDM!XK^i z`WD)4+IR0Vi+56GFT}=-jepd8-)?bR9pTSbva7VW#~Mc(EIur9^UBhii4mq3&qZv2 z#>T!(4mUO}RaWTnKA7-z*@$YK`j2#;TW=kRjqID;vd*9Cd4z)7Z?Ob>3~$qd8z-+`vt{~&h;CPzVpm&>-Kw?Nc@?A=P*9t?&@{Na%19}>9$fS8G@R`7 zq^kg3zB7(j7F;G7nHfJ>!P$XBB?{|0Q}pL7bI1Ic+d<0ASsLSu-Y(^JndfMQP$MkK z=TJ=17i@4M6r7qb8r&c&rE2RW&`ehFGcKYwPQ2d-FEE%2-U^QjjJb*~=(FKS&CxV} z`|54eropC#z}BYmofkZ|g4#FKh7Y))2CD_}%(~gCSzG7!Y!nQ_Pt|WKTm)D0nZhR~ z2;W&5-3+M~MM8hkji zmfiNJ<_O{tim}kb(5ZE^oKHcszF#H$%EtPVKa?{*b#BYb`dhS!d8AoPYweTB=DRoC&{QvDU&*e})eM@e-#Fz%S%g|Tq>&%XN17c_7#EhOPn z+~f0{az}Ee*s@eh`9D$(=Ea zHPrk~1g}}+rMYj_k|E}cquHzz*RMwrpc3UYJPHya2U4#YrvfCc}aF1?e8sFgDj-VZ>&bdpA#w zCBRwVri6D=k*q-9FFn31f>SMD9wZ~YaOrzXIm&Iw$0)&&iD7$WG{E@jg0(d`nq%kl zHh){dPD0yghOXC}sjQ3EGiU#WYFwgT5jJ&JVKx5;J03-S&s2+BA?%D{z{P z$!ooEz2CdpJN;~L-ccU47PO()^=1a+7fgwH&#l(#g}pp-mbLA5z*&@e?mK5A3}5o3 zT~jzvugHUv2N&22k%fx&-7@<9Yg~jLZRj=@W=|?JZ1E?xCp((V8&Qva8d%-=`bNZ} z+Bho7v*<>a$c>ASzis=)yi8LMmx~RM%!JO+GFRz<6b;yWI1E6-||OP@f;m)soV zD5x;Ge^vU!Dz@wo5?!N(?Yd0RVsgi4Re1vw#O8z`*<)f}w=$el3P<651mj#$$L%Nz zILE7mVBEyS`{s(_*;Y+(0o_Vr)wAJI33uj*g7U*~p56pU8O8(msDxK|$^d zds&6*JXzp77>6urOUlZQGYxrzHXf6aj%;*K^v&y(NUEScd7*{%VB73y6KDDxraTlb z*|$U3#m*Cwi|3?m7N(j=?aU4o`Q0TF{%1~>In+Dvo9nFfR1?7dC8TL_H$8vfkJt|9 zP}I|qN)(D)PR+w%!W?_#6#5uq+JPkZ{z9Wh*yzHCGN1~O`&N@C_gwR{9p(aRoRJld z%IG`M)#?n-m~Yq_D%6&bka@_V>A4E(c$J;YC^9dPQeLAkFBZ*MTdfnt!%jQ^?}QP1 z%nqHN?P_qZX;-J^s#rQwa8HNjCB`DJcRu>Q8%=q_EOv2rU^Y=e=hS1Lz#?F|Gh*`v z3BV7V%-R?*nMk`^#_?9Z?&{n_P{wdtd+{4#YZ`93LuhzTv!-pQY!gZbr7KGB-+X1` z2&55LQBrEx$IL38wUId}eT=QfdJU6nD|3H4j`iw(Vqw;5{C@jxQMYisojumNPHbIr zToK$BiD|>rbzxI7c_S~~oG7tfTpt!%Sco9=9pm$$2%NbWL(K6bcvfdwTdaD#&_9GI zuia8Jrc%sjz-dmes`eg^^Ye83QC{ch=E>*QH!YnTXL#Jc@2#*3o8iv?Br24kE&J=g zabjs(t2|irRKTt~y&g2APJ<-DPI+FdO&m-`eh$4rW(94Jpon!1bEGxa7~Crs;_{ne z=tQ_#ddoap`nvFIicAJMX>-4BavZ(rRGL>ZDd-y-SvP2}dVNGT4KZQZVg#`X=;U#$ zXqF>*uwi<{Wh|XD6V2Lkjv-n}7MTld$OW9|@tQ6oTD>sg=IN)k602o3ZFVoa{@ky{ z=4x7D_-?x_zTT;Xp)YdU{yP6#%fldv&h3NqJN#COB&NkfCKo2v>qmR52KNt++y8rbA5;e-Y0PsWbOO&O@nV<}z;V1oOHo!e7e!K_YzSOvMI9LLwWp~AceDo?1VYUYporp$OO+ymhhw zHkztP8LnQ%1R$O7Y-H;9Det%w#oPg*s4!!z&94ER!)h~i<)WvMHqLhpvG_y`dMR|7 zQV*%^rDLKyOyP}kqZU4gT184`o67#=<|HAZI<_)Dyp02Z^$QpQfg5|M(^xSEJQs>4 zJ0J&Io}In+@ufEr?4?rW-Fn+TtWGRpQBRKso63$^L?~%UWgSd;2Upm{a8THk+{!t#jRFaf=KTC{p2r3 z2iAme>Z+QSX8)_LKk1@&V)|CS8j46SA~XRr6fyO=f5t$?0pZ6=VX&I+9L!$Qz@QI% zv#Dnl7s@+HtbNY~15b9?v^LSELeN8yuZ11f#UkTrSZXLqy=>@5{evdkS8U0pTl@qj z6awxZ(78NGLX=Lv-^=q``kjZL%qOC}mk`v)NCpaS;>PhNqV|pu;rj>l)zaQpITAa+ z2aW`kfGA_hIK(ZAb`xF1v`RjI@(2#EE+G+x;zEa-sVV-pSk4iJa3wY};KU!PS6 zZ73`Fh7~VRmNUTH5Qu*DqC>R{K}MTg-6obW)FP=qA&N|tL7>w0{5^^!GZ@9D5a*n9 z{@V|E7rkdSL8%(#bTjbC8wU{5}6^eZb&Z!;5Ty)WX^pCN>m zUzFKm%Y(cw;4?SjPnE~E#EflQFeT;?7rvPAi|NjL}3AgX4Ce~DOgjX zj@%>cn!}S9c@aIafBN<-!8_-R@|?@aK3H9GEc-v=B{B5_lT~S_uzIlg;16VqH9*I5hA)y>N@IN@3@ zq|9G;jF<7w86T17ZvDaCmRdgU!e9e4$QfzZD%AN4%A$8A!Lo!SO}U9S$cMMnCq^)^ zJXVjPD#XT4XG!e!t#ol$D>_VdUgV4%U*dMvbW?eM?4h$n?m1Abt7u2`4%}*)%Fd0t zq3j&(fvA%+`a6w&AsE8#D7GI0KC#DB2z_rJ9J|f56LgdRfc?;F#b`O z+ZiJ>ZgZ027Nnj1Y+B3g-VcO$J^iy)c)hnC-Xptt4qB(DzjL9`%H`h4^W#J4#Dv{6 zy$VG*5x9hS65p2(Se}XJ(o&;suj0t{gi-q#@_nfmPuyc zr83&jFu|z-XpOr@9pUiTV9-;a&L)=^T zxPKs7UMlj0Fwv!`dWpE>zCx8Uoc!}Wl`CA7HWqdQQjb;qT1p-oKoug5M<_Bm%U(vv;lPWMGn)-k zz77apJ3z)Hvx0w?tfSqC7-M%RT8QY64v|id>A!v9ZOX!4tv}DqUeDc;P;n3uH9z-$ zG>Pi|Drg=vw#^JY5GRBM6gx{s#MMX22-Y7Umk&JNjIDT+f3Cs$c&uORw^(NLRKJch zlFNXFf8{|SH;l!w=B8yt;g}bsup!hY6qg5x5DHo*Fqj= z$BQa=`PBs#YzygGu<4xAt)=?8y5`u&7_%w0r+wyGVme=6y>QU9&zHNL$1$xZi+LA~ zm04;Q6s2Wc9Xx~2Dl>`OOBIU;WR~=sP_&nghvF^W zf-xULI}C7cc#ZOQwm&LcYxm+mFUwfh;!lvGgISG8;Jx1cxnAQYs6W-3zB0I<%!M8B zvS(0>5}Hyj1}tm+x#hBm$4=xj*yAj>WFhuU0v^dJegVHMVv|Ybbz1F`^V>Fu1mcG# z?IFyoq<;B!{ko?oy2!fXzIVVMgut0zw|Csz6CBEn`sf;XH9xR~&OgEqO6Fr_0v!O{ z#7GiQ_XcgK)r4t_T{hBk{{sRlc>jHtr^nBzO5%$q!3ddk|YNAg#*jiOg28rp3xUN zVoqL0V+?aIQ$B#CtyIWW;%YN+H$wRo6tvJ6_&fIQ=g#AXy-xVMlIf$2(6gHhBs?Ew z$gf6?pE`{!mFU~Z!9FjcSF+FDTtn&MRKjB3kt=S^J9`Jo;On2|+#Ug>ZFR;8cTdasKGQ1)l)}iBRE29hLG8yL-0k`0v=_8N9Cb6zlmfjba z(j+sOhl@#HR%BHFis>oShSpym3!{#&-2RZ=6s203PROEhl@(KimES z8#TxLv#Id^N?X9c>iYkw?VPibK7!jVImbpqzR?5En#O%}2w-_X$HpNAy!GM)@K95s zAMOM0%Bz)e>`|h}fD0*+ZVp6VYo8^6xOjF?Q+@#@#%8wA z$H3e2U|dco8-DYR-crt~>m>|YMORt~PzoKxCiprR6v-vo=*!X`_HNDa1H!w~SW;hs z_K-P56Eb=*+?TM!LZ-v z@}3FoX(al|aPa-(U?|n+a6hG`xG|21`sa37qi=ya*c8+F@L~W!+)anW4xsySfJOkGOSA0*3wO07&;4<&ZSaH^pCi2 zjUTBHQw_~j_} z5Ki=fP19&YCAd=28rD|SF+7?4gVEQtItn*g>vE0#O)%bKAx|-8m415iDZl6_)3=Qz zvvW^kM#Ppik}r{l{9*xvc`A<9^ zdKCVn%T<~WZ_qQZX0FUGQ3DHmd8IKI;x_^tKnzL0qc)yPky0xrKxDdo0b_Mxto2AZ z+gF{3GtIWvn@xlKytEfGLSi*5q#gLaf}QazF4xNI;4eSB3m(q*x!1CDql&&8vhHBn zg4fY7do%qncb(&T|AB0S$j~zfzv!LA3ER(q+z8+|oNx-PHM%qMZ-2G9f5;%m|48wd z>ekyV(*=AGq^nW@B(JNSX^9)`l2}Hf${V@T!Wx@-8RfQC-Ib@#Js+tObR(8vvjbR* z^r(l|o8Tfquw%aaBw>#ENEe;AW>u>{?o$4#4u5)qB!N6aZnnnE?yo0(CDc73!RaI+ zrCC7cb;aVDTq45e{(@l{SETd|W|l3U%90Ymx7@IB5bFp)U<(hpH0*{D3-z=tOUaQq zH=wHRF?~A5$U-EhajU|isrr^SJaFfRxK}?OjKOz|Ghw`Q@#{fJS zRL|=(KKPN^PBZrEc6V??djjO zegqQGeMUi^D=&lXedG-TvOk*bCn!BRHjloKeq)RSx6p|#gQ@$fjLTj7kBd&y$~ZGV zh|{ny($ARa!LQehZ2mm&>!W(N2CxN3)VZ9s7qmQh=b@a=)mI1U%UV5z%SDVynEp&< zkHYQBKDD<{SA8Ki=k?p%;b>);YYOE3JG2ymyidK^zMPKS=>idhdWf(omzLYnx2p!jlK6NU};<5 zYqwEt={WV;O6geGNvIw=+*=&xtR|biHKlqWCHmfrqTo7zaFcubyWRDqxXaO2roqYU z=>9-MKYnyJ?OqpFX%8anJvk6`OJ*)={)Rc6v^0C|I0FnhqkC;jSdvOfelAMQvCyL3 z{Mmy?0@0+BcE`3C?(QkMyJ3a>XNXvD=I5w5nS0*_i*>>D2P={ZYYo|>si>tTXAa^P zfC2S+TdiN({OP0nqW|UCb7~?M{r8F7SE2iA#;9mP`^eX|ucozg)BDH8*~loIqmeR~ zWXvN~_@#xwa~~Iu4AT)j1$WD}MA;>pt0p-681I9wjX>W!-?xMhKA#tTGP}?-pLp-J z!P@fEXgiKlc>$UIdC^wzT*44`fs_P*s@FAo1a|;EzYfszfsb~wmi0QLNUuxQ7wqLCZlru|!(Gb12zz&F;4rBe5W;IKAP)BhZ;L+}}b`qhBgau@~9N zDvwNGxb`w1t+^q~Z?pa1+XGnmt(UAYo)6-HVf} zg#@5y>+?2Xz(jf;TW;sKKTU>7%;MLkHtsU6Mysh{mZvFaX%{q>l!3WOXACB zW{-=SY3Q#sD&x)7D@vPmOEpwW!*tvK&|P6ufAIf-$0#Zou$qL_82u|injSqyf;kqv zLOjMYFw7GB>(?jov4<;nov~rg9nLY%42Or*i&QB)Gt67DwX#w8fM~A=FN`lmd$fdd zS35LG21e&Z0XJF`N2-2;ry%n{s9!5k)3x`KyZC-Tq3|E91-*z-ux28KfH~?`Nb9F z@R*sJP)lAD!U?T$W4T)!G~pStvE*q+({2Sn*o=Nopmsaz*4uB5?Qgr&^Iqq%p>6SP zK8_!f4AjD+_6T{n#!~4C=Stz3B7IW zeFM{vbSZf2q#nT+d&j@ndB4jp$sf1>lf84i^v=Y3$UbIb-)EbmcuGF18}bsul{}ns za>2+yTb*(5bW=Iuh8s@Ni0>|ONqMsuy_KXT?sg&k_?kVJ{cL;VroyONC`)!D0g*kn z{01Ub7KVoBKE99C@Bl-h56#Qx($Ar{pxYRSkx0s=ydZtuNKZ%4xwq*r-Sa?j^75N~ zkjEmv>Q`feB$VJ@`GP)*{U)&nULI)G{#Gdl-d5X#|@_K&9;-_ za)qB9b=qSRhDOa&^Zc3Y%w0S@?lUHH# zY)X+jh&qV^FGVG#2t!K(CpVl*rpT(l=O)KQQRx0i;g(rtmn5C=SoK0yUFrC-W%2gO zb<9+by}`Hm3U4?bk@DF15P@)k)_MiJWzN1#`RuCv$S;}9(jPUnmB+0#ZkBb?|4gB( zp>Ynv`n771{TN@`qRvSjobB>1_rugMO38CUOg(-oZ|m1-H>kG-mY>iS#t~Gaa!MR< zMuN1LTE(}t9w6jLcan@QH%6pfni=N#b=<)V8WJ2X^^U?vDio4vSg-WbnH6qscZPhV zsF%G|!BSENi|$qt=*u`^99?973jEr|!XWS7JM#xBKkV>a-DkGB#<-nfWAyF(qy0<| zhI)?ZC4~iVJl*Ljd81uhRLW${WkUP!$a?Zjd3e8j40D}3VAAFe&++PXpePB z-buZK)3Y_FC?~L+bSGCw+_hO2H?9cmZC!M=7T%V>^xI_bm`eIZ!MP8r`m7Xs5Ni|O zTU%gA`0VWSVt#I?atAGVoK9Dml6-K*JE5L%ugwO{gSAR9Ylw`Oziv5v<5HlsHJ$BS`Opvt%YTN)M^_m4|**#u0RWvtD z^JIM@WhyBnw}iI$TB)4}jT$vVi6Zjzjakf=TDbc1yNlZa2KLMgOB-&t_jJ&9^56ppmka%Y?tSL2Hp$C1PQ$VNTs_@24oJooMm#9Gb)of0reB5> zgEW^o)ns#-R72l?&~Lzl*C^PKxQAY#9|)?MDz^LgME;B#XU=~Y>r2B$UJ!}GNRFgOk!|;lsLdaJ z!NRLtI@#-07|i8$I3jXm4M$#7vkY6gC`b^;r1Yr{(CJI*<`LF+)i0^i2%8!fnA|kg zLbV|Mw&kEw63g|g(b_MG;ov&IG~g){$CM@kC#@!LNEV3e)thq4v9pv~Vl-Vo1;gqW zyJmudBy@~BD&)IJRO;`x(f9*9JjnTT$~slUV_erHL~+kcXUaL|s-8%1%rkITu=p zIl{@QJ9#AOlXXn9`gs`NrKSx0%LDBf;ZHt9qJWF)xk)uDmw5$XN^OM|cD44&ThA@+ z$@|tths5N+v3nH6-7C%S&yu;KU+mpz(wD}x6J!i#>*CDgKaAw7P3*(4Ehq7H1IcXRI4AkIL;$uyHQqc@+Z%!H&BJfh~s zjIgN{`g2+%h?==v4H~?PO8U9@ikl#=Z*ca;w+HN>-o6AgPx;7dW$hVM?dHS_!21Ug01=LX zP%hX3;~0!{14gihps!`U>qkq3+-B>(DI(!e-ZoiU_G0bPQYCZO4R0aCc+zBIgP-|A zOC#I>#|Q<8*oc{Qj8 zciSuyGSP;z9G7Th}gOKQRKWs13jROEGWrGEts;7 zTINsf?|t5=?6YUruFBjQH*V+q`dz#Oavg2tQ20>E;*5g_tjrbR#N>r#9VafD0}y1?AD~ z-l5nVuu57FZ>Zb^kKyNRMpWz27l;dnX_2S(B5bnHHIghw+~Vo-o}t^RG!TklPCfCL z;0@k2dV;<|@A;0+=jIs}m4IEq3LVnbW-r+lqlEyI)tHRByLKIi5m#<#*}ip39KT6M zVF|@p!^l@(iblq8bSjnM_6i6ez1iwNSPvl&-(Wz&JJ8#$JX7SvB~FGek{V7kE}q;h zu4hemhh*~NkcGnA+h%2;MnTCLx_Z*_XM&W{j7AyvuSn$JZ`_?i|p@n_TGfWhwbx2ZeY4bGch zMy^nDM`;Ae)SDUQp)Oc;fBRaqi(G>5;kx_oMzSEGiMdKp&EbxIGB_oLG|V%RYrvutUmT?}v$S&Y zk6rlBI`Yb-&W&($N`1Y{f%IBPiE8mRkE>!T=8Qr2fCp`o+0*x_HN=35PYY@UIB=pZ zT9g$WFIvB!&RTa^GvrF>3=b_Dh7{jrV6n_j+^nc4-_;i;q8}i=p~jB+5j0TlowbA& zb}amj27QzdPB95dskd~z5@ZT&#hQ;OB2kJ)22pc|eKXa>?CR3CFR<^s7@98|j`Y`u z+`m!1*SX~JM)1_*vrE?Lt7swmj9O>1*&R>3IQBQc*m`Z&RuM6o(wuT_Us~e~cKWi=vvj z2&*M4PIao5jeO&tp_oW|A!ATkp0hAG zlnI)_v*kD7255{kiMmS+mg+r_+kYgeEa{V`4W12cX^RxbHbH~pqdKO5=3t ziD{~G)--^^T?o@7Au&Bp!5lce(g3qOUrXlL;?g-7U!;?g>ZrJ8bJ|aN0>4g@n=|;6 z8M8u$Rb!-CQF?2q2(4zYt#bds+j!-w3Q)lf`T&D7F3a7`r#vp38P!I{?fj}kEXn<eFOEWXLKD+uvNO6*tn zJNNtckQI^j11j$m)9@?lQRU(HBzkc&I@*wZJwP9d!YFVmT5WGjP*d;A=@Juyx4nu( zM#gv=Dtx6VqMbp8usLxMy}@Ffn%yJB7o19Gw!?#hSO)T}F2O>URb1dVW~-Ya02O5B z)JvG?hCYeo#?!31CZ%^{%qb&ZdIRD>otHQfBhVX{#1t$dnSo_P*UNJ_MYj#2Pq;7LoRq~)wa8Vw~m)cMeKM^2=8 z!(%~*9?uiqAV!(GSm2-8YDP9yBSvnOcv;h;HHaM62xU)=WhnIp_WhAe8e587CY_ge z$Uty1!YIgz?KiLDmTDE`Q;%iAtIFx8>~10AJ)5^CJ~=HrFsXyA0_q;90!28*$)GNDUetP$6jeJ#pUDtp3`t1ngB^qTAj`!FSy30bFP z&#Fu|Lxx{64%s^DRJHQU%!{{&8p9lK8%oh34PrkX4?Ma&=MB6N6C~_28P_8oqKA4i)l9gd9ephn0*APJ}S31mEzwZW!pQ!Ke4&Mc`2%O zktB_X;qm#z8dXm8c$|~qp*(u--rh%KxCEMbF~foa1^vRQ6iim{8A?w^J~hsJ|Lp1^ zH!XxQFWCrj=k(ZhqE^GCZk_BfA!aXHmKSwLSNu= zPL(tL!7SU~E)TP=X0X3T1Odl}K_zf1m6U`4;)vkx_ux*HjN5hhZ7iSeH-<==7g}F? zrqd;=GXKp^-~EeQ6&f(}IJhdA{-f~a=Z|6xL$bEtyV~uHPJ&h{U@xkJd#+hQyf=W) zkX>xz(-j&@;A%a`+osE7i}z~1p4%R@4v(BTxxGK^x)DR23zX!OAe_f1h?B(A~-yx)KvOGO6acltzviI z4q!NrNLcpP)L+q4#+PyPPSCRsk565d7Z}BeD?e0pPBDoXqxZFE`AGD1yL`G;Nvw2c zx#zbEMooHur^$PzT6tS{d5psGg$-tJx?Sf+Yv3%DeY7&8-0F7>tL!gAEcV*DK&{ST zAqpwu(Bk4+9Mia^;ogiJ3)7KF9+VeB0PZy7x{epeQ>M?8cz&T;{doMjHlg8E4l^?S zr;kZT{(@rnx2cQ#+P2cxzG-ZUOdCwGMaYm$iae7klA&Z!SQ<>!nk zgmmHWtSKll3ZNQG&J4>C6W!r1clmz~}mD-PA~#`B0LM zIGpedhH*l$BBtZBbh9GL>f?5gw3<5#+fy)fX!43+^X3Y@V)2!RvrHc&YMr;)q*8{+(Ra|MY={u737I`*{s=_|mW|N2QB88(h?YM0M{yN*9o&;WI2 zcEsr0A|(EIIzr82U@*NLs+JTTpim=Iv`S~MiC>-&>3dQ9%p!Xo$NnzOCCYa?UXZ&R zBS%pXwCz=Of&5WYxG#;ypcNOL0)|eN4yRDV7nGmX;X}mdb_U9c#oq4eS+g7ZD|l~E z3nN{?)eo=wSzEW1Z|hg8TCm zB6$V2Sc#ND{_FVr*8p28fl~DU6n!uHBlkZgltc*@|5W&=P%rFzp~k-t`3X~q{;ySW zo0dqaS2pM1&{M%?xNu&y{!*O#KQT9g$jHCD-TJa*%y>(1g=Ho)(}KyvaQw3=sEt zFlpNAjJh6z$Y|Mx^ulszJ-8XC0cgoz?6ZGoRQQM_iPXgROuC4+7N~AO_Nu>D%~wbR zD_d`Gv6O~MtAate1WY9JN??HZazw;dS4qXG`5uWd8Rp!G{rDEmzRO2L(z5&9KDHMC zoxe4Z&z*fRt2v**rVA@j+C?%@fBIO-1Dy9N94!242$4?Uo{}CE41Ac^o(M`x+h z(YIsLspWt~dq4$zFnK;*>|S^)%QOXy#`eCxHP^ZK3sJ9*wspf{5$kHl9y^F5hHTz0 zyVEj@5^?-PDUOErG(*zy;}B6zQH2zq4noD?CFn^BRR8zgw| zf&X${;__-@aFNXuqj7z~Fg6=vml+H!7Ci<<7k8hzBTsMZNze!Myc6kAe&Cmz ztx++z7|)A1Ve$x~Y9i|rK_<3Fd(P@4L1G_Dma4p>Tb_x{l9@(DGI`sg03uHpOn<&F z=o5Ix=ROV?OPbC0o<%MTl~Q|~pc&n7&WUEO<)F5GWI&t^Uq0UMBYHI@U7(~}13?sx z^z$hibx~bpH#^5`LjBNYg+iqXfS}hz4A^97>MN`iTF7U-IguJhm|f5%GOYuCaqeGRN`{0Tg9Dm zRyDOIcb05=Qhr(~6 zN0dIWCwHA^GU$r(^(^y4D)c0E$mG!=4_E|U=)9`?2Br2i zH37y@NjEmk>MdOSNPoWZbt&=e`@C!YpE>?LQ{y0-Nurqt^VLrxa^I}&dQMlnc~A}> z@BPQa0o~^-cGQbG_c;{T2ym3LCy;3^o279`Lt4_e} z?zhscgNr#GH;Qg_*i2eu+?B@gxmZZA`PrzJrA*?_#Kq#kT!~GeYe;j8nV>m=QOuXg zAD^R#A$N&`cRQtgSIiFg)0*z>2Jc)7>e&HM8*9?*35fzFOFOF6!?3#B(&QT^Jb=tI z?Qdl%xJ$NnbE^{~GN|4Bny>x){C=iWbOBY{ zLm2#~Pywq%4lSLY8X=4HfpfRm989^R0_=(3S zL$E%51)laqL?rpO7U>{CvLa~XMSrq#gNz%D+`nu}LD&O?mzG@6_od>~MufjMPWf`r z(O5kB^<++bdbIDe9Fg%JPCwN?>d_y`MATze!NYGZzHwnOW8bJ>Jvc$9@3_6wE2W+1 z_s&=}#pf+e(})rBVF9ygQEYl3WAKC7bB0~|L`lDm@jO_#w6qxke&io+y33BEk zsmhKr^~8>>_D^%to&E>Zk4snF(;n80@+6eGd&X>Ud1*!8FVLa&v!+6hhk^MG<37Pv zn-hV}Mn6iq*yFm>JiPyi%lzu53AsLu=CEHH!jgt5KP1oNdg3c|RO0-l(xjKp* z@<~D*$D<0d+PtIm<6MVy z^(1NkvV!)c&4xEzQ-vz60_eu-`>M zN9@$oBi|ZPx~m|zL~mSmz)<)w=cs^Rv&ZZTFBy2ssi5)e;)L&gFJ3&YJE5fML#d&6 zyN2DnGVtqkXwZBJ=lzNJ=};W`kt<-#4?tBHJ`*(GMw-1kJ^gEQYOU`P4Lgf@y9WtV>g)20W$N3-H6R4W}?aCz`uXc9e z!@r;N^^%KHmGC+z<<7YET|n^xckkWTU|C<&2@Z9uB!7;~2@S@|a;j@f4%=ahx1plb zNDivlr?mMLmEy@C-u$XiJF%)Lyj(b5Bq$-C+Y)-Q>@Ga6utqk|-RHSDFuLs*8p}K4 z7b}@>Iq-iQS>{|mg1@)eg}&tNIeOSzj)kU79dPy$a}U>2tpuv#Q;7AhQN200MfV8d zwM7hmWyRkv$oL$(n{VAr#KpLt{`4U({i!0O>Ls-rYLWH|k?OnHJoDno+~4jAB7>Fi z_l{&+GhB!2Y6u~D_WzlYuFF%=t)FaMIus8U_GaU{YyG}flZClj6`K_VQ0@*?YPRw^ zvYzcC)D~l*zwePT1=6xyZVWko6Y*3NCP4iMf*{d4ZoA$FXVXdkS{%M|^S5sn-_#zm z8Du1-mM@`b)}<%K3NyF6**6;o6~uI6!(;bosY}TlW9N=L{9#>`Znh&1_?Vm zIE%@Afy@osu`joN-_Zk4Co4qf==~QC`6F7?&ES`uth`&YI#u$2Lmi@?chIfoK?Vi) zfiPe0*=4$8d@G|>eY5fjd38Q)fE3xjJRT(iRIWwXe-i}4#0Rx4 z2Xy$Ir0^fNJS7}LlIlNcQ5jsNu}|yKP%m(NH!oT)$>U{3vTw!oVq$rtFLQ^)*I(1w zdndEXnAjfPUhDpZJTXYO@y z&D#`mF=2Y;U>E@&6_MwkGR$G&OWzu>;>PqPmqv>kn;Ve4tjC<$5Eysl9A^rERwF3^ z|G`i)AA9ad2wc-LPOBNF7BLi9n3xhzPo0Q@{>_hw0JpS9d%Y8n`yEHJ7A5<;1d8QZ zzx@}K@X{Nrn?I+DqDc{XDht{}>7A+e?mqzkZd6&!1C23PCk9)PP67NFN9BB9YsQI$ z>S6Nn%>5s*!=yn<;HT$4edr6$EyweF?kp2eqRRk0N^oG+Lm|}}9e!lO+TKRpI(a&J zKF1L|;!OzjQAlW#@N~&&=f^Pq1McI6y$gp8Ua4jrgz+u+xhX@(164Igqy$Fu=3qX1 zF(ZH)9&YO4aK(q~bfb3M`={{1t*S9O0XbFl&E3&KuQ6wFTjyoXIQiEA)p`o~$ktOo zkY=sdE2kq&uRoFYEk?v652|geQ)*Mq?<2dn>7`hH-oa^}y(&3~6e_nZQhGHCb{2i%$n5u2 zNeN9@R)TW}^MoGCt)PO~N(h>eQX42`FRIkK_?`(=fk|oH=LW2XI41BM4W1B+h=m00 z4oH;FE9IF#X$AMV#rO{Y(eLx1{Ae5=G^P*0VS}3yX8&MsYF!{&Y7aL&TIXe@6V~>oEH6&zZ9mUNLRjTZnWyWOGgY8ib4poAItp>b_G0|;Z@Vm<&CAZ zydQ-osKkSyyOPAbOD=BFXdOMW>`@vHgg@{{vsxl}{(Sl@X7dF$9h+i~oAjOAZH(AKJ=2 z8YE1v@xk@_>@W}2%>H-AvCc*ZjKX5=2r`=J(*5b`@Nv0Pw%MfuBsnJ}A3ri{l)tWg zXC9-xSuratdg%HZ49!ZJt_5aw10XO0`@WfCv-?in9l?qs)+49=k2h(hT3~(4MQE}6 z{igK(S#h^NfFKtPn=nl|nyUYe<)!|MH3v#%I?votEHS*xRE*+^E|x_Fmne^cU46Iqz&g;Xs*tnAHt=c-81z#7=UE2)bD**I!P zLzTM>P$)rD<9MlCrQKAKa%mJ>x>x6=uqbq{Xv1a<#J=)h>gvQ68VsIW-~{8{8w9bE zCGyZUx)k67IEa@w{y{%BFf~rV!HKngAJS!4gT=>U7p}VB%;<2Q_QiTwA)8Y(&pr?T zw&-1#xL{diB!_`zOj#hQa8J<%)F%GzP8UA^DhehD!Lk=LDBxHO{hhS0Rj+Mv8pT$0 zh+GPrvM?)8^@)ShFJ3l$lri?5A++u|lyf#HLbXWiICtKfU4RwG{&w3I_H9NEuD zvpGI}Xu7|LUcA2ocqa%#LKoZjf@cGqR~C)(Zc`+H?`L8`F#x0RlTBb$Ni`gTs+X>B7C4f1eLFAcLqn+gIRNKOB}_bVvFgWd}XXd1hK@488sqX zOXsvpvsGvk5;6v)kWZGy(@YQJP+5|wqI?P}*1P!%s=@3h)M1Y2|IA~S(z5?r6;GqZ z-n}=^nIfdbU6bKVU;?e%ns1-GZHWN1*t(MR@Ms|%&pSV{>EDUx*c)bnoZvcsLgh-Kd@>p| zC$TXrnLJ&Ts2~0fQV%?0R);cGZB6G(lKDH#n*ASD{H?w#phL6?Z&oyeN5Us z3jd8gFbL-=m+%Mnmgndm#zc4$x)cvUs)8nJvc{RHlrQ3;V~{!Gz1_Dz-_=I%=A2&% zq7qXA(f-;NjiX3ZmjVSge?n;g27V+vxmcmHpqT(R2g|0=OgDGIQ>D3r2&XDh9X zmk$hx|G)$k{UDlK8J0{P@*QRp01enG@QVO$C8{Pkc;ymWDN*c0a_WbNJUccr6R z7U95EJF&GJh%dw+CNl#?ZustUlDcjc7sc8lr(_CrLj?r^9^q*m-srCtsNmdGl-5xH zVy^seN(bnq$>LPt2*$DNJy@4bLiBmH2NV%)jE@|--Mw9Z3ctxXbVX{u%|}6Jz$9}vHxUT`)&wSHjhc{$Mt0nMlNnG!%vZFrRLNLQO1krqmG>5)Zi+Q z5_2I1VFm>V#x4dd8KwI(Zed=Qh#&lF??lrFiu93Y&RMO5xtYn72Rr754(8lg6M^W- zl%{(>kaJTE0yVHtT>7QPhuhsQM`?K;Lj)V(Lt?$>hiZ$7N1U>l1If}jZQ73*Ug;#M zY2!a}87P&^(^nF3L~LIO+;}AZpr7PAO|xr1}Ksu&i(@6W;Rqge_n}U0&(>*ZgPb+AnisJe5544!TK4!^7aD8qC7gLH%2O99ZJP z>Cf0XI#y`QCcEWRhQdJT**AMg+LBrA`X8MNx-j(HYb>-*N)Ap&@@go-WGDj@`XB2Y z$()6>iO%O3v155;bm8l6w3)2gkxF5Oi<3xM6A$J$CHp_*z99F12b(ZG(>gpW zt*a6M%q~X-l0#bZBwUhb%aZl$%dn{NENSDP@b!e6e9wp6vqICPPV1PDkQszGsxGY-S;I^$kqT8pgK77!S;F+3z=Ps7^A=G+QdCd;sWIw1U&* zS}N{Xq;61DI}`k^mrX>q(-HtD2VCbw3__uG7t85MEdHJa&=n*+?SXn&L;MPQQX5RasOJw zE6E#68^81^CGFD^^Rf7i1fl-(d=PCW$mUdJXC3JsD|{EiqjKFqAPFhmMEZQl=g5!Y zM-My;C-?E0-)~I$g$c`m%EV@0Y*^WyjFF_!s{4koF-;af?)KhOXJ4VK&&wS~0T+q1 zbrDr2t$cAkF>WSjb!rcR(iP&5`OWX7SsQ<;LY$Eg45X}BUW((o8GhRVP=Z}QT?aZV zsl$Gk&!wR4OX^OxpX05aP>rN z*&15{AY@Ba@g`fjCG;01Iq-$=HqW{t zgCmr5s8M@iu+-evtk9*;oDs@!(6{eJKhAg-o_SFje=OlYoQ3WY?E{&ds7pv}lZ2?1 z*OFLfQmY9HJDEUpHbGj}EKAyd0~SKfRt;4XAZ!6J(l~DdM>gVndz&H$)bhdT20lse zPoEaGXw?$kT5hK^D!{8z>lzLdgfTdkg-%Y8`$~hoytp6dLzPfcVtrU`)THB0x3+1< zGR3ETe!#zJXn%YKnXD|f$;}2!AkRz@h&Vufm2|m%%O8`rqQ%^F3&22BnmfT0JuCyK zgyS?9LQuAEQ386cYKRIV4w!I&zFZ|;NYCuQQ=72?Cb@3QHgFqA>B{^-<(uc$C&Q^| zG98%u-LdhAWKsL(yT@1$DWEy=r3$T#iHRsQsV>Kh;mY7;oXW%+>OTMj?|Q&({^~tb zXpQt&Nm5%rU9r6w5Uq&l*!Jf$NIKb~;M`=m zLH>TihNsFqjUn;e=MBl)gtko?a^F-)-yS7~*|0s^i7yonPq^cHf=&WQ%1qH=|F$D- zFx00Z8Q+0ROtkN{lx0xlfElzTmM2rZq)Et%qng;%a1R$t-MppZTC`9abHWRcBFg&v z!t~8j%Is+5MkDdpfE?*JU1ck1wn!;ae5e|Hw?tkjlmX+|ZnA8S$(ByCX(=Xa@+igj z0kigoe^VJ^1kzHt@BPfE%a}i@1gYMz^jY2y*w2+4LMIy^QX1F?fD z$3)IT82`fl4?yvo)RV0t;cAC-Fv)j*>NXOUq#Ueqp^ED7PrNP`!Q(mFo=z*H=CtTHe%HEj?nJrp5QF}D?anqE927PoHEoU z?>pbG$@#p?>~&IfB_)NAGM1g3GJYmB7#`t-9dj2N2Hu+(khY%v5&RJ9oKH>x^N%ua zr~WfLkGnjpGu_oe&in)Z2!~mL7U}!Y&lW$cOpV%qK?Pp}#=kowgv#@a6362yeUcG^ z6!L8tLVaoUS_+K+G}D?R;_C>;#p-Y2g6M^OxV$nkt9J|W@ zXz2lx{yzQv6|%TFg*()19$?uFhjbH!KZPP2Kai?FO`e4L`gi6CL0Wx)EyI6{`WH^9 z)&COZlcW9z0|@&+V*u2OxrbNQ1^+GAw_yH>{eLFp|KHI5|Nm6QjaJh=Q`_9^l#MD9 z6BkzwTbt$m{XK8W@y-%*#sU!cJ{s@=x?M?-qO&kJsDPN|%NmZdzKBx9yq({rKTBVB8 zCRO3hKh-Tj_sSUZU*pP7yyowP;+pd%sNR3K_x~bI^8d|<`rpiol`^(8H5d?h|L{N= zSuAtR)a7c3KaO4$AA2s_uQ~}wvGK)6Ql-YJCY0={1hy-8f9s6~@CF2dE2|^9^ix!o z(}G~GSMTgLRBGaGdtfq76c?XaVtYlRKE2Uh&A~?F`h3*{xV6w7hxkpKcY-i=f@WM! z8QqqQAo$3C8)sVSUcTCxy1JekUV1~M@JC=^meTAAF%?r>)1yn_O~TB^^_S0^&edm5 z92ac2yY8$*+s1qmAJvbDt#?JT-i_?Ky)J;l&N)y(tuyt)GF~gZ970?*iL`Wrc-!s@^Cp`L!M(mU3FNymHam|*tS>GQTLT>Yx|jiceI zZ$kae`j9MS_lg)xy$!pB3WyS{zXD*^W_CJ3-XE$X_lqMc{Np8!Fp&o z_`LS4UT zrPQP|@s0Tje)Uc!oW^ZEDI^#no8$SAu3cye?cO+Dk4c}L%gSKcyAZERkH;bA0fu5O zf(-HJ(j?FI0PjBa1s$h_(i#KEeN_8$gfm;7_&7g$pNx?h9o%#6hEB89a}oU!Eb7}7 z6$Cx_E0B-fcD-#B{sZ2Kn`%q4XCesGgh@zruS9_=H8a252KTpyGc)B==hRwy5();s zU)oQo2|+ch90WL0n<7M-0rPe=v!!^G^rq=WZ|6 z2}$@xd7ED|gn5s!=ZF_eOf}58)EEzsZYNmTV}DSprUPf*d?=+QM86EwzrU7;K0!E6 z`fSqK#zypG)LMwsbU7)$ZR($DWzEKF#|R*ijp?>!Z(VvMe_VBr1D`fQ>nt)Ta6|Dh z7`|{&^h|UL5N?Kki^Ar6xl-rSro5JE(Z3%7OY}{VU=P52ce-q+N6#$FB z&-G$R#<@IK22XvmH;y&~94J*@&pl#D1e7`Nt1I6}ze`el$fofp5F&v+Qs(0e!!lmc zbEhjM9Ve%WP8NDx*JP!oBDBhr$H=rU1o1&>R^DXv?XG8)@ep>Yp-Tz9*{=9MhOarE zART^$@*(PSu}t<1kNvL$;|y2dYiM0BdC~S{`dIx~vxUv);M0+*u>A>Bw1#$Ne}rxt zHQLP*q6WEF=th*+m{Ky={z{`DkMU35WLnYI3tC@)QP++`y8I{59NR(B86&U;6?2&)cvXCda9q4Iz(E#ik0`WS`$)ufIu= zgL15GZAc&Z18x@IvS-fIr3DPWbm`wg-!jc8d9I!O^5j>x0RW76Baok%l@E z$LNF)r7Z&WF5Y^aEBc z9l@?-6g()_XHwWJm!F|h%(&?IrLseTPP$)?l%AYk?Zn0-xsC-` zF_T6VajFEtQoxm99Zm?n930!Dlz5rJ+1iIUpChUw@wSq;Wmc1_Ol2+f(U@eq0|yw$HcKV`CuoFM%yZgBIym=!%T z7he3HSAYJ&iv9&Y^cA)6fhAVxv4f#UYVs(A0_0Rhn>ltEQQ|`TPG?Dj8KpEvocT-W zB;j$scaVR>MUxiw_UY?sBXPWdHHlK-M;o*R zF$q%e-*T+k6XSaSN9<_W75NWpb4fvenW)bbK96;g%VGUN_<~>vJW+~O)+sMb(}1OT zBv`wsHNk0WYp|MgUa@$z>ukwB)w@6JTo9LG;lStOpe@!3pO zY?I#ItWZ2ad8Z;{kRge8e3C?Mou5RR zsiV^`H$g&LRNG(R!aR@N71^yK&#;|_8N;YVBTQmPjg>~KgL~?CbU530fXqEu$*gfD zeme~=ASf5{TGnvU*$@tiL+&WHeEEw=RM)cjihG7EqHE3Z4PvW(yBBYU=+|xDClmJi z)Y9gtG;8;;)H=jdR?dNPy_Q2lSW7nG9|l+tS09CJ3`qfeJHPv;&$f-IS&coSIl&Qra5fd)X_MD1qJVvq%p42Tm;#l^B3_B z*rBruhHL6iFPL{=^tuko#d%;8#e92TzZfShr4$bFwChD=q8lxUS9IFq0Myfk1^!?s zP6n)64V@N}D(UuYAX*X4F*?sfllfsYWX{T~vLf>#M>~ zp})V?9+_PsL%i^9)RrGDPUvwG(ea5jixpjY4T|m40QM}I{70Gdz-q)|kq9ZhjQ9N! zPLX-8HfAeeW9U%xZZ^bHULEfWJdrITh_gNc(QUxua@dN^5Fh)3QDZVr*Q7qxhw~B{ zT?CbM~+!g0owQcX8R}FC6hnIDkm$h|A%Zy-D)yNcdfa9lj7NylIDOiO= z0)Z{Rn^67{aC?LV3y+K=Fr@65WzK(8Ls1QgR~ zUIdZ29l4fzDgTV_a1d1WC#MWOe!}5yIWp{X@f&Ci{m?+*8#hE;Cx823I@tIEg8EZBC4CP$q66Y|CPNfuE;5MV6USS*1(Vhwh4A= zx*Y>9_f)6Pi#p5=;w>yeQtvSSYM)`X@<`^8M;JluyT^Gf?fLti*``#>jlRuvq{#2z zO}EFE1xfj1s*bm6}Q1g$5AFGj~a52PX8aY zy>(DrTlDW4LINRpa3_Hv!QCN9aCdiT+-aOnfZ!4!xCIOD?m-)OcW>MpZ)7_6-rsv~ zs%HL~s;PpatLfAGoGoi@S>Mkp(+t7p+g-%`2c&?CRUKLaPG8LEY(BF3Bc@!wg=@#GJ}C&ke-=MKF6Ex2Dv`!!8A)ls`oq^uqI;eRdm~qmTfNGb z^LTip?RujoAte{yfJzkh3h}aj+&UxTT93u~*eo}9_b`jjyjMZQ>~}SY4Xyh{PPJlFdDnDRNo3u+K3qP$*OE44iBflhB z_)(P5b=?PCW#*9?METkj-Zpj;C!Au-(ND4!-!@nh-37Rj&bk&&6^D(YlG;hCzTN6# z)jHX1@<_kN=Fx(SwDBfE&*qoRuHJRH%gMAm2^_sgwqKA0abm=7Gdy`4r-6P#yv3Z7hh z7bke_J$@@^zxES^bC+15%$V$P%njDkklv|)9`{!*DSX-CeMX4J=9Jf5v#VZAG8+h! ziJszL2a)Z&Tr`(&*cTWQTZBpQX%yb?^Gl7dJol#B?!Mb*af|}IcFcN_Q{!lP+|7c0 zTx(CyCB?UdjP)m-OHa@-EyvQL3Ms za@~f-x9Ul+Wg_D9QfC(3;#_EiM(y@!1oAKF zwx~V~5gt&WYO^T@+3|kJYc=*t<&0bOP41E%i>j@&W7#3U@f@NQ_(Gg-M-M|qiiy#q zW4V{VPi2*1cg?TV`Aeapw@YM~Etdb9n#Vb(7>DROV^xW0p)^ zeZmf5{M|8Q?u&N1n^HmZ(a$ z6rPOi+%r=)deK&_@N7YVvvbB&n;!&7emlXBFHy{vMZa|n$jqKMYfpQt#=t_5hC@M` zi=(`+S+i>2vdj85aR_Jt8z$fhABmcAlf zbo0)6vxnZAy5FMQ4L_xZ>uZW-_a4gE+1$Fg55K~sL~`DkzgN1+&A%6udc@7ORI)dN z#>`exN}_99G2BtHkP^y?ro0S0?2klbRaMiisLiiZ&m`YEk9QGlCbX{7(VNM3mVA}A zcU8+z$NXIz^i{K1gb46ftR~D=wgwv4mCj3VckCXrl3++A9+5X&{E{rHp22`AGltS; zKh8{G_q~W>gj~kJPmfTxtj$J1g@ycdy(~LQr1UlK0j~-g_%QE*EUw$&W=*u+P~hY9 zXh?3Lb-4)57!P^iiH)xO3*DSJ!DvLo7x?^gMq5Q>gDfvHSyi4Uh3poN_KCNG{=jW# zkr@#Km4Fr9b`4SlgeQeEYKm3L(#xw3*@2edlu}SxfI?elH zo^3KA8=%2k(XTUr^7sWZay-q`_FU43V?;N8$FRA3!Gw-SR0Z*`pJKgi+m9bYgWldp zKgMFN`ln3flIs%DwZ&jwYnman;v{l_i+IevPCelLz1Yu^#$c!}=E=ybd0ana}J)61>ZFwMDLml^&lnFZcs z_O)?w&F@WUqs8#Ny?0^Lk#~8jV{FoF$N#d0ZUx~>$(L;rM9$}LnHq3SHLzw{IPnq? zI)qM7OAjptzDx8;E}!_i@f=mve|}cLnTjA5CU10P?i^H80Z&}s5Hr5W#Ys$l$miYe zo%N`+79x|WzB1=`jxYN5RlvYs!`5eFvvWrl-830pc4<|VGu>;CgXCqRWu^b=6QF_0 zwSwi@j`SReX){V0NDs}_v$H0=Q9s9K|=q*ovR})mH{&ZQaTyRV|A$?<7TEOf# z=u;r)Zu>-=4a-YSFF=%Y#kZ%3NoyjU2#fUC$NfGrvkl-s$Y>glNXwm&qZv^7yDbpig<0Z~oQu zZ*30^u#)Xq#1+Oj?vMIfW3(4V`o$+BufQ3NcdW_kF0N!DZOLpuNnJ>&Z*?a|K1%0n z|41fQU^V9FFnrXZi=l>L37uNz@4U&bQk9zC$?FS8YDVA;>xpdQ%4~)8Q%j{k&N0%w zBrNCqlu-(o12dD9TjlF|3{eT*Y}06ts8VILd!S-IU1K|>8FeXU`6Eg5qr2b=f5|d*^F&0m(n@ z#E!A=Ck01T_X*8S@l{xJ&~)jF;-qffOLQPAGU~ZYl^AUW?LT&~j0W|l_RDoobr94F zoTiAq#x`Uw2H5C_zoE{muD5E;=I1qqURZu*#eeSRxArebR@WiP|6;+ZVkkHQ%M{pi zV#H0~E(+8P;m4K!6|!-ixXHdBp&*U_?DVKzAX_}}5TY+*l77;b#}wk}+QF$>BGd8> zeT(dbL*;jVt9-y2sYBDLVf7A%&)rf>-#i(Bku;>UQ0D%cKW_Rn^!*e2$>w~;w}mRJ zZzsDGO(PtTj~ioH0Hvsc;rdB+!x7f?j&-Cs!34;eRC3k_i|xdCMSmTmf_fy%uT8IG zJdPoPnJM=oLTnCN? zn{e2V5PND%qgOuh&Yq3?QJ0yuEO5QSG8GfH`N4X%!bTI}=Ghl#fmtfwHq^ii`DTsL zo!VROMwPR7>}r8$t#`4Mx;IAxy~?(N)&O-}w%Z)3+g5Q1rWg|I9~FXw-4Zdc#cZ!Z z*s^QFy_UM=me+F0MRy~G=_;3s#Hw=NaDE-v&gzZXI!Fun0i3xDL03~;+yBI}l|3nm zla>iq@Nn3f!*rnLP(@GvC-}sB@!f?WFGiq>HD_V_b+@4)RTOoD%k_MrjIUbIsl+J!1QvGokybVc{K7I;9_J1O#Le#0@u7ZZ)-TFQW-UtdHZ~^~{u@!O} z9$x!j^?7CyeEU}??4>>(xiP}jy4ea1;czDLTo=B>!u;iY>L=)792u25AX2h z`h!pFCmDy*eMp(2zQ!-Kt^bOPvo_1a$|k7&@7-za876y>a*qNk+A;F($9=3Vog|xY8mAjaaX` zz)`|Lf)w4p#G>?y2>xhMGfZt>7F)j?Kfq-`3Aat&f?u}=+#H6rx zl2X9nJXEBmZ-Myi!94`R%FEGCG3o^nT)x=s{qSHk@RURnnluDG8^^-;O{Sj{>NZI5 zy_oE9n(cntKEPvfUb)$oMed{|dkUx+>t`1IjpKN}aX8j;5-)UWUH)8EOK=6hG|@!a z*7pp_ICOM!j_>lCgxrH3`^_sgz^D5?JONcQ!6}NptvaZt zh?fs|7L_=kBzp^s1+HUvq$nzwGXF+o!wHww`WI}yN9XgIjE4`&e|@hD`^&jQX*(mG-0Kzqu@46W;y0ak(nnJ z#d!=#I6e|`=&_5RS|UJ_AEj!Plr>x&tT6nv*+Y~&)iR?ULfzDH8&MXoRY)6X($H#! zE#~?hTbKD8O+Lt)nZ?Ys#+aMPn$JI32+kZ}p{bp_zJ*%|Ylg=N&GUpi9AWY=E;RY7~`*N+7NIJop;AmhmIp6L_Gm`RoFU{&h-d;Vg+`a_g!20WXu zvr0uz#qq-67EWPSx9H6K+@I~2EWVVu@K~Yhop7?ET_4P|LG+q^foo!s=M~yZs*$92 z5R8VMcNFR&A{TZ+WZHN<0M@}-?O)6E4&brL^u5c;0bdC?`zV+jSOf^O^;$DjP^MmTplI5VlcW!lcS9KN{jfwJg4>r z&7u&Q@P~g;ATLgJP+aRjp(7(NCxAC5I5b(_{u3eL#jkZQw!hg^0C*!WL-U?FJoLxs z2iU0c^&G^@Wo%x6M4MQSZ<+Y>;xhGSt~ko|&F_{CEYR%^2wJq{(aa`2YM~&@x|&M2 z%c8Fc1TuR+PpMC5fvP5U(z%mI+NcQ~@^@{a$Y$rSfhojzs{G8wNtuv%Vf zKsyDbnkt`K{*8?iQJaveSl*{ddI#QQqAd?9qKu}e7!BR0(Bdvmc3*aPL_iT9{O<8j zsEocB{QM7379Pao^!|AjFc1^#G4jKMp{kx@^J?wrX^}wdpZKsdcXm9#%fSpacu*K~ zsA0U$qCvM5y0|29%+cnJ66*JV;$i~Mv8ZPLR{##~l?vP67_xuCEArUc>Zt#|gpXMG zwElm84*PHE|9+13OVI!Q5+$Azo*F+0x5ESjQ@Qawp2|BB_?e8z5be0h^$i{Jzl0W0 z1AK`q`x!^+Bp7~-KR*RO%{kh;%2I;fX57inO~pt?xk-fnTSVs?tr)!Te zJYG=Oc1seYTadyzvN-Nnob@TnCF+AxzbU$zGw)-3JooNQy%T^0@`5D}YlB-NkHq}{ z6YcZ&8P<&9N`l7cdeR)L&2@_UvvHgy&y)PQ`4&fl3Xz6*2+(dP8xrv|#!r0w9SKEn z-;M1a91Zl(lmD#`keQ5Gw}Q?qj2X3BR{@`rVOiw=Y9*io#aaH&t;U;UX)ajxX;M>I z=AMnjRqf4{R^W&jK#d3)c$`y<*xwikb{zU_P?r|+YbgSq6F89A79wnC!x7Z~FwAcrT z+QV+Mlj|e+sRK1t$$<1oLB+*= zyLTzo=`6nx^+e+hd8WWGPP~7gHyIvX5ECAI5D`RkXn3NFrvN)g#64xnsT~aBW(!I4 zuSaPkF7(XWGWaDsN~v|{q>qr|Y|Kr1QK*cVSJ0E2gVznl(JH+9KdAl2vAE^BeEMlT z@f790eQEuAr)GYiI7yjvJV9(b_E^O%>*ZN^SjBGs!f=T)jNs-+#hfXhd-CSv7hDR) z-0yHWJvVf{%)0RF?gji#MC2og_KJTlAG2)6f65Q}uNjh%;g}mbtBJ9FxO=zTNm%)g zd(5zXoK#=E6{z`U)bzO7^B*0u@?RP9;VRKT8szO;WIX2G(I6zNKX!Tfde$6B_fdBJ z7~PA?{c16%+an&5-B%G>gL_vpA3)-noZdoWg8SGZ3v8SIn2t}^Caa~Kxw4GwG!3hN zJgo-Sv_A*Lgk5O)kE*d8|D~6HIc@E8jd9E`>=ZNJMVh4*bF^Ul0{diD^Wr!Ez)HK= zpc2F5e|@Y`5KH#zOU^O0!&o_z-2>lWBI$nbe$L;qpH`W^PLx;ZN#~4a$a>L>Yx)sTsSb9L$|1IzJ2h zbk%xJWOvS5VmEI-?R|{A8&bms{!a~x{H?)ct0E41bOgA{?=wj-#vhljxm@^vnb$yDRqe#9P(##a~!gprd|cT%XZ~H{k1S<>;>Qd`^&q| zADNdx-asZyi#E|%MEFOWR498=-mfZk=S!*` zt8#`JK;&o6upyd44I`{>Z7@o(yHn2A_J3cZ#r;piqeNW&Izk%zsEIl|%B@Rkc@fY% z@8$KX7^`|b?9cC$=oqId zM2^f2mG-qHLx(6UjOqEW;Ba86J!W&GB9s8QM6^AMah#5hR{M5V8i^X4tW1s-ZLs~dIC;UAvtZJ{Y^cO96#_Jlje(kp4 z9aMrJxCD}JV)zW@9nrQIbLT5=T)h@`yfSx;p`=K*?i!ST%iuwG)cuq~I|yY90Xc?me@vBAGG_M-|n@y*Hll^Mo> z8%~1CS7!dyWL7TG8r1$srXBM(yZ@XS>&QPY^U95iVTRV@F){bai_s`iG2DSWwta=u zzdl6d;U>hP#gSDR9LZNA6Qw+*dc8(k3Esw)#9Nl0416gZ)fZH95&wn9vW3B-?|M~Xy zi!boKyhvY%`+e`(r#+JsU2?bp&*9orFd4ou{Krc;HT~T>m3EUAE}86RB>_T>9M!#r zt>K&4|NeV{3jfo;V+H<(+O$-!^#A+B;s3*2v9|t1`p@J9L`0|Y`-sB3)>UQ#E1+c3$>~oir>W-%AFZGNPj3+n`Fkga3xE0zk{8I7Dv^rUpZ5`) z7r?sC#Ja`^YNHKmLo_)>F*#kAZRog!&!KWQQg00%Qbgxlo^YM*;Vq33|LX@f=bFU~?>KMsy@{PK^O#43qGr}Q5=*TFjAk=lQ`(*D&Kf4j^qkQX z^Ou?l(w-0ileXI^E8`EQ+Q^SO+I@yENE(HC5PU3{$jFa+{hCj!%t0PxO*Kd9r2>bY zhRfhq@b*Q9V+FNEz>Z`V2z8Dzs#jAdVhdJ8>ydOQ{7Ap-}>@1b>#Zzr3*J`aklTtZJYkwMCaMA}o7>C+hqSJM9 zy(1^HpeqG|@R1@`lxdmnqbU5|Ri{xfsW&Ahk45{l&B4pv0%l70RY5U3qe*kOQ)`1; zp@8kX*kR6wa|?LZ+t12Jf|jp+rsg!i1ggl2+(3FByH4U-HH6UBd9t9!&sQsmo4ags z0zC`y1wV@4kDc`RHJ>2#3@cFfPxi?)ZQdN6xHK2Ij3|`I5SQnznzgwK`+)su_H7-^ zcQ#*3V3HJa$a0^Y^fe;)u<;1LfxQ}1&~_ol=73p?c!gtNUMIl}NCaYTjU7kpG$yQS zO~FZ_c})^ljBZ?-6E34r*Q9QPj(f|CMBX~RBGhCNkZ=u7w~9yBu3d-UwBM!6tYJrh zTZllC4_ZCjq@(7ENTub1SRA4Nco>tsJC;Ao%N4-EpdOgt5SS^tA2tCo)oylR8X{me zHtfODK+l*LY6V@wxyg>8r1Z$w!x%?-#X7QtuvXiq^VIzC+y0w{Z*H0yyUU>>Mf!eI zHgO>gMe#mg2XBV;{5VSD@<$tKVRGJ>q2dG-uY}NZ&u_L*UY=%NVm~)}_1K2Mo9RWB zE6%vC8A;79gzXV=)PVDoPE(4-bk(K z%Zh6j4NFx^QqK)!OxvL62W|>2^u{K8H2tu3YrwM}h{jUPi^JEo3b#801Gi z?#|3IyC=Izr!&2LHFyyk*1{@2cW>orHEnnGvoe0@6b-8{W}TY4?ChpxHmn@3BNSt{ zjXXa&zTdGwIidCV{v)m>(OqigyH9(jiD+XCx$E_>h~@V8axVo3T8! z&p+=j>vHI#F?G-hVo8YZk;a4E58q0T(kNPs81nLav+DVwL}q{|ZRcE%H+613eMd(l z&!~6rnqY^%USl$}hEz=+18LHh5Z#~7`~yiwT;s=^V|PQ!H+&qLLqF2)ESwdp?G&$F zSWq*EZqyHEhY;9Qg`(!G0$0-dOAiM!cS6bz8Dy_Pe+ak=toT<37iJ|74i%k?cd4Vu zTuujt4Q`y??S9`{T8qx!C`I;h7FiRb|$E38pW_-zbOh}qbPrhPs?=v0e7@LIueQcakc%u`|0}bnEb>wiZ zG?Go~csh7B?~H{;C_-X}XwastXY{S;islGW4a^7J3hQO}NyPu#*R8sIyDYC=uZQ^g z1?Qjr)&gZ@GZe#c?1wyJk5Xxm1LUOL^BN*9^ri4`DIrAEzA&#r;CI4HdQY zU?><`(=PRGQ8j?2Z66*)@`oD*19@*NiW!THTd-6vQE^Vu)b8%68SjBjQIW8W!Vo1w z>_kw|$cMp}Z_V}Z@0woZK!!g!d4_fp`QMIN<<{4&nF$^bOJr4rvbGRG(H!l5OWLIR ze1wftt9Ah<^_~$dkfeOKeE=+)0uq*f5v|j7)3c(!T>(Vg7+8WHL@E2RXzDx@I0+-E zt=}bXG1v>EsrzR;KaUG0npQGKNDf8lkU{F~|$L4?d(40b-TJ8zJ zrOJ4SWD2!&@Nl0SD#7=|#~E9Otkn{0Bct71{Bb)DwB-?LnBcg!va+xmO95a9We(|X zyu20*3%U%p=)8blp}p;9sTWcUvgVj|) zr~0L6OU=~yjLo+Sa4H#idS-_l`ECW+5(3#iY_%vX$xKC}lE{n4KaOy`d^!2Sa(qSq zi(Jg$NQWFyfgioVzp24MZD=Y9(NwO@+5#zz&sGO(Rm$F(E!dxyp@?MXuA?FGwpQM) zCg2Qujr7OzIJY1_{36_YnE9j)MOwdK%s}wHf1IA25h_>K)jPi{L9u4V>YJ?!>Kj+B zDquUsNLKq$75DyKXz$twf%2@1>qL%5D|%>dU11x7EyeMS)NN~D(8#_YN3BdAle~%M z4-pQ_7m32Uw$bAx31iFSitYE$7^=CEF;I~EEBXiynKxHpt=FuPz0+MuSqT%{A1EAO z#^F~xIS9TdcB~A{@A#ftZJ47qLX`)l_7SlV&$KXtD$4Nab}_4<=HttC7< zytNn*Birz=UL)V+p<)fIjl{LIeoWawpAX(YEzNG7=4*ZG0hnh;pmsS1^LTz6@lC7{ zh9#@54Bo^%#b`e;Rhz-?iYp^KBN^SCvu{UU*x}ztRm+!-j``rE$0=C4yz&=zNgm6{ zZ_oKzJZeJwywl3flldb>)sxZBC3Sm*3Z8bZ8IOl`tRigk(PFaoB=&5;+UcJ4G^s(> z|4_3^^Hj=pAIABe$;`y1N7~aTEk;#!B49>g_2M}%!IwcOpgq-t?=oL412?v}ZfHv- zt89IRsz7o@3gCP^=@~4!9Fv$uDdK3YZUPsnWzyJ0CroVC`wkU~$_IzrCUAIH-?f1S13HQ_Is04RF7qKTyGbttP+J>SG0uCjuB0n*p?--P*zhi;e0!;S?n zrVZQ?p<>(otG&XygU^XJt0JBOqLM=tBOzYTRwYu;`cKFJml2Kg%dGjK0{8A$EUnRa zVfe*s&tE^?4TW^KIqaYhqn^0MCQXX$-=(;bf=-K~cT5c$Xj5MeroO4^n~;(m z^x%w`-Nu@$usQ|*h=G9s-Kg**i$cPpOvP5yuyJn_o;sDeQp3EvLx z&uN6BAs(drI;iQBOsR0-MGx{KpO5ZEOO-33!n}SWOB%!XPPs3e7c+Ta+p7AkxYac$ zq?!h#vWkn9a8zsRkH+U3d0*!Di<7qJmTjeQ*phaJGN3=gLwjxeGf2XUjYb{o%98!4 zCU(kcn9QfwWb(r4zX=m+LXUTy^8tb=X*O z{b;nV_Udnhfh2~au|520QbFrxJQIdIbiG1mTwX5eqtk=dCY)O&>7Ts~$@xJA9wuOi z8y7< zMN6oycARy@M4D1@$=fH~rq04cyAqqW_61e&n$^HrX-8ok4HeBF#+*_fyWPLgu46?g zTRCs{=0~CN->^yiNWW9`!Nm*XJUzdD@v~V7k%sZhh-!jgM(qI$?JLt7B!v#FrWr)R z4ht^t+6>mX7A{>Y0y8#otADgEH%Sb$IydVww+LzaKE7Pp+;5a;;!@X|nc^_-UIj@O zvlOFEd-OXzKw^FDnVa7jmG*fLwAY7`w*-eaX*rK3-%xg09!`59tHIwOya3cj7C@kPEeER=`@tgv}4^l z&sLgA2l@KwWb*}eeFb~T;A4cKKE5623gZufzLiUDS%~-c_WoGeYxi;@2Os9T9|X7yWDZpQ3y{>XOc9-FqR8xLh4UBh;YcVkCbj^27U z*708s2~yV@7R^0BzZ`A_bjE}5#-9T719}v%O$T!vHc;*4@0=6h@?SZwvhVnWJ zB$8&xWK8Ism$x_sIDgPuh|Dyo49GWk8f^mucwLVu6VQ(*p(9afuRgvbFGNsdvWNWG zOxWi$GzCQb(%`xaeeY7!kwcKJy8!8iLD zY8pLTXLhvr>(PzFFjnT(Yn$W=@9^k$S2uSzV^oC?i(~T80;!_?y+88;>-bp{K6K8_ z$`8}yz}n>8g1p;09_}r?1c@IVEQZGgdxS5}XsPFi2jY~91g73M+1m6m(1fkO;Ocfq zcUz20>0l+zIw2N*xXk;Bxu~ROG$#?_MVMx$-o-?^PLLsA+d<)9ujYHX0ZBjku`}lx z?urm5@%{S-kMal+4pycpP?j}uS5=s?Y_~N1DT>KZVmO@I?sxiAyQ@w$FNP7+dJw~Z zu&5p*vJa?Ba=7<4l(r9qbo~v(h9tj2x6|1bMdfPTJ3H=or8KHo#wGEVpT{!v08d2?B1IIRfJ2wv8{=Gp7%3h@LK6)WKJUiCsQAjb( zQFX(60h50R`Hf|6ziX}_AdXFBUJ&#->6l7*C1vDW>k>y6AYZldL-9&(3P=f%W4?3sP{GNRy0iHGfs#ke zAnk|1qo&Sri}l<X+QQNdqk36{$LJEJ)FH%y-#|26pq=_VG?_e)p zC_TtXh^k9lIlbwuw-kQfb0r=F8rwPkDa`z$|3kzOai;cC-|F6LYDr&CuLIYxs8yGB zmUGe~WV{Jg=1t|e%aM3A5AUE3`jtJ6on`@xl;X{XYX)HyFaXE&jE6_X`-ma355s!| zo|mW19EB}aR9Dz1MyW)X^`G&UT~r^oarWv%m)Q9+Z?BG&+No%HXU*=rO*6CkA>`+A zCo!}5@k0W39^Jq@`PUZUsL3~UUyX3X*br`S!8vJZ0g6cv;6yxHLL=byLQ)!uu zUU@YpF!7GB8F`P01AaC!3;%&$EDzM_ZHSkIVeS*}3UY1FFosE=bo&J@J0B8A50lBA zD;;jy39$nljmQAr_Eclv3oPOY$CdmRoFfnD-sK`z-f+hR^U!53n6%#pNB4oTJKpHw z6$l`_lu74F`jg3WTU(qE?Ju(VuKV1k0!$aPi$wjKU7#{#kjiLcbt1%a)=sQ#$;!rq zn9rjHhVw4ZiidayNTuw+6HKdPkCUBeAUjh1i5y!XM0DJULUN2_&1Oo+tuoq=@T+F%F^@%|FZeZ(l`{bKo4?%2E zLo|{SK4(p01wv<^YgEM>I-lgsihGH?4>!`l#v0n3Yr(oD%Oq{Hd^2XL=0w$tkOZECpC4v|TJn}ag#<(PR2h^CldyRHmG?+E$vCe|>3E$T_ z|GwvZggY_uk10JpQk!O{`8y>*yHN*NiS=pS79N(^ZQfK5Hj+{}wLAah=@ZLZ&pg}q!Rtq^ z=~{H-ETYa*)&)D+>Wh~ZuIc=$Jt1J-%7Jsi+Gk3QHk`Vo@;`i4ujXNFnZUU3W#6tx zO~y;?2;(yjmsU`xhoB8$QH@)UE_SDTmlg6`S-5JTjcz9Pr|$dwauX?Zjo+UHV; zy_DcjCKi@)Y8;_p`Ez#H;$1Yv>y#jHh|k&7el>h^4)9JGfKdY(Hn}dPd+9sONzSyX z>=j>98r#J_Jtb|gP&cHpHo0}+DE49Fz@%9>Eq(G=T3^lMJU}v@^q8J>3xe>V;n~vP zrNJ%Er9c?chN3hbl}K<(9rpn~Z<|${5+O!ba@PQ**{0jr7F)COuY#V~-O8_V!bKj^ zIk@SKGI?*Avk2u+&Yhb6&M?ZOlhqe4-4;^!_Nun>qbMc0p;0JiyUIKTsos^aAgt_! zVL(sty5lK$o)PT&(n2)FA-9w zDNcTm*>&$8Diy9M7WN^h9g_;#b9+1omucH`+X0&h92x>wf=6gTvP90d-Ij+C{gw+q zKQy$($|Ar!8)UJEv$}=7L*c*?rU&le5Hf#(?{mE(cQszxF7vdPF zFXu)Yb&qNt+5kCr5Tz-M$b%p^UU5ku1gW0M{5_fn08afM?#R}+4f8*GkBZ?ljaYnH zt8DzxMgxgoW~>$bbPS9&Tta)<)z#?$rWJnK@t%sQfMlO@;S4>z7wix+kM&@0*{s{r ziGA}Pr_Uk(;RTHKw?7A*@0^MSc`*LHR_NvkcBnCdMggpCcvm5*yw?j_=_J>rhLGoP z?W^;4eu`czGwt2eAtSqiM!&j8gGad6sTo3iLQ3;4i4Xm#{3C6;G?f^##mV zW8RMAje^=VvTb?N(7gOZ7wL)694Js+9Nu>r#V_XQzj(H_=HD7<-_v(HdK3#Bqs^*M z7+3#_*6k^tnXBaV!`Kk!5WGLVhb!F0zpvfGa)^ssgakqLN&ac*b$u#I-q%K-rZUt| zVI-%=OW+VN@qXPMc7ri?jV0ZRoC%SzM{w%FC!699;--To_0?w5azrg5C!|q7)o3Lk^1^xy(>QErCBSTgQ%g1Bmk4 zszejxFC%SrW%hZ-xriyG9!4*DLjdzdXnaWp6K9LTQd4dB%+P(tD!ACXH-EK384 z@LMnWR$=t?qCW2FLv`+qCmL;9H>fp0s zxZ#5KwTsdy1`x)|9YOn?evzP7F;WMh)0oZ`>UPddg6>z2c-8g{w83|wKRo#JS8E!G z2MecS)e$7Rl5z6Ps;n$BgvbFM8-~tR`#`}t5dM46{Iu9gk?w)w46nh(^Y3VEk-ncS zaZR=rIu|~}w~@3g09Y(}(aTb`t@-n))lg!Ar1L^cuN)jOKK% zR(sNF1#vzu?`&=Q4?pZ*g3i$3PG??0C9D<`Ur!gJfHpW`umB|ez&FE6Z+4_rC zMji$6IYu9tN8`nuI7Ui%9msJIUF z>nUhfceFe+KZ6U`mJj@PpwYZh7A!{0cSC*3dLOei-B|^x(widQrEGi{xO zwZtJ|M}$Qx@0L9!su?zCK3Iee*Djr7gqbC}!is(-AwN@51)SC2Osxo0r!|EXgQ55v9&WVYqQDKm8@6$gDwmzFaWMQW;H;}}-v`o-6kRZAwlkOkC$whwt zc{g)Hmv(XrB?RSz3=Prhi1!;{zoOnCo$9>0(_?c_|JJF=4?DVjJ!TrL^-A<Z{{LuFAV$$B69Ky+LY~_RWPt6=zbOZ3)8HxzdvIc=6WBqf>G~^hK{kd zLqL5+op5dVbXy!;bqO@9T_PeRg&!3oKTCt5icc_j#+sSI$`S7Shci|!4Ta!W9ADg_Z!V;+)P0s?fe3b8_t;&dSIi>m}OyRF8wg3Sa zmKXirU;cp`Y%;J?ITP*-IAiegs}iS=R4^Pj_nicwAgF)BH~j z(p2&D!Byy(97lV80wH6TX7e_^mL%vMe)FHABKbN%!?z2SI%Q}ALL#Q8Kh7)Bj|_Wg zC35S5&Ij=~bbP)+)!|#yv+7e5fXC-0AE>chIkfWB!@F$>o>McH>TknZbZ_z2oQr=YE_PPo-;#u1oB2X zkyE77Zz9a7ymTK$6Z*?S7 zY4Y+f_pavr1{;i?SR~t9Inj&2yA2(^H3eB43sb#MDZU`MyR0JMZU2!)@#TGyM()16 z^cwdC@OVL-@Ld54(B4_>sh2nYH_qXj07gzIHCO*4qK~kUAUQnk7m2_BQ~Q>YwIE?$ zRvgB{smY!a;QOlNW=Sxy?Q=gjh-)jCC)kiKq{N3?=dQiETK!L=-C zN05c7wN1CNOlA*h>uQ(DeehUyP~Nm*_dW^q#>r-ta#eU}e?|2!;xJ{FxD~+LL6mJ7 zIy_Z-zcU2hdLY)FTsymU|9(%Fsqg%7`&O~KyCw^!CJfWcf(*qF%%N8_ zI$RE=#Qp4>GQ4j)uLXcqaAS2fQcr>&n7%X5fmH9VGXc5ro1FMXq-h1%j?VjD=sIl7 zbE&1=yD98`KxQMt9*PSo@BN$mEvM)H>aLgtmS={#HHELdGa+HBL1US_ITcljhC?3h zHuPCkJZ{KE5oTq$g+u!DVdJB3J<wwq#_;S=DbfQG-Y(%=>}W%2Ud5-2|n881i`-F?`QN$ z$`AvNJEvZj6)$s(BzTl=M#C2}UrH5HrIdEA4jN)I<`_tpg>2owjja4EZSN}jZhCSq ze0Oue`5^I|MGABEfV_EYtX9tAHvy>8Z*ZT$I=*2pRG&k=gXmMeB-r5kA^Udst(4{Q z`^WkDYo~j9fr7dH1GFh(Uktli|K8uCwrNdfQk9b18j56D-NU~1%ypvl%yuVnmhSjU zOlYf5EJ+lB$7L(+ip5keX)`U+suMa!w8mIL*t%NQ<;miAX^a#p;8C|#MXFwEtlS-2$CGyl#3KTLB3dTy2&MY{QJS(pznYx{I1+-cXLtM`hLY;TYKJC^mgFA6 zf3zDzsMLrCPWTCi9s!KcIqAe_SzG5727k2FO564YbV?M)$CMw=mido|e>DLyp>0N# z-WQAZr6WswCp1a?LOQEPKK5KW&#S%WrM@r-Xv77|%fNxZ*fP8iP=p>eC_#?_StB>S z*K+`%i=K|57(;LGF-A&x)pSjj;k{+XQp}i%CBIR<8vUN#F&gO<^VtWZOU z-%tT=MufmE4mbwoYRU0|p%2j@64{S$drHH53udxc0eJpG9=vl2%h3!kb&YiuK+s*{ z{VsGhWVD@>R6!VcL|oH-vF_U zX3k8_tv&1Iq8grC4lpkP(3qxuM`31MleikLgaoQF=h1e zhaYsU ztyEZY9j>=911q~1-g}3Ts0b)k zdMDDG^j@R10HH|lASHB&gc=CRm*;u@dw=`jJ=#a_o8NtsJCk+K%)RDXYt~$|rl+w? zW31NqKyS(K?M4tu9DUvO(tYDcJ6tp7g(VFvrCgrAf2y0gdYgLCBj)(_VMUx+Ro@)~p2`q&0woAeehG^hAzHByJH*JYa`)NUiPnhCun5iY$ZSo{0|7$C){SXc?FQB4R5&Q^ zqzwtCchS+qKe~QIEN;mwyEvsLbnEqu#Ju6P_D*$MV0O5%to8SrCR3OQI zvn|u}=5?39sU`N=mrH;b*-oG7vscP zi|Z^?M*~Ci3Iz(&+vBJo2(zMTI#PjGS16v3_Lon zt{smCbapR%m%l(6HGR~&5i#&6*u6(KB;w3Kwuod$j1EeO32pr9{G&H|U~l8y@PMbN zsqSsT(bw$uffe`9^&P#wKH*G0dIv+#Mq0_;L#EACBi)|PegHv_=X89&4-2Oo(Kjec zkG=_T%%0EElx~P{q^+*&o>&4}RO?I%40F=0?HmSpdoOHY>{oCX^f=`W8NkiZ9mTZ$ zo!pz}AF<`pFvSDs^CKi8wuGY0Aj+EuV(C^Jv1YbOySli9~OS4D0De$}&7PNwFBrKgc8%7o} zG}KVWP5So?o{8fHtc-8}`J z2N(w7Vmt9+ShF3j(dvt91k~c1o`T?55utSUGGFuCGxnBV9Se*~u4KE6NP}kyrJ=dW z#vJO-Ss6_+$??rP$D4T)@J&(mpTk&g-89jbxIBAhUbN4G1d@30(F;M%$U@W0rrY9 zBw+*(EwDHSo2rSmrKI?Ja?&mQslz9yud(sky{lO9RpX0yhxy+Xw>4=sjxYPZs_w6I z=si&G%LcDU1NXbXnJdO~Jv&=qe7$8xyl_FJA&C`CGk$qK%nqBG5pD91>5GuRmTbOjT)_>&XV-7YOynf_^ezt$V3D1bDo|- zkK?p`>08JaJlmagzhp$6qu&|73D_;Rz(p~g&p7+~${^6WlS)(haB@0;o(Apj zv(21t6pE{=H;#qFh`xB4jZgkY$JhjEpaUbbE|J?%fjN6fFn z)wz$zw=2C4cJ8Ef&#dRF2Orj-%t{`g+KJaH`Mlx*3y8tU;>}oIC-cropPm97p{A@! z2lb{nx26m0hKNply1u;<)EEIrxU-u#H*H+?Jm~5~(knPDuBzVCH`d05)i8I)cLuWd zg$ERN==!TGsD10kN)U%p+VH$NI~NID_l@pNw?0(!SPyi=p(4isLupV@>heHwYhp8Q z9L>IaZj>8*s+CI`acUwNK2!QDl^i}_-!4;1q#R8u(*(Y!f>%#ou5 zjJag@IO$#wbs91)03%qROQ;3P-#F&t_C0Fp9i3HN=uF%!DJ=fH673<22gUkGJn`o% zrjw|9pXKU(v~RI_yxlW?rST4@eBO97Ea0k06L2=Yq_4wL*EiRiPpo=gW%}$(FfZ60?Es~_oMLCV>tP|mX5Usk`MN3g$E1I518~gSoq79V z+MCXB*za)^EW5KoF7e0{JAdwoy5VWpHFGkO=z|H&1rMcmWsvNa8rC0mzVG1+ZB=Ku0gUf1SwKs4{Cw- zZ`$dlsl1qz1^`2i9UOS>7e#pmv4Pegr(3!lDH1;m5b*dA&~O(TY0#B*3!T4im7ga9 z?CQ8+fmz`6-SE+n z25_f!9@s&U@twKNxurG7Q`(XxW4oTtF(w_nB8s(%(m@k_i49>fpeIb%sQs$`kYox>~8Nu_+Vl$Elf2Y(jc9kplmbncuAsUmy2*IbG z$w}-@`lkRRC3+g}@u0U%+Ex&48)D{YD7Sm5pOz)4L0|NNWv*21w{YSEa&yv3_O%D3N-M9Z=S>V0_+Tkg#x|%-qfJ99AdbH^#Ba{L?@?I6! zLQ*O_-+bkDvbk^QQh1pog`wg#E#BHn_a>V7{j>xEjGG}}x41bba;cV5VmjVAKo{P^ z+Z2al&)AsXC=v9+{4z&x+C~o`ckOLn(owDkQK>8|Ia5zNMy<=z1B*ORgU8jvRwqEqRtlb2tQAn!e~h2@BBq@`nL(mJFGmMJA6yFDR;~P zrvn5f_c<$692US)509(Na?>1Yr5EB17;xf4)%a`z@b5X2LqH*$>cgaze_kuk3#i_8 z$$-aqOc5QJs5pLU@EE~jUlT?dR{V3wt+`gbXg=4nc{UXr*fJmwlG-fD0Zl z&!Dmkd(P|e;h%xJ((_dhP~M*hEHY4o{@!QDtT?lrr8i|nm63`cT&N(`e^^o81C^X2 zp2cZm{_Fnk06)r-qn7AJ$5m5j`gvNVct_|)FGniS;#5#o>f9;ShFp>#(GiF*V2)VV+-7aB*FRKmP zBwIIYm*k00h-un*BOSC_KnE&C^^}4`KNN7BhuZnm8T}{;mMl8bOz66Z#mei*F?( zqPwv`Ih<=7jYN_K!U$Z8Z*ZZ=RR-mk_O>V??DDEoC@V zxql1e8ifSqMyH9)&ClE3tl#I(4T(F+!Wu3mTwY`T>GZp+nKR%OKfngDscEInP&L_c=%=tm!+lz=2N0KibQ*k6evM^&hM?UR>%6*{Z|y3Q zN}wdVRic4ChRBgqe#I;n>=uiX&4;q6hh?ytaIQPIDn<{M1chLC?%O_0Xoj7ur!{2v z_sfePN&1{UDlU8DGBHrliBJCwpe^ZMN{}yCiayy7>3oxH=3wv7#!#u$BfOZ!mH#A` zV`)pWzEg4l{_M!DZk`)Ns7o?Qcp_*@Sad5VAtL9YQ!Ia@Z?;o2UFN!nsij1^{c#`} zeDSyUtew06qgn0u3NY!)k;*)AlF`nV>3X@OtAjf_LbQ`?Lpk>vnuK@Wb_OPC8payg zmpF^+Ki6QD4$0!1PI1ciY@n}hI zKXc-GpjL|0TYUF?b~i2mn@1Aa!~;C;Zf}=LP$(d0B-H+DGjVN3QL6Y<6)MK(oVj6E zvQt7cEW?~)Ru?SW7m?&_&4Wv(IZ;j&!Z``Si>3?DAyp6F*d!FxlP8`BaU)5{^ZCd2 ztcjWC)-VbVkIi!&{PGlL0N^G4{zZ`ze()!>@z6>i$$2-C-s1b3KJ=NREKXN<%OImV zW5+TQPMagM%3xLM;Zahs(a&tw@oUa!THNlVx}ebRQQfo6GdU{9GIw~-EH2hZcv1zf+8V_zCDe_fF*2sJ}qrTQk4>aO{3Ut&qsKEusoP zj#`O_$S7mB_$wxb*G7YcC1^_d18hH(mnn77H#N008?>bAnEW09l4H2I-M4tE%o8Z< z7PGSP@XC+3W8=5{Q(i0P9X_&e>AC8s2yuf@5zhz`Mo;Pz#jd(-pkIDzD%EwK%1(}h ztlnjBSLpfhiCsr8X<&aHeOt-iNY*J?@eKD0Gh$`liKxo74s&s7*YjIftIVE!5g=Ri zE7Mc!uF{B*r6dzWVNNzU!vvN2+z9FY90Trj|6nDuv3ApOYx$F$B#+_j-4DE! zc~@+@r$eH=O}Q6GiP)}SuU9#l;`-bnY{tVpL}c2mT=#qVK=y!S!laf$8R6b*Op^PCqW zi||Z)kW)l?*}IS2W;`$cvf(wOYgQPE+I_%BbHDR)p-_Kk{6fy!&Dw;7oD#^xe&A-x z@J%aLKDC&xZlGZC4CPaK}YgbtAKdU!~4SviIeOlv6*BnvM zeyN2mjIdtP^e1LovJSP0)L(>8Hq&r&j%W!9IsG0#j`CHdovqYqWNeekj0aG))T<0n z)4dDD;FYA;u|%Q3`D29dfcb%PSk%La{lv!E`MHInY2pVrl}0jloyV^pRD3k4T(dRn z5N$^87Tw=VW#R0v;2lp-m08$*YNiK{96ES(BE->*h*NxQ&ju?pt|;inNM^IBgwy+l z+(2szeqOdhq^p46zl#f;5Drhd`Yu@o*o5hO=D-+oWHP^6T9*-lt792H-TR=JZu@(* zM$n{(${B3eI_FNVcpa!EYzw8fYFGIrd(mIus`*^c!nSqSnnn5$|2Ssbh%fSQSwBag zzx&cgk9oT`MI%TfWGY9d_+*!`VfFW1Z$_>S$e(8R-NQwb?;d_p`(_~Nv!K*0y_nvD z#p8z`(}!#L9{&0GOwB9wOJ|qa#U#&_(t+QxPHcC7rCyDus1PlN+YF$PMbfmf&Yt94 zAMxnKOJZ8MX+zVGUmSF=R0f1smfFwl^c}qNMif;heZAJs<=Up(jM_cj{^czS?7LV2mf&c;0pdGXh~6rR+t78?#j#nfSp z^h^HdU&yB!LgnNHOb1Wx-s=>s85*)%JA{U*KYk+rGHoc`(cc)!QO`VAe$O!^v?g*ymdhxAu)0%K~voSuB>ZsC4ClNPjl^n{pcW;VH zxH@ok9VceWXM6ESlCeG$V5K4_E-#^PPm%7;-;v|zFCK(ugobH^JNNw@7Qe?gRpQ_^ z@_F=%B8PW)^1%O3K{Cb<`=FkX*p+^K#k2&0IHU@{4Vq<=B|deuzqgp>y8)~jfLl~S zWySPp;$n+r{bxjWPfrg5{*-C^(9<#X651rPG4gW=(#g-QSdT@2UN5AIqbnz-B)<-n ziRvztmQb8lJr3ZmUp_wxj+6$?DoW^?&h~v4f4lnv%Phs-9Osr5Diya|5O5$mhZQ#A z^7zG_AwTWM`{nrKv*z(acNfgEpRu`yhgq8pQQJ<4%`PjI`(0SXch6jM;n>{=3aRg% zWS3s5>X{?UULOn>Og|hyBJ!Cec^l&DdaxcDc7E7Tky0M*;?l96@2t0{%SlG>T01=m z8|siOxS4bw7FZpaTwdCf%HwXtLrs6BK@BJ2vYCTVz%OZYk^U+@FVpDn8%ejysYKo_ zxE@_eUEHm493Isc62RoBxHgA9xrS%ckk9`7-1heM?@@jcJGm))9~(+Q)MrM>H`;H@ zG;w7ESLb)^i)8xvfBh6T$ysWpIr3mXs%fpA)PP-B4?CwsxjhcPogp#eyUchJFz@W= z6T;!XWnEca1#K4?Rui+uQqV5acbiMx|EVy`e8??tQT%wwr|y>A1tL1<=WICL4o zW?mlVsQI5uCVo#|IimvSe`1UJQKnU^jj5y;*Ls^=7=-2|kXW{HX*Y{ON}GQxouYHS zE6MYz>&Ce|P7$;yU9)H&bWXe7VVzCeK-wDWdskZkT2?lT*L~GzB^!q#zOF5k!=qyY z2%17r^kHQVW=(w;b9G8oMLlV>T1bC>P#?{J@UXh5;tBI|W9!0?C^l=TO0Mttn5z6l z+ps_D@S)(X<}FJJlFLQcL*JAA0!l00in&zA{bmDXaO#>{)g+4WI$Digj{wG}w2>oIsjJ)uxd=iE_a@~kC^fysk3faEzo?_D z6FY6qL+1{DCl_Bu`9UX7-{n`S*w;_JyEW3}Q85Fg<4w^UExLPt&Q&t0Z$l-JIYpL)a$Mn2ebBikL;Z13P_8qqj%BK$0L%ic}5?O3By8X?j{shg}ET@UYGDO%e^j1ByvcC_1 z_gS1ipA-C3<*u<_&2Wt9u!rb!dF&8lN2jpT>t)WlB3gS#c+mpkEVU5@*#q=gLyz6g znMQ#)+;y3e@M3Q1xkdnxW1Z!8&bi~YwQngPPeXssBkyFly+Z{TVG)rGq4!t-I=iG2KONtf!xiCT#VrJ`A9FgKbzueN>U!^E1Y;NXe#1fNn zRl;~pY6aovck1f51Lqt6CFQfpVZ?1D*JYe!=w#AwPs)iB-7lJr8}JJi34 z$H&Pg(mnm#cbZ8bm0{t7m-MCu%g<5%0x$lybI;g_jaiv%N8>}XoWA)+co!QP8`CUj zaw|Jf6-y*4&^Z)1v1ziuQWI$4_ln}JJh^SXDcMu3?ZGb~0mEPz38Pg6&88V_lk0e6 zVg%#f^`my&?7Ki^Ms{7MTYWUCqnZlR4tvUry(scEr>AnSB^mzU&mZund6}dBrku;! zCz^A3>{sa=`Vo`C9Gx8EkS5i)U;zAvW^iL~|GB^Tr!W2GBegBOW2yxo-G~q_G}qe_ z4Nf_ElgWXAc~4sNg4oUN3tw>f-@3VOsmi1LA&Ts2PTuf`m5WK6M2cWR)+J4zKUnj7{%N@lRp>W;dEzES9n*yf>+h~o0jMAzo zlc>2Tqg6PEh07%RHNl`YY%WyYQZt8VJ4M56GUAKhEs4wyu_W))VT`AnunC-NwUU=p$Q@4aOEbg zoh+@zb{ez8#j%wB3psw*C1)P9w5ZBoTJH_Y<$@+_6`N@{MOyr0jqNaWMfl^yI_rP- zpFfw$uXRTAn6zRR4$f z8CjT~UL1bdqD!B|95Z<8i48j!IC#{Q@Fe8r7jF0n=Aju4<)L9!NcCvJnZ{FMah6l5 zZe1%Xmej-F`s`Hr6zx6!wXwbsAi*`C)H!0~jG?)HrPX;rA-`K8!j+wK-p&XR5xY;d zBl_ZC3{I;K09d@@1Bp(l+gx$&*HS`FJ(7k_>2&l6@2aI1_gcMsX4h&kGpi`O8FKlA zt>;Gr{gN`dNu<2kg_4B}(c({SYOTi`x-RKu8my`VLThM234pH`>$hU>>clB#^ zl{fLh_ER5|y|Y)3=pEjgzwf@V8!I(v@YPX@_)eVYV;uLTq_^T~dT&x}2#}^Np)kn16nn&|e&yIb|B`z*5E~StjT5%@?7XnVPfM-0> z55uSsqK3sXTZ)A?{t^ePLKBxGBUokW@SvXaZ+gzJ2P9yrE28UZgJ&rrx9v47%`jiDR( zOA0KT(b-*V4Y?FU>wNGELORKe)=2Vfdvffpm$W_cm7Q~|ZSPcD)EFd+G?v1>cBYdr z_QUPYQ7K>z-M+Q0wQXPF<4R&qzpOk0vT{1#zuo59o73EMv5u|$#2>?s01O4*z=ipz zKuNBJj!31L9agTHVxgSG{wSu!w@zxABr83u8qk8^Z00eGCa+4MUFLEQ{rUO%PVjTk z*G;c$`O%I2xq`_BtLEvK%0l*yGV;Wm8_b`)qJ&-P>Z9E3nYFrT-q0u%F8uK4a-Dn; zEgMXzWa|{s|6>Sj_MEmuO)jtgldYrJ$D0Mp%5fvvkTIs22kxPnd+%25aVVEqeU|fk zL0H0f%GeIMv&E1YK?n<9)#M#~)gFj*p&AsbNaR%uY9_6oBpnJjhy2==zJZ-Yr{xSx zj#*>Q@zLyd--2h~+0UG-d&{R*PqOmO!Lxm1SR>8Rf@b1XI<>ljvhKKZ8$ce*F`kSV z;CTIHp`2Gp0`6Pz(a0IPFNKZSHPjp96?TV5$@oXwx)TCnam6rcdh;_h~iiHa;p z4^>U1XV}@7e=L*h=)5C>2A4jVMWfvHkvq#(X2d_{5`EG zVK?@OZa-9t%tuREESqZc=+rRXq#tFpPt-m&sRXjAc(5-(t;LEHS~g~|zp7nWIK3ow%AlFcHS zPPir`rfR9m;JG^w&ev&zzEn6BEi>=?E7`7aa3|3{!em0RY#GH`G^m+#t_1bWO(GhO z`l4*pUGtdP3d=m~%cm<#cXsb__!FlG`b7>La=VLqu1rj^jKO@~H=8FKiqy`KsgmV~ zDDt;^eoIfxVqgcbzFuw#YXx%Hgss;*Q0&BYyi^7RP((lZT24InT6|=+Q`0a7OD~_> z#a}AAm7cQ&+chMfuxx>*6v?rLYs-1(Jbmy)DV6+zc4EPo8_z;$q}E$;{Vl6te?jMW zcO@psuKIZm*Z{YvYiEla+fBaHY31<=sa3A7%z9w*Jiy-~Dm=>bJC@(f9Pi20hShQPFEbRqSVmpme9Z z`_61w!GhV62|WL7pIu5YOm)05^mgOi7k&+zS~ymX6zR4xRf>OT*SQ0y(d-z%qset3zbT#r?QKw!4u`6hr3 z+=@j{I?mG7J+ZT1pl~m$Nf7-(7$Pj&#Tb*aT|tDe%}ccb-MmP_(QQfgcTZ#O$j_Rh zgoVx68`+x@0Q9K!^*y1Npj-yDx2Am_%$MPM0B>%41?f};kYsUqTm7b3*MdRcE-HAM z+t=R8h?{)zlVEo|Dn*s0xcXKlUnPcBdEzfc*%=qPb}ATNRlGfyA62Eq^BD>iJL?^EOLM{d{peJ+ z*IzJOX@dh4u0s+(I@uVZl?mZ%8=cQ$+&e)L`?~YjulnVwLbmboYzyHm~H@7GIRpMLbN@NPlX1Lbh#zCLD)Vga@z z$BE>Jv!t}JO3g9~>AUasipEQm0vgwi5mV{q+i+e5rz*mp%E{7c&*4v{A_^&R#cCWq z37+lMSB6j1LkQ>O=0#+kaI&Zi4)OA;lJ)TNCJ#}5j6bn+jhOs=H=c?kU`F+Bk-qKu%_1@zSr4?H-Wt8S+eU)5w&_Cq%MXq4D8CqOi; zb`aV9=m|Mx#5TEGUJ~%}jFc3~_Nqe%)<)Pm#kEi*txYji;z%k>leyO`Q^Tu<*w-pc zb4o(#mvd>X+Akx?yja#=WOT%-VU;#%&L*W=y`v2igL+)V}Us4=r4^Q z>}aS`A1X;#2WB(6(@YQxs0uI;65XxiWI=VVzxMn2XHG|51K&?dCH)}rk$3r2l|;H@ zO~h&x1c3?t4JgW?2chHLf-KWg9zAa>OdUd(@%e|zE1u*L-dqe7>EfiqO9OBxA8$HK zMS3=L$6*s}kUNDk_3`v*DGBX#eXNVcQ)Ee6#Y^x*;O1=iz7Y27>OK4N(t4|Bvzg2m zG$U;NbUkl&KMfF$>$@i@B?E5LH?Kb2(BipSxfHU>hP$zX8Nr4-oG;^4nuJU#dVXkb z*bOG8`_4t^Vb@rA_>^aN9{O?da<;z<5yY8?5Kqh@gY^qXzEy_A6z358>g@!-HRw66 z-n4n+?$GJQ_2n;x*qRrw;?AQ19|XDjlYx@D#R*W;tiHFPG|QGz@mK1P+7B%NEOV9o zC-H)I5516gtkTi33-1g;^y5xJaW`ihc&-Ybhw@l!lOb7@S3f+n;N4wZA2?PyFo;PQ z_2@@$}3vmc#`I$6|7HaTbT_BM;hwX2+6K(=Y zeBX$M48ib_J_%}iA#!(JmqEDa1ZaMMci2bJd>O8FuWJ(whGxKMr;i8y9_Z6rP8FpS zr>43=mc@}KYKjL&Sd^vFx%aC39b5IYJRMGM{Y(w88m!zfmqlfeWe{N)r-k0LH*phbds6D;~?=mwQcQx9L+hz9aZ@elaC8i?ZT{D`hO+b`Ee|6Tl;#69X9YF@!r2UQbRj9(Af@r0mQr)}2iz(I!= zPo>gM?*6h>LfTcJ&~^BhT1<3WeQOxHb}|M^drbuK)JrLQrIQHZc|Lt*T~(WD3}zUg zvuoh(vt!SBcvmxnDlVNf?WPjSGt&pAuT{A$CsfhIsN6R7vO z=d9=&&umJW2K>t|y0`WoTiQ{|_Fqh*x7p7Bu#WD@h`w0=Khu%^2W6?ZjN)H0pYD23 zicTBvTl0DR=kG1GY(>9u1_VW8z7{W65gHHfML55}I$* z3kBLt(bsd{*piVWGFi#h0AuJnFvMU@;@ zg_jhI$8vryukZK&sq-f}MHjV1mU}XR-&m)l+?D^Ef#v?ny?mG9zj#_4jOva5X$Re7 zukwHYi|@wI|34k;f8B}yYtdRB=&pc0@82s&`eL7XiH2mlxztiZKs$;-CsAtq!{Dt+ zaL|)#;85(hrlG}^WCk(jM<>9AAJ> z669K5w?GI0SatQ}Fw$P5=RvDslDZ`SsLGmMAJ>{L^lV#t=z3L?EBs3GgcaK3AqlK~ z1`7&wvZHb^ z1AyBGUXp2;_f2js?V15G@t3>tz}uy$Djd3}<9akYa`tdW|Br&VWVF88 z(uJUhEdJ~Q_T4OgXPCWbS9CYwfINb&bgy3KF>Ku6HX+B?fD82_Hn_*FMkJ62dZGas zxLNJ&*RC7P@Q@lLvjD&|4nG%3AQ4i~Q!5Y-xx5Hg25tngRzsIrq;IGonEoWm(`(4K zq_CoTM-_10&o%|>HG9Vu9E&?Cr`uDyJn&EQ{(ZJBba6t+aAmY|lUeuX?~(DzfW7&X zPAHZK(8r3sh`j8Df@!veJl5kfP}FkS*EXQT06-sEn$<4x#YqM9#(V{Mx6|NyY7Etm zo9v`RFhbDV(1BHKtHt&Q9D?-n4^nF2E)*4wIV8nCfrt^Pte%*IE*w_0<3MtyHzOim zCA2+iaD`6e!L>`e60t1N(ej^ptL$O2jMfWLS;0*S1g@`Rok&h!i>m~z_M1m?7f9hB!ZsR&7T3+j41%u|0KliAW!fIc1U23H@FTO zn@<2s2DwS0oqxnm=9~_Yp(051allC~?%X^sRb$QQ`BfS&{Dkw58SboosC!@eu!{lF zPVpPF&9FWxKPT19WjVQyF;Hf;5Gh}}*c19Q`lr_c&i^mEn8cg(mVvmy7jDfq0lAxb z@xXH${h&YSVMXadGWslum2+ux3kn7n2kyEjcXY*$r98Ij0lKapA(NZa%%h=EtfL=8 z58U0eO>QW#M~fb?^V}`m_2%#UW3$zqZ(0V!eVAA+0M3A6vx>(MwDD>!<-dxX(khg_ z7Xp4CvSoSW0@?Dp1xzGZxzI6amF>Zga*izCfb3@TF-rJoXd(gVs^oGJ$>9dZkw)>S z9gEc;hM5!?QJArs<1UUiMg;gAu&9^McULny!V3uBhln7tm(WcxhX?Me05kd$`p4*c ztzFCtmK}7Z0cZSAs}oJS%c_=}-o#QkFod>owy}?bZ-V@de)rOPOg;OW^?}8s6M2If zihTb2C#lqrFor8G&{hb==dl|~Y3#2*{GTik032*>S}&q`4ApyI^eW;S-Dd&3*31q& zjzfyZ(F4XdI>na%1?Of_{3y7I2&ewmcSQ+Ltk{gEZFoN)PTOj`L|8i{RiQVxyl!GQ z{-Ml!wfcqviOcfX%uRC17H#eSl0-N)x9Nl1%9|iC2X+Or-{DvQID>09+!FY%76pugGSX>)dztxMYalea(Pa4p`_dVL(JkpJERf{!Ah{ zq*4EpeLX8gr{g`2?x>}9wR5&AcZDRd)YPM=>o$wlrfx8MHvu})5X6`?*48N4v3Ge! zsAH;?!DCyLjWF$)&SuXvt9 z6tTZagD&^8JU1IVDw~y+gRT>hHh~Y31xTz3<_BNa*%&wmg8LOF`6rJc=(im1Q@k`L zoDwb6f7f$etz#o{Wi&v|bVmMP#AC$2>ycULz`R>JIxgT(mCKE|^U@I9#!Rxr{4y$cnvYD~TeO)QbMJXyD# zPF|1*8oQ{rLQ41J&c>=&lWr;$ioyRlN1E$1p5;M_;Ij;eFD?NmOaMO*C*vO}rKl!I zFu}!?buM}sX0b441!;p2;C}wd!ye);UC}XFUz<;^$b?OS_Kpv`_J>{K>VJ1%6;+>w z#jSuns(PT8m(p;u=iKLm0puLTl79~XJLx}6&7?|M9cNMubcsmljR-5^mGa+e7FgsN zEjs;M3WK~nolnmY!Ej;MG?KR$)$@n4!0Ty2NZQQav77r0A(xLK4dxtDkqd6ey{ykW z&%FH zq5M}ct2nL|;va_{`)SXA-v9zHz5;QFJ27e4_#H5*xHA!4I?pO^b`13nbdVl~ViTgE(1mZ0_59qqH zW23O|Y6G&_3_=w7mV#p}aA?{?O$Lf;MEgv+=dlxZlGwZSHr?cH>wLHO>=)1@QUAb9 zp3ZwDXYXkOzR3MU*Y758Y0P%Mm5WG-$FPI9!@#{v@en(h@$cTj_Mj6|1cSIyopE*$ zMgu)pjkyEBB>+Nq@$q6ne>D!%-coQrAvT5IR3(7;QIS6+1pqNrL&DBHunwbXeX|L$ zF;iyyln^i$WRBZo*cd5sU3Y-?Ga<-4*a(+ilKkto8vLHg#CpMb$J%k@9w$|e6dIUu z0Pvwcx)hI!q3PEe^L9TQYj3C+LVlB?hlxWcz!Dd@1%ei&E;0!vtr5C@7jq6tQ*W2) zKs$i&-QO%a^$+@*QhSwGQ{rF!i~(R|yUV+Pe($;3O7LoXi%mHlD5YTx1(Cin8eP?l zY*S1rSjG8c`@gUp7r_EA$8x*8c$C#Gw;HSM)&^Po^8WD@tt|fM?1FIgzo!~mg#WjA r-T!lw#{YR%^uKoK|KAji#@)W-TIYDB{DSiq{-Y$X{UG-M)VC@3hj_p*}8P*5;WP*Bhx5aGd3%D;uKfiKWb$}-|m zp!@9#O`xC#jNeO&sY3J*7TpX; zrW(0V%f{nfgJyR zN$Ez0{DAbIpC-e+J9+!^Hl+!2R!8Q`i@+EnZ5?;yt}jI=-gyx+W!>Gk(m>@JGA0@mHmSgHrvGxEeX5k84I9huO{A;Q1(>m-kbG z25$HDm4` z&`=&to+l$Z6}e4Ka!D?KbQ&z^5-K!%TSrFz^jz7HpPns5m}lJ6`uj^jLDAMAHM`mN z$1@}b&G~*J32{T_&y=@8!DH^y3r`TEP0TK?hM5<>gJLQh?y@$)&c^l;r)~H#>719B zFF_SAN`p5znbnp z@>WGh6Y>ggsg}Sohy=Bpg7qB%!%XRG@X$kf=nra^T>1~w*C!>*EnOc+z1QE!=GQ6P zi66aUF1L-5rCG2FHpIli85*3BDN+^7jyd0IEYw}W$LvUzK_pC${XaKvTW*VqvPNfT zXID7I+~RKHm^|s{qq@P_;Cb?a%$XnK)za!)WA<8S*I(V%+)fo*2~tv0d@PEY85y=a zmk-4V>5J)wMLCuKhm}e4E$)|>mtfsDhN5pL!nF^dtqXfR z{Tn`h;1kG4Q=bVMTZ9^Amnkb5r2^ zdQ`5XH*QVtC`Vm?)T}yLQCZQ^SdLXE8)n>J7daeDI*j&xl>M06N$R%lZ9^FO`-BuJ zxtP`_?D^LYL7|>5G?aP?9NMLeZpySoNKz7S^Fm-u6)oO}1{49xjp-OU+9wAd!ZvD3 zm8m?`hkRu^?ALLWdqPz^m+%V>UrE)cuTI3NVkx+p#ZH+2C)&&36W$=e!$}nE+fB~@ zU5H3aW7MC@0EheL?$)W|e=^1&TGN%xA3tD%l;JL}N|jWdVb$Vs3oA-i&kCn3X=vZf zw}$^WjTBk^g?pl}C8EsEUf3@$XWlR|_cPVfZPxBkyEY_%$HMwFSs?PCEJPVsrI9)L zi?G47TiTW)el3R>vR&pAV5$Y#5j@>c85LhYLWg1{jGS#dq{i9UMowUNEHAfB;=8k-2QBX?N{9|DC4n`XiY}xzZW+)mVxz8eO{(fR%KhxZPY%xMDQXJ_oII;2F!_!z;4F=^j} zwQ9CI2j|1*;iYq-xwyD^9S&H_KR5qroym}=?LTNdk4;PKy3YV_qFtgjKu$@j*;zV` zst}j=eyyxv=8dLBj-A+p=yS~MtY(aWr~Wsu6PuVCj$feQsRo}ulqQ5=xcbT7!C|$necVD+XMeWT7z8aL zZe()JR)+`$lw~bSj}hBgvk;mzY)Wd1QcPm_`L<$iL9R$_GFT1;U^k#iS9;s z^%C6vXwtq|2?U6{%-@nx`+1vp-rJcsfs#9df|PNC4d;#H29h%70?2l{{kJs*zvk^@`fk<6_OZ~v$bVRbb&=8uVi7T)m3 zNXYYIJpxJM)!m3ojoGM}ffB1KJ=gJ#t+lnN!j-9Ri~ZM*%MLFG1vH&!$hiGhLERBS076l?59c}6rvEa zOIL)5iY#ro>aru~92yoLqG{4xSyx|6x&9Zf^Lu|(_vPRFxk??DwzembCj3^$SaI-| z2wV#=kkDi#C26jk%In~7-5XENF*>IEGsN$P=IkddAoHS(2_1`zr0*wGRu0z#h-WHJ z6-YSDM|&lr1%s1j8=#j}z%L9Bqp?*vghqu)fp}O9t30}rQ5ikd)5_&!e<+9pzn)iY5bM5C1qvbDNpRbd@^qzJr^@-3 z9waRZ=HqtXtZT@QPCt0UAbDEwG+1_o-5~wcY!7vJM+^HAa+r5d45BzT`lC$WoypQ* zH#9X-3Q}QLIUo%T4hN50Y%p2n-B*Jk@7Bo_wXop5ZZ1`4P)O(Hr`SR8{dJK%W?>B{ z1)^dG`RtR12E!0MYRw`%HC}kt){$rBEjSNcOho0axzGPHN8ey0f%6Cx;LqmmLH`Z8 z;ymHYGpURb%}iQ0Zw*@CzChH>6aTDTip30JuwkXaDwW{H!0VS~iR??q$H;P_9y$ryFC;qF;HA_@k%{gba=>E`x!^vkO0mGnmL&vaZ(e|0cXR=YgCJl~G| zGe15ZT%v&jaWDVOuo78Lz{Z5E?e?_OHsa{w(ycP;MG7jXYw@2LRr=oVn+#QuT}Q+3 zDg?oud$s#jo2&gR6sWceDar4D8qlrT!cXd znUC-f4>H1&p0XAMWZnnwZ|?5TT_Kd<-OQN45OVK`d;Rc4=Khr0{d6Lug;5bG3_I_? zyN^xMPKqj5y!_g9WztDZ>*BZ<_p~hA}+tV361Oq=b?s za9wQw$j`j(3C3(RV}H{9`^nA7|f>;^D$!kS_%w&=CtnBfP07aJ1&9?kA|j3hkPtQXfezW zJ@&h#=1>Yhtf4x-n=-Kabn3MjbXwNtROgT+Co->{WLLx_J768!9LD_K1oa87n{~`$ zA$!V0w9L^@*4pzJ5`R-NX=&|l!5}W)4Z)x33--_z^dSpDKXSVvg%&MRrR1e~)z~-} zknk7zgORp&O6+%TSdd5=@ld{nhudEah>?gmBD%QLVutmCD>u6Iv7&MwXu51VqeL4MzN|iBOB9my1mY--X5Wx1iDNt8Cu!5tSF&S-t@)2B~Sb<^s$ZDUIL z@1hWAvU9PU#tTaeMP1ctne})>KH!gZT5wX$9-V#_`^xO%G@mP<#JvUceAM31uU)1O|V)JV8j>$~iw}1Q>9va%^ol3r;2N|a2_!f@hur>5P zWSjkhOi^EdNvT-1;cMjQ0jfOE=}PF3%h=1lJI!5cr4_Ivu^KK%@Xq|ezOKZYvGl3egx`RJ=QH;B^5cl|+c#fa`3CZmoT zu`yRj?*Mz5Lm`4sn~SAg7FGWh+_abXV~qD3tD%xdL?6Y~$bHQg6GCTxMMHIi&~-s3 zZ(8+=M&jxvZw%Md)!%Xb_74M;KN1tWtl7`&M7miK5mEg+3uUC-8yB%}A08jvYk7xg zIp|C1W1QZg6ey7OgI>~p{jPS2Kxt6T$D9B@BV%c2??}=me!ao%@2e-U2n_1wL8;QP zP^_uLyq~!TOre|kp=H0E=)U>ow0z078RM*s; z9QiCus+++V(=&{XkNdp@L9@0mM}cD3A8kw=5||>@K9svv$(78c9})2kGxo$7P?lC! zC*$|WYdf(4@95~n!D0EQt@BA)Hx!(^?7Rj<(0wbti??@h+4TP!p|Uc{ipsrEgEetr z+r}>-a5^`lc+#7p_l}O4UJ>ai?`1myHSsPamIgQ5qudI$^Q0w-Iy-la z0WCo%m<_^w!CczZlwEiN&o6+WKX%Wk(}ulUvt@7mzLGKg`x;ft`4us*=N3+V>s_1q zSAF!63_iFjNtkC}s3QZ{tykMSCVM-QlQIqHq zTF`4ok$LmrAmchZm7gTgEo2x#iHP{T`yuzMwr;KadZ}|&;&XOO>5uvfGZS4=dwax% z#rsTn`XBVpuCDe|q@?3bo(2;MA&u8njNyO2IksG8&e%U4>_Tr>JDDp=WUF?GP++2V z&qlpvVNumk&&ki%YJgbbneBZy| zQg__ExxKk^@q4|ns7PGY0P~OBH-yCynRTx+!5$7}?sh|!_`vNYm)dQ8I0k94lK&xkVUJ=1aPaZZ>Qq3{=U(s> z!I7JfBS zVtFDUu0MM1(QUaaKYaU>Dg-tX?Q(%FCNVYgp~YHKx>q#6TBr&$OcE5NZg48V(6yTL zR#7eUB=jA`{W`Iswze+oBzS%0bZ^Q_U(eC!87eUNlIMa*K~fUOX#F)r?78T*OqJ8T zOaOPGu@+u`6iHu(o`50tqzdUv?R7grqMgt-3?1dMg~TrMCOeCPS|_WggY1KU&hZ%qvKOkzlT+drVufr zS2k<#v9N-YowiP4rVkjB!N|?i(6<<+$%scX+m7#feXw>&-Sz~XRBu4Za-42kUE19{ zm~bxVFE34fb?`3?_Wl~(Jc^lg6xvcDr|B+^T@$T@wZk-om;XycVJE#3OKOgv8;l0Ag?td=CH`}At z*j^_`^WAlHL@ZcXu`S|hrKLlZ^C=+oK(MWo``k|?A@Iiym(6|-qW$MWOT&rdDCKjR zv9>0jCgk)jQD(a7{vv&zh?w{k0Cv$k3$6_CMy8tZ*@jq{;k{ri z=p=sjz`b?ntJGu!tNgNgE3NgPtlZ?~@|77sSEZCpPq`i5ZLD7-2+g>-xfpI=DJrQ? zf4(VkaFdbyhpI2^Nn1mx!On3~-~IsIA4NuV<}ool8w!^6tUOE`1EXB1bSw|)!PsaG za(N~9&Bkpq3I~5t)YzE%TDx3{ik&_s(fl2U*|48@`HFsGLc+@IcsDwRMKoHhgyqml8g z;-f-3zwkjDFVP5@BET;PhJSCBy>*Ke_~oida}X3HW1#Jfdfil;57#WR$Yw1%RL!W6 zF6hVH?4)Ebi`e2Jpint8vmWS;oBGl+df4kG5`;({x@c=fxo-9d29 z4g+L&%eTbmmvAI1Q()f_IAY5=^fT(duL^H&ZYmt&GY+B?QY@hZwDoNi=~k~v6fb|h zyN7#cZ~SLZBgXf6*boRLtE~;s)Ag9dRYbz>Fv7Qk;>hKrOT5YqbM82(zzS!`V!6gf z2sZ7pnTY|5&93~o2r#0nkyaL!46oGRZJb;?wfKDe*6|s%^`T!rEm*~khwjgix)s1d z(Z(SK@sv9e`x_GU^!4%K;y~Q*DR}dq9cq7D^fYo5z?a1x4fLXHL*8bs(kLjd#-r;E zk4l*KuDG-Zmg^16sO6vE%1GSwOLgDjYuj#e_|Dvr$-F@eS&DGgg)bF21- zWOw&SObe-$e+(x-k{1~9X@)^qO1=8ZSq{nh)ji`(GwFBpPb$w=j2a0;6~4j zF(!x}O6E#CT3u<#fT)y~UEiWjHv~W zK^^(4)08>uzFQ~rv^H0y0H?@QVrTxZ^Ht;Q$?_JZ73=!pl1A?k!rb3Vt&kIPp%L-u zEPjQuu>B(aWp_-^6?dtjubH>0*$RPk(uIbT2Gz1Terjyiw~KfN;vTG`p%XAIwugu8 zz9YD5dabEqH%9$--hsR^zz~Fb_Ya(<(k~M^jc=J52iNwd7O)mkrzX|AEv{9?w|6&H z74Z)b4-1PO%YT*D)&}8Q?|6DBX|rE^{aesjg+=T1e2n#h2EF~W>Lh33;Xk{{nVD$F zv*@wZpEd^RWcjEuo!RitLr%i!?9^0v!_~(r*pFtf*;({R>J92lUM+qH;Lj;CUXL=J zGdc`nzFZh)GkuhhkkG&=u@)E9y#f}EGLo0q&0-^at6C&Pt*)9yz#EH{sjL}+%*%wK zLQWR6fuNW|#9uw4MP4o}Qw57z-pM!2>2(i?f|5=;1P0B?>FE<;YHndeRiU+yuV%_O z1-xyotgR&z_3rV)r{$c3dtz@S`P%l> zB53@jlDH3v{DuQHATW<6^x)2`)#0$XyHo!lJ^?4UyzO~|A^tb(VpwO0*W&{uQ%y^M z>q22ZnJ)2n zmUGU2X~vPQGrxu4wydWP@sKOpEkLWrrnegSlVX+(psZ#P%_@(#*SGuO_@I>3w1n3l zbE7LS`km4g?O*qrXoh5DWGKxG8QI#|NQGtP*_9>@(dv`_U3GT;m0MuGb58D2GTHcV z(V(`jE~~A)U2=dB52f8ODRo6W=W?ubvHbZeBZF+ALHI1SAvWgs#%#ka^vyh+xc4Eo z^jV&>AntNQrvUl=&CS|)euEht;Oqg z`fqFTb5=F413ph%;t#d8TnhNA5YKy2!}YXw_ykMOEvrsDZk#qlm#b&4WL6rW-;QA!r| zoD2*w1>twAi=spHkiN&8l1^_Ef8b9?|Gt!P4+RDdwu6tuWm{`|e>_#oVi;Z!m{pB- z_uk#z{pAO|V|W>=Sn;*2Jc+{I@~TnU$PJUF@g?|hcY}R5%5<6MC*xXeB`Xbcr@TNO zt1HEnBa=8o(miMWn7PADrqi%+8HO5`A zTz3JBjfVCJ*VBD^aH^3V7i#)#hdb*!@ zx>ny75<=#VdXCR5ZeoHXDAY=!ae`1!9HE?_cH`%x>xKQJRgV^0R7Is`BA*|CDiAlv zaw0UN^FFCTCMmtpg1ZzZp{MZdL%RR(bW(Ka58EM3qI0411n&jYrHh+LUT) z;la1+^Li>p$R4QL3td)XyH7TRyc3ivZAR1gNMXIrfhgzr|`Exvpq zQ^IDBm3skTM}?lT$$a(*oBT+EbUrl*5TSOjyr6)?O=#$#O;7%~b8sLj4D zm&VQvon3mmeLgv>sM@a~MTg}DK-2E;FOb#zV@iHA$Ej5^MYpaeempSet&jTLkP3S} zlTNB@V9fdu1dD-jqwRLhjm>jJP@jnKGu&+>>{Vl|!so-~x4Mi++AXgp;#3;N?GjAK z|NZsvMriM1Xus$BdOJbBy1D!SW7l#;?`nNd@G?xAO7Sr&)Sah|`8{83sOqP;S7j*` z$NcEAgA$VtuKl+ssji_B=}}x(mTdos7LC)*ZXlWKbMP}}uyZz1Kgx+KEX)GHruU;s zrHp3hFOREo&YHh3e_Z%uU21tEQZLhX9W5|Vz4@gHSl#7wzAu~qZB#|EzUKb08^64k zS@8s?QOyp|Y2oJJ(Aj!zlTyjUR{syQY;RE#OdV<|$e$sXqc`Ua?cK=}ZxPbAHyruY zOLYS_COMb9*uTn4ulsu2=EYSWCfV!aluUiAF;`es^toY*A{FAKc(#zAElHkUgnjaR zbeSn-4F8v6`6B@@8iI{JPl`>%zhkTxusC3@9EOwfN!Y=@&PTuI_kfk}!NGkXr#6PI zeOVQ)^%oz8rrDH2ZJm3WXp;AC4p9(@$64LnFehuOJZ(4s!S3E#UwQ{(Q)aqsN=m9h z6$-N&{Zx`0-Q4*2NVA`tiwp53-;fqyd}g0F@moI8gPMJMGW|B44C}$QxU8rT9FBF2 z_Gb~@M#p505S=pD&Ya%CzRRsIIZYrG)3fqx2|ph#1|<62qofHsfWY%prR@fDBpT6j zOzOnaGz`GWWC~7sXONmJDvHYc0Q!Xv`OBeATjQ_>|I{UB$p7!($#}@|s)*f#RnJ7C zrwBf(X{p2zMYL)vTtLvP$Q^Jl=Xw zAKNu!_i&eyKCriM<&I#WB)e0{^7rZKF?UZ(3r|h$(OYTxH&_dtD1p_Za#H7;^RVLF zR?=H&SK-hPP%iZ&n7 zD)L50mwhC=zru=I>k8^}jleyjp;eXZEJ5|KXy#b87yE|=D zU7fnV8bXk+Uk_XP*1HGPBFS$zJ$av&2O@jGadz07bkGlLZ1;iUw*6|L?CcV{Bp!cm zK6(Q0^^sgXcF&EtfirOC9^Xjl443bP z6t4I;_O~(8_1rEbl4JaQEHrs5!s%<~BKBvHA@0&S1;TaAk8fs|#5|bbt=QhFqW9z5%$l|6b!cUW8}8X>YGo%Xgvl z0(`TPl;J@RTvhcSxg)kpgy7MnJ|sr+j$zu?>=nzYYN(x_$#aiP2V|n|D?vs8odbxH zxbi0#Fc`HEUnMo%u(8k#G&&NRIX$l*4-9-s%3yym2s2*YSq&KY-8Ljp9}*b2x>}|f z0r6?l1A{(fTRj--^bVOkO7mdB!A*egp46`Q$)f;EIM&>QI3liUW@0hmTQ&PpEf_kv zM{*qnJP&Bz7VwiI@Yna~SpWM)@a{mmhIqGfAZlaV^n9U$37*!E4XjH7DQ6EPv9562B^4r;cF4gj^SIIZ~vda-0!}|2bx|VCI1=>-P z3d?LXIU2A&`fb^vy+$uIi_)YUz4;{xPVv({rV6d7-! zR$K64XB{0y4vK>3E%BwRkn?*}_EPQEql5(-fN=;l3roCz&vs19#K`C+c|W1)w8guV z3tH5v+IAMZxN+W%j#v*dq%3cj`Y@wk%ctrG6>*)iSL8*ie;9VTQit8-bGO3Y3EZ<^ z2rVvHDJrs*>bBZS^5}?(QO=&7sE6>N9=a;xKvB@r+RlncfW!+h|Nhd^&wIn63drs?u_1m$GZbW+k5db#!0 z$=}8;mpSUq27}bmXpI0Z_5>P`)3W0S%$n_y?MPKYLQ`{dN)et^d<&Y7ioNTK=!=Pt z%iKCXqu#o9YK((a0{iUWzS$zyC_=yv=r1kF%BGDBawPq(L;Y?U?u5>(kj}hMm2-W_Bix#}&1xJNvqfv|z&-g0M9dZy>!PkPw-wl2*z-@U; z?R;-O^JxB}BPLTspUnM$PVjD4r5g_hXNcTZ|Cq~xG;Z7H;b@$*PZ;YEv4T86_{7LDk0;bh^?l z)oUhGPL6OJLX$e!JbZRq9h#?OW`_R#Wf$|B+ZHHuc-_|sh-6qx&kR)Qaj9~CIxr{| zDP`5wnRQgp&(BlD%V81`jcmrt(u78&1y4>A+Pls88(OR3tYLqzw7#V;)viV7S5#z0 z4XCch=A4}AoVV@9!^cJE&uf{olmb8k;Grq^(aZ>f{pzDJ%tq2+MSOf@O_f6luwVkj zUueZy`(y9Wh!prL*@*IT1>GhHnO`U6f`^rzl>{K2{1uz{4i9aBNK6t)&W?`MK^35< zCjy!Pzf2zr6GuYH2t+b|#k~CCo-U~ml~+e-vI_4*a@<~RWlsZom6wX@Zr^ieYMMfX zB{9*Q70ekxjzK=fZ|~wF;V8%NMm;;R7&(|s$U4h)@=F7<4lmx-wX!sy1ZFe~AwgEB zEfPsdX+Sso#@C9MDN{33B7sEF({y=lKabi9y$@*g%KMsh30E8XM248}Dd_QkTwh!K zk7yzQ^|cSjzCDcxP@a%y4xq78;e}xl;-Zy>ys(^r!2%})d36=5;p=1Z$zi=SX_Wd$ z7hFyvw-?dYHcX?qUr(M%-e?jW3k%CTHF~gQVB(HV5q|w4v7#%5YadiGXm7<)UvK-o z`H9&72pSgrmd^WY{U5~od*3lI1&yd6kduoX@f50B0^N-0+JyV5I@4+7DGc*fFx^c?Z}gxikCNVo*6#ikX8=zvD%zH^<|m#81mQ&L0VefEB5rTF%7uANqSuRG?LA)??!B4J$cSwiU@zo^5nLw-;&Cv-#=g=edeCahBwGU&q!i__}60Lr4I)h z8L`~)F3VccGG(X!yJD_=phS_oxxJO3!A5I3)X~<)Bqc?eS;F!LDxXXrC}kCM(cJM4 z6YkDMy>LFKE#t*IPwT~{52i2kh?*(fFS19)JcRj*x4J9skX_`ZMJr=t*eEi-j(zbj zzxW0Cu~IQH^1JYSzx5gchB&0VH8DOxJX@8Df;Cj&mK7B?NI5KP1rv-(y%EVZY)t&{ zWc;L(t(TR>B_)-Qxfob@Wa#A0%WSHZ;Y(RfRoT$7u@eyE7e*O!e?tW#94MEhjg;9R z9$_0^7q=AD!1@LnT;XBc=m(*Jy0(A7VK4@6(+BF`^$ zxx=G2^ae5<3=B*o3M0@ZG2onFAZ}dU0?r;04J|-=eIy-&AGF(WGCVoePM>B9r; zO7_I*h`D8Likt8Uc{yny7}M2Tgivo>T{u&(ufaLHU5l*Y(K9dviB-q|%BdpCW4X~# z+c_e@DS&C&pnqQW$`lNPpdSEAsZHFhS8yTzSZwG4&#|TbvDWo%o9ti<#GyZb_K}p9 zqW11%97^INc5pxfcNVC-^o+O+rgETxu;)9+w$l<%PbWP&Jq@a`^D$tI`wfU)f1rt! zm6r<+2|>@#eSDnJ^u4ObWH_5`NY z=yB`I#_E?%(#SFAwHfg^IVUlAqv?Lgy$T7W0#XrWu)(}EKjxnRt$u%X6kHOH!g54n z-JYSO<_)kwOp-32G!1oSL4&Msz^V)XIn562kMcxH>t z7l{zdF@!xiLgD!n{phtro64Bx=761vgXhYYdr(oHz#>lUSi}VlCJEmKuD13a@%;j0LM>OqPDwFU;_tE9G2cDQ ze*B%{KghbgKIqc9ShnTmKpKu2Zj-Ny7Y+x`-oWT!?-k&M^TY8Qp49J;X75oo)|Lxu za$y0DC?5UMmR5%FGZYHFlVv+LSPXGO3Oq2k4N0vXUX+HYMTaq{qpWh;p@ zYlt%9Iqm1ziO|t8_qDZpz9_UK-%*f!q5Csj+CBbmU=;99xX%2bl>K?K^e}sESz={> zHB@BSyheKL!ixtuy9RH4a2eV}F{;=zvYP9Qi>#kNiOdxOx1vA+S}icaX6)_^D(+dEzxT|63(4V)X`2Erz`ya4wPvU692j@?#X{H z-n{=QXbCQS0oV7vR9J3{qQ_O^OH08ex9&GaqvpJjl@CiDuH7IeE&Gh`?toHcn!sbs2!0IRj%L&N;-pFuf?hgmGII%DFq zGsob=)b_?AtnJbCU{EkqQ;AI+`HuhnTbK<6G?&4AKe@d_JnKuen;E_SyK@y2gETb! zG2m_H*M$AXY%p3Lw_NfUm-PSKmwQo@%cH$6F2Jh{$X6C)KDL`Ip4lH?{OI=|A-HrfFrLshfZle%!!rlQ_h(aHttbw> z!p9@~jV5hus-+qww!H9iwPX)qU@?IKx+ZZ8$a83-jSbVg0peK|)E8m-fc?vx{%02# zJxyrD*)a^hZJ#VF^oTEcvHtV7{|W^Z#cgYD&yU3|?@)1YadV3QaOXHUJ2U1OuQfbn zjL%0uG~2bl|DU@H0(WOy6BM3cbo))MYfey4Fa2gGz%8BVRn&{(y$7p!Ph#NJ%-_Ee z(ISv&|F19iy?|4dSpPg4#DPRvk2xs5y@?ht_Z#ifhLcE1M||1 z;l|Oa5G>k%R!Id)?ef2`{=e+Z`LB=WT%qdAIUdcM0>=srvf1d@{pjas7eHg{R2I#(>orAxcg9_vW^7b<%rDi96LUX3GIe6hhZqjm+-*cS;k=nH_9 zALYJnHZtvi(UALPA!FW0%RPAJ=ly(BmT)fpZQVo^>kX-?ZI6fH}Kj?+<@$nfy*AfBwkD>biUXe-n7tWwY>Dn*@9dnnC zU0Yu&&!_)&0Bvq@P0p-}Jby(4o88jV)A!#akH@dbVw(YY+NeWvY7v%@#Ked+$-}Tp z&nLJd9Vo^-*B{gq4hIuxhxq!#sIh3n&fk;&s+P?-*+PeT(wfb~-f z^Qjn#a!ndGJK`FwE-ZYvy_r1lPvD;+TQcMvq%pc3a&bjPo(qq!YULwG3X`g8r(3b( zv5jz|d1Fvej6%ccuxgFmI7UTG7TmQOk-|IXJty++uh>xl@$)hFHBI@rdQkfiP zQXJ_abYl%po}@qP$$MUKK=MV9l4(Ox?#AyQO;ad#eu{(^hPYAKZ`x6)H=HKCc-l~d zV4n38IapR5``#pdGy~1fizseghE#-nQb3|5T{t0ms_O7S{8yhHVX z1v*85cenk!4q}}r{ZOQAG`?mEIn`P2?z`tki6aFR^3B@$=-9%-D}bEO&4mTrx97QH z83*D|x=nVK+F_abbukbb5I~74di$}?(J9yBi}SP~A8UOMk=>$~C*vrmJjbls)oTb| zgp;PhqFu&Grp3&@Zi9~Qx$g)y?b?7Qh<#**i}fRDC{D8{{LAgn+xZ+ugyPfB zX!Of8noRJj-j}bPf24115=P0t4$V?ykvPKYOSL<)6qlKM@2v4zTJcd$b}B;&F0Yfx z8rp;m%eOc5TgBJFku#^3YqUNg4=%55AAG9hZ>jy&>IA9bWBQKRi)w_dE?nZfq_FWm z$hs1EqB+x@W4aN47e~8u?ADn^Gc=2&1{QsS#PR1u;!F*#E5!GwT6Yfg-ZE33uaMQ? zvDZT#nIZX>eVg27>HN!a()SId%PN^SPp>0Xsa9b$*GQ_8rspKPLrr5m17V>w!;$X# zh`ShL3I!>gPT`<5(e%dZe<0VOzAv4m0w(|>*4|ixq!!{6wtrkEGkm(7d)OyNFi$_G z=1qu(zbrXk2c(#fkrph%@YDZ3*V?@+&py8u6B*2DW=80pCzAs2kJ+DobXNNuTvZ11 z5=dJ&#}1FJm+u}Q;0V%gJ~rhcWSxYr=-1#;?~Oc>29XQ-_6+{|o%Jr9$yALN?cxZ`B(i8E~q|fjPQsC<+pH(#9TKgKk zfNk+3vaUfna8ECV6OqvAO02c987tw)?~r!mA8Tk{o$tR{W%!aJog`BwX0e(Tvepz& z8_Npu-Zt`%$*GP(x0;r}nHxf%EEoiRQ{sHq;1+o#c1aPclm}b6>oi_c4@)$4*==*S z9pjpQIPQ_s?c4LSmntZZbMsofFU?y?MAJ@4?pX8aIF|Fkw1<61nGBBl6BLwY4H=jD z6S9g-BkNAO)z{&weRral5lszeBToY}bDV&mCC>76I*1;@Q;Zr3KX{QPS29}%}XOl?( z^z1NQ^sv8+SN^qGyMJU9;5Ml#ozCmkc6ZYmrMH`xr+AiDTJfp)(_f1t68_8Eo9dsM zLgO$WmhuvZ0XPw0^r4^tNbv$G4UcvMUGUgHqh{XdJPqC*j>)3T(}$-w>={~gm6@Kt@B zZFLeT*KGvuN%Mwf2E1@KyC-p)g1pYf?f_zfArtQG+D1aHL=OAANhD3iT$V@R^pZ`< z@Xy<&J2}4w+G_to9!QmT+1UQ2g|&6Z=1};+$RGc4i+!ih4SMODh4tjC9X3&Cs4}#1 z#(hItp&w+4Dj$K`&cnk4h#{hYHa6#^kE=3j&lba|(B=;-DzK2TT3rc3+&xdDLaw5? zH_Xh}0pz-Kw716OR0{cN8+2W|9az&J(b!D*CQ}GSMc|p>;`5ntgG>d8)X2rco3s0! zV%L_;=md}zW?L)7ej>p@?GTseT92Mr67;+3S*{-kE&CrkROlePW^dgvvAre9+V zV&6BHm$jb|5pgW9Z@zRs2OqytQ2{;_w`6){e)--<^Op%7m@*qE3Ro6F8BVU*UIRow zx!YpxM#9Be7b_|rqi!Ji)99%ED3DL{^1cG}@%0=a(qH^uc|X@r3o?5zcIjG7oF24bpNBG2n_z`VM`d+c6ciy@&xz7V^ih+6f1LG z_5TV5grQ1Bs)*?5oxz?*{nP!KYfQ#eN<~u*b`yC2;m6gij$9iaf$Sbg+5w~#0-)}o zm98WG|wu`O6l zm%7b5VS^Pkvg<;40!B&5UUG$TXIF2G+&%1Ar!0^tYPDelZwx+dBpnbP$M;~+B_&x#y5?G@^^W%w>Y~cLWY+n_oDfkJ1baZOz z`ACUEp~o3$l@CwMm3Z^-{{D>OpUk|U@f9SMl4+E3Q8PuCtq^+Dn9O|a_l0kltFVxr zdTeiQW;WM_adcTh4W(iCHSJ9C7*mU<8)B#R*h3kcfRI`ri?HhvaDHM4nu8ycWDe?L zNbvlusDkBT=@d8zG9o6~|Fq<9k-^i4ka?q0Qa*J2&Ei?H6eDBSl6XzT>yz2nwB-F$3z43h%Min-k^a$Lme;n z4v;H=PqbrjF!h6o_ui|kquYoN$En}Gm3LdOT}xyn2)`zZEK{hS;y27_kdC*U|hv=aieT>WS|pEq&{ zx>s1~AmPsLQE+oJo?}qiDuYKR)XhEIO5bJ4&XH@|c3O57>#rYK14CDrXqkI*1-mtFaL!lZkX$q)O;`1F1WNvl9ETQ~+9Y*9z)$MD+MpwH=fCXfu^?m|^*V&XikzMW8)sV6m!!aF?FCM{l74Eum$`1 z0Nrc?qZWLTD*THFn)hT%_+a*lP3{}Y`1HSTQBmlsYU+TOKqycolL62O3`~HMy7CZ8 zpQnch&~SBr1SD6TXzga4h=Mt0$1C1y4?L7d+shycK!S50*m?GrdAfG|oTisC1XrtmTOR2ZiCW zrJ#5hI{y|x=miBlO-%xy^whpwBY9oT|9}P_A~5J_?07a+na?O3EY-dSs*42=2F4ap z>=e2swvp_}$&Y4Z8E{xrBNFrZ;?=r0c$ZNXpVxQ2U~hrkHb#o@y6 z%i&sll@Lin2X08Htg9FO&!iSgNgM2*uc{~wG<8BJ*8y{3i;;nK#V@9CXq=4LlhkXb z9p!p)np$t3X(P$U+nI57h-`zV_<#*N#X$ivG`pXC>D_BrDykez;>-?|JH>R~Z$Mu` z^s9<$u$tF(gB7Uwk>aX+Oa`m)u!)cxbJ{%=9ULfuyQX)L+8Ky$e4Yo%V^UL@3`jTQ;+jJ4MJmp11%Xj^Ygh-7hX?$B z2nLc=ppxnV=Rz2;5Rq#ol4^(mm~8fO4qacu@x=_vHC(jk$Rk0cCbnkeX=KXf(mlq0 zBCS(8>6pK(F7Y3Z){P6C8xSEerX?bL*S6aMuN?W&I#$e)^*D7#U0g* znkBfth(`H%wy}d)uFpD%x~D#v*O!PJfeHRXh5R=hNdm;s)5yVCVx;T{U? zn2DmYUQe}0(m|A_f!1VT40cr|e-=t{azn4NW!U?T5#M{E6ga`Rsmt|juI+QG{w5u|?Ur0sd!KeZE&k>LHgl{5 z%ceD@@rsqNRJQc8$d!M$^^fP7<~)iF5}TKJ!le8-(E?1_q#xvd>g5siX4*uI`}n*R z^V3`~!rRxPFw8g1rzpxBv?@Vx;KfBghMPt`Pd9UZpI>XpsdU=VoJ+_rA-b?};*Yt` zCUGE+<3@{=FMeIj~=u?Jo*Th zQ|Co;1y&tD@mD|xAk^~{rn>o-y5`4Jw3eqOtwxfWfHZ$WQ~!7!E8<@fx??RJKt)oO z$F7SY*f{St542!`!pG0wpJi(6JKH_!p@pc)TG;BRF%&lFH-O)TY7lwn!e(qt+h*lm zetwJ1Q+fIR!BOvM#+aVzxk=>nUKi!pl8hh6bJFlB{`T;SPfY84J2Wq%SW4~-S+F9` zJT+Abz4=i@d9$&Ftb7xl+WHkex#uo+PQ*DWCq_z(`Rh51CDL-j zdglvbrpy`!4q{APj#gY}bdIyf=W&-m4_TVTi>$>SFx@?;ytvu)_6j}wDZimB?|{^w zUL~|8fq2XbtPv#fwyize@tw*6fBbVv$>vn`Bj-26|EHin)>phk^Lu`NaSZ1E`!FS+ zlm^b@_@4!OJzkWTGUve{ygP(6O;x)~VyrP{4ch

^2bB}$QM)(CMUqu4{{m~Z(cH|g*v_k%EQhnznV&%u#_GRdZ8+0S<`Ig1hwmf1v5 zXL@lcNGO+WJovPAK{zHK-eSCYtw+1D88_3JrAqvpQ9xNz!fvO-od$+6W zmlrzE!=BZAbg5Ro=G@ze_!~8*-4Rxf^=C}5g1MJ42ya!3;xy2gf323p9qSDmw`$VD zlJeyV9@LJoqb)QkEQgOW7MHD!_zsWES)7Ri-196MxSwsc#lLi4+dNXADc1h*UF?YK zZ_?SF#so`B7k%Pi%@*rY8=o68ZeQ7J_L^yOR&?DqrJMiZ`@*ZSg!t~nbmjILajvjY zdAwnHeE%OC+S@ht;oW`>Ph>~6pde&qkt?Nfa6^su^fs=T*UhAu&$?d4-L#^64Fplm zmW{h_(nG#INWertuDimoN~4@W$G1f@OjPR3=YutZsK%`mR=Ym}7~2^y^z{St6ufAz z^M_#)iR=p0pIy)!&7^16UW;Kx0~tGO4Q5Tt%u|s-c=Cyx4X2i6md9w|ifAo6Bu92z z)CpXBA}ibRJ%z2jynHuj8l+-XgsO_&$#7_JAMd>Gs?+kP_;ze|TG%)u;wzR*^!*c~ z?;e)7xAO+^tMC67PYz-jMjxYSy>5Q>_nNElN4lcHQRjEr8HD9`t`NDd)6p1d;lH21 z=31c@^LcqK&{uw2mwR?&A8RrEV=yEDbkMguEKLoE(7 z{YYLFk8*D=p8NVfgZGP!Z(?q0a{~J+07U1%iVt?OX2kxfcte?;o?%!ejIGX$=7$)&I5mAPStqE(1#YL`TwT`h+!lq?YjZ{RxlOuh@ zUjwr1A!DUt%S-zh7aSac&&uY#Fo(?X5U@-CNrA^<_V6eCp?JuF`hy35KIkd3P3-XE zPU)K@PHBa<-y5%Xu;NKn@jTtSIGNJ-o7MZ(GBPO4U1-Qdb-*UJ-B5d^PIt-2v3R*9 z4v(bCoGvFi@PJqBy=%LkJ2>XvVknfdKZ(Fb^OW7grcxr~*-nA(YTKrs(Dner^0x3TrUS9KUwC7Sh)W%kfHU z2zO-i7|OxWP1xDR@95XXwbu|iN{H%abljllhZ^gU#apTI@wXAsCJ2mGEy}w=QHr6S zo4-W&Lyb(r|Ma?R?%?&%>cU~J`$pSOkORUJ?bk3a`BzXL$@3uOOv@2c+|u#DhXw%~ zFmRDt*B|TsOnw*;DpwL0wT8xSRBH_Y&uFO1(i$4JV#{eA~GzBNZsR096s zsLazze(oU#0{7dvoL>v-8`)?MwedG9(Jh-KWR~`@bh|M=-H0xey zYbWk3Oo_OhR5mnrk#!jI91Wwzb(_XFOyB>${)4^l&w+#EXpYSOZ&$alv*qHswCrje ziWIo(=D^|A1m@=AF4gijBG zT1&WjZ`g5B6_XolUAZ~nwroFhub8g)j(k|;lUYNxxG7`xq@eG8XoYVzkO0Xwi;7A- zo5Z+$BXz7KFfV(<&aF<~y?f_cm^72N=bt!^Esxd_`Tvk)KT%e`nl^R*kuYX{lieDALNxwwAG{IlZ1?+uiyXGgtaDm#9sX7NH;Q=M#e zCJeFTIRaEdtA-W1c5``~V-`>p+F>E>N!wQ>Hlz9|wMm%$+wa~+Wx_sp#qj>Ov8OYz zbH17I-e;=4E{WMf_6d}q)nhkATK4&mZ^WLC>;IlaP#M6CD$LKGi6K2ZIcs_A&m7*~ z-qtc0lC0XU)82;nJ$D6<0}-NznPlZ~yDeJf`E#_fvGMBRr45qvL^nHu1=g6Y+OYjp#e|qfs`88RDbxk+-Yg7YJh87Ixlma@q#v&wG(m zRwbW$Xu6%oZIJR}8GqhW>Yt}ei~N3umGH1aqj_R*T+aqMY~)*zgI`2JrkRTo_^pe7 z6P?l2*vRddvo4pI(^8@+vlufej~#plF82FUvhLo6cdV=uNccWD+W%nyYK2q=c6U29 zsA7uOwvtvhDkrs!cl;-$o~^BHHkIdM=w`e^;mZ;NBm23Hh8{fB-@wM*wR2IjW@64V za(l|o^~(l&t|VlXzv>>{#P96tT3q=2fjoHtVKQFmPK`CR^{SPpQ^bOqzLbLO$6MK> zL`B;2<-8xAb+K#BX`d_J$GuXzS;=knAfyMukU;AZEG8mdz zvN-+)Zk>Y@vd!XAMz!m5mB%a7&&U{ zKUlEuQaeU6zqFM8qvWGICuon&4+OKUjlnOkUZ9UhS#x^PCe3fNVJ^yr{0dUBH74Jw zRDGn2WIP&a43_ErI!nX{y?*|Pdh)t*LwXJw9h28GEWWQ+O_cYcS|Uuk74){#W3xQF zl#W)~6V2^l2~9KJU3);q4L5HxS{6D9c)HnQ{4MvRB5fbRRstd5e#k4h;U*Z>ZHhuM z#tV5jr4HT$NwuFN&&iT%H6c<tnBQNVcKtTmHecnNi-*pEDEe- zgmlNGdU_XUnP5hOo#8DYV$nD>p!=kz5_@b^FS(AFWMt6F64Ku6$5=OIR#{=NFMrWS z!|^@t&2>1iSh@V13@dUV^4b&nvN^_4h7^E7@m++Z_Oj51fNDOQfjRJ1cgFVT6J6`! zBo655DL$qjAKa>3L6UPM>>V}sYxl|D@sCuQld0X}TynNqt(+tikYh0Y@Za<`lCsK5 zarPJnUfwTw-&sg;V8k-=Ibu#smK)xD!l=OjBHEtV!seV8YpHwt=q=oA_wI>HdeYW< zT4a7eDC?>Pm#B8=VRtX!LhWBNvt*F+QR2_eA76aK!+j%8$o}7IPcHKqYF}~2cs}?0 zLwR};@gpw5Zn?=Q4V|zpd7JcwNHmZn&00I|NDPfnW~i}BIw)}H>hW=Z{;+*S)H+tP z<*S`3A&nu%_76x^!`f+=FY?}EoP5t+D~z+UW@&KdW|oq=u2S2TD&;@|(|!VlOc# z5!2K>LT?Y*#+qru zWQfUdCt`-Wp7?{DvHL?HZqDAK?z_mXltS*zIpt~***nh#%V4tlM~WXfp-m!bdH;;B zXJ1`Scn7h`G0mHC7vsDkI~@9GbTd!eo*(4&9T>m*fJoKcvDFaz@}4@=-RcIyL!e#Q z&W;%APl^5)dR4dlOY$aN?cUJZ_X0YBC;i`gbzFKJ0|(99K-X97ja+Jp-L|T|x&}=* z0MqDFy!(5U@l*!Lu~|ILT( zTjbC#ArFnqyJ;uZ^KFKXpXGPZ*89AB^l-X#Q&C2vmmQtns9r*@wR! zIanq%XgBzyWB$8gKO^-bx85ZD<$$^e`k<^AMsD@i-6%aS8Vl>}TNbzNJ;^{;#Br9I za_x^hGXQ$KIQd*Y@m_HDpPpK+rM~2L@5>9W zPcboq7oR~gh|+APcwd!17Z;;SD9$I%tZ4`j|2PNutW;ge2WvXY_Pa;l1@|8o>GpH| z)j9ojsMu$z1B)ZY_;@1Ga5hnrL1#X{QSRwTZcbifz}}J}FZImgDsHwKZt=+B$Q0l_ zOH0v!Ux?!DRypikSExhd4EvhUB9~K-QhI!`&_Pb&W|A65p&J*+-%Hy%dv?36ZKTqf zsQ&o8l;p)PfOGtxTDqqt1a4?3(c8>9l>;tl>2d--%pVb&v822z1Oym-K$(})81r@* zUf>qa6+PxEW;^!j>2DoQTnjtU~%m08!D zVdv)Dl2jXln-4#l81fkRVwO?TZRYQBy^~tF7NvGAkmMQ%UT!v-$id0$s$&ZC@GA-t z$G!=hgQH5=@R|gy-0Q5oaVV6x+j0^2^t?;pGl(Ey2d7))d2qUMNujp0@-|rZj+{iN zW)a4v=L-+?7he7J>(8f^+W+_$tV&`tR^r8+K);?e>3OHr6VSPtcH27aIPyjiOSDI4 z?1g8z`hU0qvNmkngbC(bBt*4bUqlM<^A{p(u<%|*e9&Xoqu^-NcF*0@Y${PNQpeNG zrBV;2%4@^VZ9~s(!yYo0fI8I-;1X6u#dUSxwD!TY*uktg9QmSzr}>TXklfHOVr?!-xK&VQac`2CbPzg0a(327Qyyn@Q@ChB1)eV%>3R6{9@~$!6FRI7TXsrJS8!2_)Zx~d z$HetBP9jTb2MgiLi__fiIeEmaWISepbaip7$I&|=f`QBMy^H4fPS#h(2X|3M9hbQS zjXN*{_52o@Fe=(X*j$Eo4hR%o&bCux+z>x56pEC z&HY>RwK!Y&5Q29n%1*?SXxtF3N(j)P19@f{&Sr2baAZ??@j|Q{W}Z$jP~WTRNb2A^Zlo zd-@8VZJuqMItbOD+Y$A<|Cy14hT$WTZSFjd?WP)Qu+vo2+yS91{(olCCzV3VK0CRk zm4KpcIaGK0Z#G#iFSx8v=N~K|hyV$5HnY64e0Da#zSb4ouvOZTSTPa-si_RSmAq+r zRW~=aiTnekZBBr6m8!-D)`j0yeCdx86{=h=@ju9*@dMiyds0MCnQa~|=ediktI3{U z1Og%IcGW<>PbRS`fYJwb(gv1mLt7*%ZrEpfD60MrIzbo#XiUgiGxF;QCiV0HOc#)w zNW6JVa+-{dI)g(*#09WY<@C!xC@b;uYxKo33-_pi*Q7 zfXVLS%7F;?DuMFd7qv*Hn5V@v$!BR`8T?g1TV*@PzU2u@+dKc_?l-yf^Em-Y7o?u+ zmy*)J?YgdsML#)W{?dqt>KcN-U@4=ZfP!&mX1jkC{1~`gT-GR+a9zIQ@z8j)d2uRW zig-SglSKXb^23P9Ke7uY0K=|sad39dU=Slh**B|RuM~;r=@$kK82ZFzQ5Mp9(T62s z0v4N-lT){lACan>JN<27d-wViWjVH=X3W$ymP%-+)F(f_PZE>d2$=umP^7+A#kWdJ z5IXo?#N8>qtPCAoIqltpYk_*!uy~9rgTRZ?IrUFgL`GT~UDw?=Ro;7&z^Q8H`0}kC z6XqfqLumGvxaKn-;QP7aQGm{VdTdZwKcQv_EhMC?)>TF20kBIY?Cr_AyHDL)s%u z8z-l@>0e$c#nAS57@^#ptc3NVUT~kn+vHUcZ5`MhuE*l&A0l@(_KE8yD z6bk_Es=Ur=4|<7ewg?e%_mhH}45t_J3k%x@&pm(-$1KW-1L9(7Y5uAsGP{T0>Amf0 zR6t6lJIxHHk|Kbps0{ z=%I7(3yN`AZVZuAlPF&b=HDgjJnP0Rp~@yC7eYQZU&?$;ftR0ey6)-{3wj=?p_JS6Y^GK>^uLzBPrtd>rUlf@<20GJmbJ8F_(ti z{D!G2l@!yDEL+&?2Mn0{*XPneUcl?OZ)7A*&=h1cxbHDK z`hyJfymIoXo*(LDf* z@#mGU$gY>2TvhN0>MhGK3d zk;{WJd1~tTXss|E;NS7WW>$ksAJjnALFe)MUJiQ?iiHS4N&P0rV;6U$w%-AfJuh9>CY9Pbm+9Y2F@bN8 zx_BV!c#Mo_rZ|Y-b}*`4Rab$0VnWxjSkw0Wuh#jtw|5K1g}qtk%o);+Q0%e^4?YQ5 z{lT{n2NlfzACk5^*@X8FD7SO3+%X!i=8)AZ!wnZ`p#Nm~`K&V)1#0qcI3ueNE_)H* z8U-ddmFcIQ{=8||y~CU=)_x!DFYhc0(a-b`L?e8}&JP^3-1eyOF#SmAJ$9+;FIo(y z$HmSuE>GHPs^V=i-0H5uX~t?1c_`PV9$?Q$h>gmk(^Fx?0lNKH4K;jTi|_C-tbZq!c}#e^}=1Ai-ixOfN)m_m6eK`0o&7CQBiS;QXA% zZ&-9PrysPwb8vNRc^}E2FGMu7bkNs_CJBi`_x|{v*WOyn>`0Vfb0yfSswEY_HyWg= z$k}YE|-{w@x9^X&YR9JcDl))cWLCc5^4tz-{kUf3+e!Crp zWM;Kid;qM5J=v%)~P?fJ&VU+k0BjUA%Wn6ewq(epIk8>{{AMplok;4lH?XS zBIYSh;I$m{RS;+vxt$n)A7lsJBTOvVYAUmnW8>gHadswBV11a*)WSd-0!wIBV4Ybh z24{Z0GJA|}!eBSR^P7w-QcH3$F*K~P8)kz;q?jO=;oJ`UvnK1SLQx=3Z4%JTKs#Pf zs4^qXXxg=tBxVeQX0whck=_FxKxcx6&R z*(AJl$rJYWkP4J`B_R6*Y@r$1#lDrgX~pMX>K!-fiGyL;te!P+ndS_h(F}k!gq| zs5+MVJ3Pq5zx1A)3PDVK(|^nBs$+X^Z)ul9>&Jf%sX985A3uT$!W|b({T+ItapG%f zEeCI*OL7M1S|ANDftPFT!F_O(ae1v_X_h!(08(B>L#O5JXV<&8Bl7UDv*ZE4t#o?G zo@4keu`q+WbIS9@7>%eT9R`K^yOx%;e&LvTs_cf`)H|C&LOUDum)BF)N6>O(q@`c- zU0xhw9S{66X#eGPA-jG#i})Zz_US1G3?r}w=M*Tk5Z?_nXJ5LSFq%C&vvb*fW) zxOi4u79RfWd?_hn2lX%O6Xi^3IrB+4j&P!9J#tYOidj8nh+sgus19c$gxv%FVQQ#xR4#a;L zdo5M3=FC7B0VH11AfioKAlbl>+1`JMd;*8r%38QP!iVz@VFxd6_3RNL{X|4_GmYjC zi`(ZHCrTkyflX*SbZx&Ea{I@|-l0D8#OQNCbF?r+Vh50vxf<$cTFY3Er@v zcox3OzyllmK?Yz6PlB8;P>hB00@l}o_FjX?Y*OHJsN8h7$nc!?Wu9f+AAhJMBmE)X|P;ksJe<7uz+ zIcx9c%eoX3@XK#^Ag|~l!Aj!wYk|d|NgeJHm6i0ncBR$lHDcqw!&f`}01q-={8A0P zU+KP>d#^3u%0Lo`+Un&4ZQwSbrbbQ zz*ez)O8xO$R{Gbk*oC#{Z`Us^u)8jc_NT~ z7R{>3B?F4$^fy~sU0va=i;zFhVX8zXMzRpvF_j9Y|D>W#p{wzQx%w+D&}S z*7i2ZOllWS)YRC{h-d+WPm1RcT3B{ahgjxmfzw*W4}^60rb2|v!0=xcA%b z#{pq3)@|0-=`LCNMxus2f^+0A?_hc$5t5lp&19EsV1HKl;I@Rz<&dn8jTa#W^)etE zH^~F>8>HBN`bfVj7q2b{cb$9p^#wW-hhaMAVz?^bRl~-`m9Up5>wX_PQ*iJ^Q`0-d z)xzWYCtM7;$+Ka(hK1j^saIE5;l>fcK`XUVO$f?PdU|?noju^WC#oAj5D_U!$;jAm z@g*v7CtC1}{E7b9#LcvRW=sS#v`YB!e09b4P4(_m$kD z1-4@by%2`hegEVj2wgR*fUYw=I5iNs_|11C1q7Io^pQ63`Wj+%wBqB@*RLy2tp9NW zM5nfnj?Ko{^ozS;GGx1ZC28upBz(OTf3Ywzn}E)rd0QG|pXZI6J0QIBk=(>kSt8EdnK&Q&BNeZee?WH?Z#y8EH28i>evlCZzE=$XKyJUw5mO@jzfdSm>QE7dEq z8u~50M?yo35qNEtCc)vmT2av;RFM+5Aa}@aAZC8&f5Fvs!1|; zZdWspLi4BD=7A=$DAmoUKr#rY7yAIN9isSf+Q-4Mu@o~p0wx!a0rthUgsaAWBmJAS zJ-=~puVQD<9Dhl@9!PWGwlk1Ez$PG~-{$op<`E=g-?lDY^PDlu9%Q$u-?I9op*HL7 zeSqb07SzxPw9i0R}8n%tFCm{;Lwm}IA!fLGFY6OvCOYG^|f!j?2Oek*btj-(_RK0!Bb{$n}7@62{yY8+S=Bo#Vxdj02M_FGV_sZvqe_((K z@YP{?v6&O}Mn43H9|X3LcwO|QOhc6%hHO*!UB$DyZ>sgIg_KcO@Yj8iKSh7Zna#(s(GW#F~2=PD#vqG)9Tj~7cp_J1<)jBgBxK{!9J2nLa`AeGd~|ep*i?IhXIQS?j=Cwp5>#^bcn4k^ z6g3KtlXQp9Bocv-(oT0OwK;6;|7@?GgsH1Z-AvDtHr%>o=B7TQNyfy&U=U=#g%$C| z;W*fBB^@TbwbZkz7r;m`ZHFaAMelh~bNVITXJ*ff<8c!$ow?!DQ<*O{ks(B)C-^sF z&fUJk7ijQh=I8aK?d9)*x1af!v+G=4YCfpKMDP5xxVhGp=e2R@_;4!C$ zwZNq5opd|}8I}RP7|U(Nj@jEi#df`d!BWPb08L&}d+UMi4nP6Jm*X{;g+)kcX@d>` zkR05}2B}Mp05L)!05gTsZOCy*n9lS1`vgn0;&XcIAtEJ|YkU~ZiJcggRopL~MAwaEK@<#Uneg||7eV^YSje}e% zxb#9!@HSBm5mApL%l&-^h{JgTJTO64Lf9?2)ld?&&t~2uNr}X+6DJCh>OLqmc&-;# z6pRhvG84C_)Me1z36f=a{P+g2JFv4A!r^6M9JJtZc~VBH{E3YAq?(+ZoZEfu$w6`N zc9G#njmyvcS`wJC&pGEE^c-oL7!8NxB{V zl$+U7Fi)Lg7|-&K;Age>VKhUjb#-W2eS({$984THqezUZK1ToIOHLAD#(-%p;IzxJ zN)uR+5FO`DU_dJ|9b-iC~$w6QIQp?@_`yZ`BA2pKF{U8t0HIWDawgl^ zOQ-2(QLKsTp(N?i3Y1;-T=TOdXgB3){v}4t$Y@mMwZRT+YagoPQxq%*t3ER%`@t8u zsR&ST3l|YWh#GrG5<*N&?hp40_OJecX0bJ5ge?$6x*;lG zA|l#mW+vC?a&kA>@AZHciOqH|#S*bmuIYN zkT%>AnkeJ9RDa0?k-j^t$SXDlIyH}G2Mbr1d#Kpqz72YuXq@VnV#xP`hCoGvvg7#~ z`3*hFKI$0n+8qNp%@fs&>c@Xh#;`9TCxyM%Kk->H4>0fs!4glIg5#20k61oncL9ze<+XvxHj2so3kuk9R|IP9w)HC1Q`!o`~xcj_I5@PIk2p zOpQGi2d#rIqSM|4Vc-eAamB$2QA+ct?`;O~Hdm9kXWjgO!Yd zD(1(YnB;Z=+#wT~ZhmxHare{n!;;%0yFxW)f0h&t?yltSN}^OPbAB3wK}J>Vq7`GJ zP%i(_fTK4{gs`;CuX5PLe{uF1F{WcV{_a26rO2*V;C)}-czeA5Ihw7K2IJ#|_QK0d zND*WC@xvm60dNn{lInWdtZ_iHyT2+0!DVLfFZhjAw{dZyMP#-a&cIR~*jXH&~+{C>v;m0$~m6P`IEL zJpY1yK0tZCHzkKUuIvv^X2AfJiGH-b#T?UxvM+@Vp;;AJDSv+v`Oiz@5LRicIq5$4B2vIW&i!qNF?uBiFs7^tO@1?nE&3hSH2}C9r4qMeLReexsW3R@9+W zhV@4i#+0*UcJiq97ULoMu<;LG_|lNiUZ)S0{{%Ja8^2Z6t31Tg4>`lVgod3NzSn)K z^K;2g7gPtG=V1RJ{eg`W(m+>4H|MQ9cJ&*lFt_UWqJ+;le;VTWe<1m{XXu=?@%Hs_ zifN4|yv@$h9~))f1egB=J>#0EK+!xT*6Avb4E)aIFV1?FN2a_jub^{9H+AB>A?A!N zf98Jm_Ue74j_;2>7JmiBx{O`H&7sn*s|#Pz;Y#l<_p|$(x*e$#@7Ckwu&blDCHJQ^ zCax?d53sQoR_VXzZmk%3A0-`TUQLv&;4o9k{`-;c&mi9{G}rYnFBh;uK~9GfPbla$HzEtl8pK8@FB`;;w|Z9?wzsah z80X>ncy{6|cKXg!R^0pQiji$&%K#ta($oWWQ9qCQH#O*GU-_ic7ri(ksV}tFN6Js# zOG|r>!${UPEq?6tY076()MaapywRxNI!@_WRHB^>b&IntpXhnZHZ^QmZ@%%lAVBc< zG}*z-=g=RqqAWc+y3U$6WO7x>uuS=r*|hfGE^M=^_f&2D6XXukQ|lIoFEJ0se%$=?#yJ$l47kRx zA%)m3i@5dBY26BOd^MAnnlf=8^@72X8JG=UE|-zEr#3Q2_n~?CXt(i;wr=9pHpqbgQE#Dyn4+`|-$ZNXK9ZFdIKcI89 zX8EKjAed(GVLm(E46HGH=mhFW0iJ1LBWFTrE~6A+5?=*K6s!;rZa5yU7b(|TwKa7>9D=DI98M<`;yiUhte*QKUJ8D%e=8{! z#sz%jF2#|?t4zvdNjGh9(07-B)WUDz^}Az z(GVpC^V1RDUNQ2|a|bnl%-^^8VU31B{mTt44ZA7_)GJD6Uigo6F4#e5q{s>Fj07qa zoey2R_gN56yuhbEd2|B+Up{Y#t!=!Q7AUuW|M>BvXlc*0%56gsVFTUX7)Z+oHC7Rz z-k=YHiGYJkV}#eP{P`+>A?nE6TuIFH#$v1%L+rGRYi5g{K_S5oh5idA3uI$ zeteq%e9FxHM}39=6z$Y2#y@lx1?e9MXrIHDCmTm=aOvt|S|>Onw5>qIy?F1BNVBQO zl@J%h-MiNikx9`(8kfiXMii)9J%p??>))S2@$4Ix0M+vKv4t=`;=cf7GwTm-z@Cqj z$T|S@HcK^PkdKHDpMHAhPD+4kX6t{cuV==;kU|JEP;FWad8aq{4aiuPTtfco(yVoO zOh|u@3u?FKd6K>D?M6=s)rHga`l&?_2vKVhQBuTAUoj1_-`NKpAAK`oiiefI8aUTUaPd#mW^-1}+Q;kvr=3NlS+>zt7?fGFX1ZFNW%oNz{(0cx9( zUx+5fOK@9oZD}b;wo>$3=F__h*Ael(->#i?w1*ur+nze;*NZN^1jYg9FMd&$W6$;MqmNeHk7h&ZaJz&z-^v8fhN&k zi#6cuyy=AVl^Li*W}gndgcL)Z@}~i3o5&CF1*1cfrH1DdRbI5-7d`Yl5gJ#I7?wLY zr|2j6sG$(fX!vOm*TmRlW^OKQO{4HxNUApn9DFk6<7P&el7mtj#BK(Xn9Y<`l+WyE zM*{^g-cT?cJ}6KFgOq|4Wyk>fBR1^`K{)dZiTu{rKs?Ey`?sLV?l#^XrQ|1io}B6F z-)6e9>H~dzp6GiafMGYR$P+A5hQD43?X)e`_*>rw%_COy)6DqCe};PZDGk<}HuX|w z_9kl}B%37oHq*~9j}tXqxPPBJCxBnVL7+-Ti52jfD=QC;cyC6SO0|A%qdk|`cbLh46_$CST zqjjfFjK2QYHGbh)A^HQOC+3Ei`cAc_Mm35F&`YV($v_SxbXoc`BXj>0L3-^ zW51ghJQu62nb-u-A^#YLHL-f378WR-5l`ghAJ$LQU}@l4Skl~jv1ZF#7y2kLjzU0Es8pkAz>ID0%)fJBHjARu=EB45@|9kAd7xi%p6p;$BJw&w_; z=6U+|{IjGMyj?i{pc+v} zG?=9}1F9%jjKTUPJ`l#`eZv8$9BjiZfb(sUtTI)J!bu+LDc4-ex7H@qYG=~cv7e!Od0BsL}Ebf_i& zJcVE1J&LMy@neD@L3su<8rsDdVXsfnN!HAf-bg}oUuhp-oIyK#?9%@u=fG|rY9pj? z3v_}~3>uYRl!WgAzU63BCziJ&uyAcQ-9w(NIm9bYhaV|^y8pQ%vCK^r$h$>$2iiRk zsKR7GurN}55gnS`vy*Vj7V{K*5i)Kvn46n)@D~NvKmIv6*7dGrUDLT3Hq&QMKM*kQ-`5R8#F|U$3DqY-^)?f;{K|tBJ&0I zkI%Da*4ICDnWmSPMni$jesMorr5DevFS&PCT0Sp zk-m_p@=C8R6luP*6;2Oo=u?+ZOzn)-=-)}aJS?2}$HL+kz2zAsms!YbmuvUd9f|p5 z#oZ~aJ0~qKr}Xk;O-!jcv2SzS)T>>lwi(j;)J>6?$m34`h7nc5mhgBuYkD;^O|Stc+IBW+{v7SP7SNoZ zJ$pueW6{2ub-ScvyeH_4&Lz0&7#&(~g876QO~Dz_%wCt*zJ686C`tdRD#G{nP!~)c z+@BS{X1<(W-3*^fmCaO}0S}6u9lNEaB`@Ejk&Vu`)>KbS)p(Dlt{9pa(_r+L338f6 zumw)7cf$BeIV#5A4RvA<_iODEs#jWV-JnNPZ;60#; zii*ygzKTjok=N3?%QqM^VY@^D16W00{pse3JxDcX*Va(=x3I9o+}Db%WJ!8@=$PoL z1{(5ai+9<(yFnqJ-q4T|R%v0@Rq_@r8Ky5^s;a8`kK#u~4JgRVXZ`r`{+W;^T9C9% z8!_#T(836=&CN~Im#=vG#hyS!#Xxbfl(aM%36=EX=jTv}A|)l2(Dh(~?J9iuK2=(( z8@(D^^^`-?18tm7Xjmoq+qf`bw z7792aodq!R^7>P*4%(c6NNt<^gBCqty7D?#nXPJJXgr?Zn~}4(_e-y!Ve( zHc?ar1r$_}R8m43q)}2jq+5wYH`1U2B6(iAK}0}Ax?7~AOS(Zi58Y=L?>E11=Kq=N zn&BTWJcoVu*?T|FTI*i-x^HPCFY*^JUWiM!_Ky#XO)6oXgoHetDDb8}Kld^=Ny+?P zbQNON9038l(U&it+rq@QiR|{+PR`CM1_oGSZYOR_QYsFNuxSw%x5+K8L`G>uK{wyN zpV#rA(>zCz#Qdnv#?tyR>!G0BTs2Gq0bmPFF;KrrBy2^hC@V|aD+|Nu2^4!lq&5?u zWU}e#5!+)cYa6uCLn|q9;;-g6CkBDbnrjhRE9`XH=hGrR`2k5hsRb842RV>G+=s|TUdat z8=eb3J_y$GrE(teJR>_2;NuVXxFG8}pSL9&DGz|GZjO-9e#q=2#ZG)hbu|)aXXE+> zOJ3f@MRfk$#)d2u$oVx#S1GMMQpK*ZkHO5;=oBk z4S?@eTkGfVzq@6<KWYu5$Slhw0XhUs=O}<`7}|YS3&sz@Tm+`!jDT|* z>TY=3@>_m-d3EAmNc^86Ra9TS2z2>F>gt3!I7|zSk4s@y?W}Y9z?fGo^jeB~Qqt0k zlcS={0S$~ujyA8>3jY0#c#iM}X6aJOxj-|_6(@E4nx0Pmwyep;!cud5!UVj_h%aBR z%9(Ov2ez4=o-!~p!_`R?tzBR`ryVG1!`fOibQtY%_AOjgXLU-4w)@ zHD|cBE1RoSR8$dbdG+C&n>H!v)&b*WA*16I5QzNxRe_p@CMqe(YR~;tt!4iD1qPJX z#wmPa()R547Z#Fw_yXPPpX$DPB_=8=csD%!&)^VjYu)W?Im^pt^XG@tjcjus337C+ z7M7@;13DI#u%GXqL#FLETZvB3E%k@c3eC)?BHtk{UfPkJeenFw$kYev>p_+CJ)#GF zeWNcRp@!Dyy$834zR2;Mzch`Ej~mg1@t6QSGUDQyHMBi#H%rja(Gos*px~yJP1{R~cc!N| zF;ZzHARs)c0NDqStY~Ox=a$x_y!6O(bBV^5%FD`Bl=T%P-1xxw5D31WHt*5KNKjNx z4BByU_o$MV?f9Hi9C@{Cl47nQlD*+w71LfIvf>jSU^V5mYsd68gbzn zwlLOTVFpB8KGV+i>GIgRwC6pot+5#$F$2TXtsUqSBDdp8*xJLBonr>F6qQw#o*SSI z$6mv(EGZ#X?9|2qh?^TK75&$fgWjuKM~)zm$8kpHQk>Z1@=F5gS^EkwyIkA(H7EQik8kdWKsAw2GGsTu}t8 zq^n0q)8jv2;$sgO;m?6J8m_EdQdZJGKHgYGYD2@wm;f9loylsbKfF$EPY4MA%=%xyHzCTN>!68DOT%@$c(fCckv4MnCJk7P9r&)An!LhwL!5Z!Wn9Jc z1IVXDTIS}38RESnk9zyh5L9#X1dx;fYs1cPn&Nv77ApZ9A)Fd3J1P3aC%s$JWL zw5+VGot+ZF{rkA@kolAs-WT^4mjF4CFji&0%{_`bJj|-h4v$GA3d9$w==~ayHvj$m zchL;>*;zi`_T~Zrj=Pii&7;e{coqbP4M^~_Se#<@u9m{Q@f|`!gqBS&X$x*u3+GcW z{Xw;VZn5x_C44UX)ef` zOJjrfH7_5(cFe-UVu8z{LXHt^BwwteFVliY-N$EoMkr57x9kkWqhDmJ z04{CCeX-wsHKb7PZ*>Sgp9u*Gkp^%jUU&rT8)8M@Ux<1@1h}P`)85SasTM=D3@kn6 zY*o0^NwDce%E{>Jl7UU8s+xwfmfNgUeSJQ%Wi;#pLjZA$1^C<89+eoOc5di=4y15puB$ALqEtj4^;^F2-NZ@Wx-e*0!*waKk!TQ|n zcQYnEMs#|_{lm^OJ*$Q7M6DWB6U%LW%runuS7wsi>a3Zix%cOLNvpGx`#xA5AD^VZ zRZSmW`$l3l!I<#zlWy4oHBeTBV zRyDeC)T&IimXDABOJ9X%ZtAz7AlOUYZz4lPVa1XX2FT>jc`7{U>gqZ=J)WOmgHqv% z%_c=uEWWzL6<&V+Lc2bKvWde-(;o|O6*sU*vZI~M;xXx!6eoNhL2O!FnF|pt+}_qABuDBVf%PDSdN8-9qIoa*}EZR zO-<2+!Jqsfz0FU-Qjh-)2MK%P{g|IF6&p?}4mkBEU6JZO=rZVj4cD zH`5v1b{D$gO93`ZIkDi!_0nd_i4@c_pNO2HSY6izrdwT>7tCmC^+monDG9K>Q^+Z; zp$TybNu?E0Naa<~vwmv6^6+fC72TEDRX&ecBBr;Xnl=%nprBZG(q3(ibNpJEjU6rH zk3=FjzGFkXZYkw-bBSK$^t#CLC@VjI%tXXluNj+Pe5i$k9a4pUM)NQF(i62vx- zchz4EY0@QnQ=OfifoUZ?w@60z5_%frf@#&rWK(zfr(F=OV27~Q7#-yM!__dXaAClb zx`Z;26MaMQdUjS-RdwX4IYmsW2SrBv`;oi;^7i(={#MQYLtQ?X#ZPRse|jXL%T%eq zR2}L`N4{R*a4%ba8Oz}@?rl8ab18gdrB0pou2Gc^l)fy$)m1F^-*EJ|Ql2M@pjV`) zyq2DZk?}QMczL)jED)`%uhY=6;Q9ER_Dmzu=Z|gcP$ZDj(Frmq1yEe@udJj?jV6fr zT)KJ!<17h($_H6B#!r@P+WosE<))uR*(LmsL$_!>;Wz(gbcUt83e1{}zF~$_Oy6s*3M~wUl z0Et8Ne!|N1V*blxIRAfol{(|HN!*nrB_(!8%IjNKONZwd7UZD~0qk#D*25jScMi)l z-WNNq)Aoyhq(L!!w7{Ma={av-Q_CV$P^ki0Y=O_TO|{Qbx85?bv3eYP;yTs@|9R86 z@WH`>-O=`Y<v1{m?fY3BNnK zW=FysokIJ(J-^U7gf!AcP=cP}+5DCwYH^KqpnptU`{Z`rMVD={XEWn?m)?CUD(cE} zCKrMSJj9Q1FP7_?^2k%)u<2?)9e;U6j8%Sjv1_RzfTHY#)VtEo#ijh57-L~!0aks8 zWaK1~i^mBIhjMtI)2wzfpW_3%;=o9H*1d-X@|>f4kqdv<{d|3|rZ=#ezIw{;hU5Rn z%#VmOYu)J+d3H@r65zh@*?kpeVEiX8DK#)SC}FiVt0XwTOLJ;{jzvN0?R zeXSITHDCPMH3T`A!RGGT-{ntj#tGYw+r<;e$P9{vrH0SL;hb%i?tld^uda*FugfVF zoQp0T9J~dj^!V>Q5`=4p*+^~!=6@};!~a*`-pYU$q=pjy{xv)O!N!Q>h2y1^g_U|C z34|0;a4a)oX#LjpzDRik9i7IojZ{O9crZJEiZ8s!Mhej9t5<{oS^>qtef_qm?7Jv_hXMpHbInIim$Tc7A>ftbGpyJSy7SFrwSi zBl6C9c$#4REmidHgmGnLqu_I^d%OxK%E1tfh6B z#x$awobkeJ*8q?-H2i0f-(U3g!$gQ;eYOMMb{s*scA*e{DI-G%B~tqi$I0mlm|=s{ z1_6L>;>KLoxw$b30hNwR^FR8wp$W4)EP_ta(D!^lDNv?(6mX;h+X!kFYis-OR>}-a z=N+A36*!chlY9(b+1Mye%)h;mn^levO#&|NQQOE9H{xErw|{s@hoWUp7GKf-sa>z91KAgcb46`@z3{$EZaa$d^Qd7~Z54>f6oR&sI zmjSf!w#fQZ?jGS?^wGAj3!mHBlN<9H*JEQjMIhi-E;+g`+A5+ZL{c(ysw0nwnW?y} z_Uc&5A-J%oDq_69as@EXgoO3Ot;)EEoThWDtL@pZ=-n1FeC!R;!ZNZ!0g~?VBb;7> zxnkTJP-V}KB2vmu%PkKT#*woN3tbT=9laLL#C!Y?ShPRnPal;!5cc&?a2%b^EcKL> zaCI;J)%=*V=X3~g{6N2F8kbMe3jL+_2#@ct7woHt;<{Ck^L#M^c1>QI=eO(?O(?pu zb8;Ix#uK*-ZUTw`eDSopJey7x7;w033vzp-#|?Io_2^KD$T@3Ln-JRV$i z;^pO?`BvBa!$C0CT|_@{eANRg2VF=6TRh8|>j-b;$r!K8L}x(9lrcC-<23 zi#oq==&qJg4P<3bFCV`3JMAo`P3*te>Aa27t_T`!O2o+A;`)fQ6Kl zdVVcMeHtaNzi7iRf7wrgfsMvIul`9D`2OeTG-p$o$@4LSqTzvJh1IWPK+=h;WDq@8bF`Adc#mY(v zD3V?>xw`cvVMIjBI>L@UmPsTjDR+DlQe>4^YW^}AOY$B7L9zK~czFE0n4Df8;L0}( zjDizFqkvMihLJZkHu^?0X&^l|>AqA}V%O>`Iyi9P;b6&DyZDQ~c&>EEJrOZ!JfNuV z_=*s7?8?N%1nQv9HP;mS-KNE(Dh>iF?@~ofi`{+|o8f2KlBH8`E^NC|pj|=n5c3JI z^XlJ`3=!=h%mhXZa}bh>ZyRC1K~c`=5%1hFi|JI$~p%a@n8D1RDr zXzfcRTAjR>Ob>_LNcN&Df%}%CBGh6XBIRdfZz2BY*B|4#xe>Vw^rUe6`W=lZ%fG7{ zXNl5et>Pvpn~6@fTB@Di%E-+NCZ*8&l$Gi{KHqJ6N*j4l;!=VdBF*pT_(Q?+dL?b4mzTbdnosle8%kx+ z0Up(dxA#hbOFSMb+Nz?fl+z6dpQuj-RMpjI{{EPa(b+QW8e7vUK5(zQ7%3Na%&Lbp z92|lj7xfz3xrYnw2TZ>h%^xk6*LB2rP#S{x<_}#*@@Li)eliO$^*4{y{K3fyQusRr z1nnoG18 zrrY3q3Q>N-YfIgxdOjy+t9)S!%XK+iij@ST+@&Pk?SuvjHW!=9SlnmR7RMAYq@P~h zdNS*<+GG#%9?;mDAw_xxa9idh;4>ylW*<eXn|`vq~9Zk6m0(@A!{wdkvIEDh=Dfx@@|427rhX;y?J}@3EsQ{HJUZGb z%@;8}PAQ*6sg+HfVql;zw$IruZCNrw%`!v9zx>dj$P^Y9v_E7raIhAduUcQno4+6NvVyWdA3Y{28C$h_(5D+cLukbnyag80VK&e zM5O+r?BBf{&CS_s0jQRe)E;~XAn9F-hti6+$jNzod&g3X0O2Hk0k-GXlS{|_&o1`u zvRiH0h}q(9^YRJka4!_lL)r`R4KPLqMlzwX2q8!TVS?xG!}}7Tg8`i>)e(k_x6nL4 zNUG!=yXF(7pdjYe_XD%1!^=D8I}V2WC_df-@gq-!AZWI`9EFXo zzH%SuI+SHlrE25)3y$s`bf)^??k=(tCuPl!+#Mf`e8UReidjKte)0e$88mSt{<#wX zlvLSRX5-`jX0xQ^hs3@8eSOkCg>wfQ#D2cqt7y$uDmQHM4)pF4j47ZEle@1FFeFt#&mA< zjwmU*X))AYcvn}BF;Ergp{MjR<8IW^vnIcENjp9Itdh+ksDB_D~cNlTJXogB;M1qWq2GQxkW?9HS3{m+9WYOQ9y(`|@L_kd3dA!R94!SU* zVtI_$*yy&p@7|qM$Mzi`RMOd`8L~^bxV0o@EG#UV1{#BZ|858<88xP3TH|`1c2`_n z;w~wvqPRGXdom=&p2iP1?U}W6W8iDPc{3jf)J8BY1WE`H1wr38xXJcx^9}X;0 zSJ!L60|0zotgZK-K%Uh;6&FoN>DmyuwFmVX z{v00<_}C&@P3;GrlDkAiie_fS0G89x(sFt&G6&(@Vs7ju*>eXjg0wWlSA;PjAz}6P zBE!s@zn$0e=$*AmK(aQbj~9kqwd!AEnsR!UY={8?nvEnB_y`~#FAnezEp0PV%cOcv zJhulPaLk}*^qP%idvEWT@`hPCXGWVbR1@Jq>*LiR1rU{K&CiReR(po4=X5zkZY8b7t1VK$Eg-Cxwy zKm~vFRLEBiJU>@gWxQWdAQW_Z4(<7RY&~>H?}qpJef$^^74;!2iynqpO<^aD>!iD< ziNC}*dU|(!?J-Tr&(F_8=;miUNsV;zVWWD)!xB~;A3tB`nW3240CXXc8qFIl>`Z({ z89CZiC*<)h_n~JU7~T~Dfr=wj5wX~*1rnQgAKY!|H|dDyM6kfxv!<-G-Twj0l}cFn z{Yb?vH%d~{)-mTb&>j4eTN;fNIVm?e>Tk=WY#Z)D%9q<*p4T4kUg5_1VK!+(h2___ zzge|sl~jn$>n~;GX3Dxq0|NugYR+ApU3Noly+K2#>BN?W%0|6nqjjAlzO!b}7in%u zeE#{EBP6Nva1?Qox?%CS&JM@_0~X~8KSav2U4Mi*al(wn_lUCso~h1SjAB5I+xkr^ zDyv>|#Djk8;=smuV8|!K*$CS8F;we#668mcU&o{wDfYy5fAGHr(}?ArPLrdf(!iL3=Ae_R zx5L98R~$Mug=9X2ZoFe6H^9b2BsV^s};RzJzR0 z`fyo%;Q6@Y-Mq?8Lq|6Uw*`_*pUw5y-^2Of)A0#6!&z zS8Tu-z`a!Q_>snr*r)8a$u4sAPYjFimg_M?!K`7URw)E8M4s#0uv}Kx0r(UZ>n}v? z7GjSKLVmq?(GuiJ^~!fUcw2maRnAdOAoRX<(o~9~;&}97`cvAAxsD>prY}hb*?C{G z%CUe!hC=xk3l9$u?yPp8zjT-`{?3X2+uHOp5dDXHQ0C0PV(h#X>jE7}vrS|WQ@NY> zy072FwZh|zDr10*S`}Yxax?HIS~q@pa7UcQ5o4cav0%d=RlS^H1=n3&nP_U9=KIqe zs=7CT3XO$@RZXtoe+TXMSDNMW<;AqLd(D50coJ#h>oZclufHSu$v$50MyDzEh5wy7 zk>qO^Fl58hZuUoZ8;*ybg|iU7X>V`JDd~$BsVsxOd{ea3=6!e<91rIo0w4Gne#yju zv-npMGVbcXfAe22|1`LKpXeus=U4xqy>TY!)*0+Sa9Lb5=NMsr$BO9E_Fh!C^j_P5 zX}mR&x7IxztO=nSJNw-NJ=&(FZetFP-eFk-TcR7j4R7})vehi2@EDaw^6nZCa}vwn z*t=1?L2`M7k%~7#L-%p~ubt1`TA2CV)ZB#`y7o7puT&;fh2aG{R^bYi1*J09)smN2 zfhRBSC*?hYy;3$~233pB1zOhQJEcoB+-$W$-e|V`WX9bWxp~;lB z4KQnBQ@vWGUHXQBvKGzZb2iyyt$~e-=jG#bXtgFiZnX7oPxVIMiec3=OZ-~|fqF3z zeR0KQ%0765@$v$BN@vQ=_zd7$f;3W!@o7MQ(cyp#v(I>WZA&`Rs#Kn(iD_sHH?Cd(xn!V4&|B-b>4>IGG6!fd&zo}WzulSo9q(0`|p9J$Xb6o zh&*m-JR~H%|3XIx3(=i2KN83Z1Juy)LuzLfosUEnFXSY;%`YG-PP=uiHN3Jj5pu

8Onk$sF$7G@hF8Fx+hQh-p+B!%Rjw#%q{FKAL?b2sxz>O8os&Hm&AMkbxjdN+G$eNS-RsFkXwVX1|+)yDSGyeLp%1h$5lY)dJA{D9dThP{~%x&fo#2VN+eyl%3 z#rg$&+@26ISg6GpDU|1EccDO&BsPAmUs{7Hc6n=|>9Z4cT`rOwuBW-yX9sQQ1uONi!5vv= zSVb6~du=S^YDD|JL)D`-=0b`>=Yo`0<2KbKl3$Mp3{q$3H>x2tsfttNs9p5N-JdueT5Yf>4Tg@qJ!V#UPNPH{hR+jAGfc>VZ=m1cd|@84`uGPLLC zn8wK+CgW16Uj|oQ_ri1Gs~|@(_x2BW4-p15eC%OrOff|p^T~3u(lo*>ABW2DQZ8CW zpu<+RT=sC4tLEZksqt6Z^gc5-OzT|h8dP^cRJ0la2}$QL#e}H$3wEv1N~;Qc+$rHH zL!MVUIz8iCHtCsp4Xa1OIajRfH>fU`W{BNj%n`!)>)h6wn5B*Yy`o~?SM*rMt@!Yi zc2p!{#AW8@IVNUjLVR~jzD?QJcmc!>k+Z3Enx31Ie86OCzOA7zmD@S!fXjcPwLYa8QNV3Y{2$1j0XVJgo$KJ;|KD zsTa%ki`^+NKy}6AkkS<0kiNHfz+u}h=Y7)HN4?~LEW=2BFk17l+?um_A5AKoD+PsV_zYRpr;m#-X| zz|w*6>;T^fmYJpKxH@#=9EYttlZBFZf*9X!@cqmlKqxatgEW#hxz9_xJLT5eiso+j zs}IRQCi7!K!5tpkDr|asshpC_U|CTUb#di!eSzMQiT&<&k)Bgq1mV0^g&`g;u6)gL zcR*RrhPGW783ExOaj%8jj?YE+nr&TALt`tz-^O1v<`x&*k84C>KXN_mLsKzQg_KWC zw%A^5--9nU5k7vRtFl=iM~-|xUz@e}srk~UTLxbS_>Hj(I#;QX5A*>w8R+O0d| z!ecMQ#0U%7l{-@fB4dU?(U7n0uzuH>qyJS~XZtKs{Rs%=_|nEhWns`HDOKdL^|p(XQy@MPumD)hb`dsp_v z{Q|DCSxWNb-19uzB3B>{yz$h^#vFDMfgg}jPWPnL z1`reX0=eC|bkky|(~%X`zP5FFS&NTj_Rofgsz(Yg_+d{v?zy*TzyNzNvo;%YkSW%_ zt7f#Kig$O6bPF|?W2=)e0?Ji)3CP4HOzxYQM91_bbeZDd1SvC;-^9ivD;zA0R#Sb= zV_>%Otj`+%_U+GutJ^3PoiNW>M7JTEA)6-FG%jvvQd?))S^nVa6Gn#Ch8ORPcMV^0 z|Cs==dwdCCL-5Od7eYd z_7fTP_m13d_M;`*XrgtII<&^ z9`%#;flj}961(DBD!S>5HoUOHw%4Z7(S-Dzpq2i5H#705+sL4?wY92p%8v8j@Zfn2 zH>tkhn_tZt)r78|X0`kASDABG$*chw$%I4KoQ+jqhEDSRLr4LNzdAbt$V|W<^RdNr z3T-C@bQWB0`xH*4TLU{M@e3Y`8{l=T5B(gSgoX(F1_dII^wMq3Fen7L@BIhVscqurxvGr0l7HDrLX+A%GwXdY zo;}m}pK-%Mw>13(gNus`CM6^sR0Oe6>_6(gxnZJ|hFU0^J153*Y2%qAI!53oF816= z;DCDiq_IhA`PdK)m_wKRS72oPy$8`iFo4wXwR;QKgQsz_+7<+tQ_!#i3L!&YSzpck zQ4>*9LlqG@FD)%&cw8j$m5!dxXOr2c&YZm5MqANYo*c5yoV%`JWCGG=FWClE2;x)N zL%*+TTda|oS#BA^f%{b=HS3%n!%JFa##itsT~M=nt6I#}81b z#lC>@h$!L!s4uWlh0F#;ckuC_;Zg7_ILjxSOh0{Ut@r&W9nDcT7>^ooD- za8L>sHSx3>XaW^jZoPme9CD=c6vNBw95TCN&lSjdNC9Ho$Q zTp#=GM;H;=+qr*C}%iem8DGO7+o+!l5P+WPV zuxRw3=@jZ+D?HdI1usIeE^{+_#pH!@AJ?GuB*70?&$S9Y**jvGpJ%9``VJ<~&SDj{ zC9hmP8W*`}i|=#yLAf*7v*+`{lwT1F|%bKO(V*KOI!xCLAIV*&cuygXUTkIq!%n=30B zm6g$;WOey^X~zQ?gL2(X#^-ZCi`t;OtWfR!D=ZM3_cdaAI2^Z#W;;?UH9X&PIOboL zc6B**P5*@$+ z;gde0qyztRjCcR*7*J>DgN8?Z2o%f}>$lyLH@*Z{zajaJ{m<^@vE`cX6M`DxjT%K*dLS`I*gn?C(XM13pkIq^GjOT6gD0ZCG6j%gL#Sm4;r_ z)aO<+u9wogv0b+%)34ONu>DW@|4kXpfByYWv90&j<^W&0{G~$h|1-Sg{}xnpguWW0 V6ZnK)$P)v8q$L!@^F$3l{123i``Z8j literal 0 HcmV?d00001 diff --git a/documentation/changelog/0.116.0/simplified-pr-header-buttons.png b/documentation/changelog/0.116.0/simplified-pr-header-buttons.png new file mode 100644 index 0000000000000000000000000000000000000000..ad3d578f277ac2e7f6f0dac1bdeeb5a6f21139f5 GIT binary patch literal 78791 zcmdqJWl)@57bQx91}6~QNsvH*;7$n8KnNb(3GVLh!3pjT!QCAK1PJc#?(Q&$_xt9Z z`7>4X=iXa&yDA|ax|{QyXP>>-UTbZBNK1;KAmSrJK|!I2i3-U=LBWPXLB05k00;gj z!8srie0gCbEAkntWRP$NJb^I~kPv`^Di24x*M|kZC{;J&_R3Yf>*LI~?VT0+*xa-;&{D^as{rhwgFKyXku!rP!00GLgNfeL+p#C>(hSRq&qncs}hZoeokNL5fhfvpE-; zQ2p|KH6jiJmd;&LX}ivj95yTzU0_(d`qg}R+b7Jw`pkT@Cwzk&o7BN0daGMo;XP@t z_yX-@P@buW#7);7n$mJ|#dKUp#j33sNwua?8e5ym-Iui*aWM&jc`euSLOs7df^e5^v%JA)& zTQdtaDI@xn`MmdUzu@JyHRzZxurgR93ufJi;SQ^RcjWqN{~Gb68s)~9J{;sTO-#wM zG+JhV93hMWSC!DO_$h1RQ;w{*l{I3?Oqq-_GF1A6vDKPHZM!a_Hz~sL$w~XB*%TRL zTQ=PnA>!lp|1Rs~)ej|y0k~}xX9RCg*pZLZ&JUq|IzmE~{-g-gO?m_^kAEGWx6R$% zT{{1kAq!T(b7UJ@liy=wF3>RXZ_gv!h;2fgz;}x*3{8(s?(6WhCzac)E(Q;16|*Np z)y(>~o+&i^P>AyfgqN9F-*xrioSd9GwTAm+ZyfJP$=`pe2?^v`>$T!gEaOdBx_5m@ zS2)l6EGVeWa{DJ117iTrn{;_o^{`ETkL_3`($GIK{J|`9hl#spjXGeI!deyN936Smj92C48xRR@B-rb=~*2 z_dOSkV?LM|8hXymzf*+>u@KNnsQT;SC5-;jfig5P>w1*BTDjSJ40Kp@hwkbUE>r$c zafLB*p78GiXv9ae30r)SH5NsshN3@EBn}KF^Z7B)}(wPT@bLX>ay0epltXsDoPvrsbee~M%OW56EOGgm>4yF^xnpSF?lK>r zF+K#6A_NADw?U)v23fdo-35lMk15C*QC`&k*i3Z7VL^R`BKXzRqr*LCx{kw$GzKlFBHAJ{OZio{p+X8ERXh5_s2?_q^jM%!qRp$ z_j6{``KC8y8QRi?6Mf`DHvV}%E8prbE*p;rj@(HUHQZ3{p^eg z>gmlD?=j;l=IacHdZT4j5=~9l1A?V)Oewcly)@#>ddCXb8Dea5w{t=^BTbFus#P3# z5E4x&&rWTT?}E)$^ut)b>+9d*Muk{r#*I;61f=OHGikCX{@$;dv$=|plPB`Ky1D9R zg$$&m7#bV@jLE1M$&nR!7vdou{6)HVGyxrBK>w_Obt5vZubw6i$e0#{T$Q^acSf+_$|SN&uZSh}_x5Sw>ymPZ0hxD?1wjs?1cG(!73+ z-jMY{;IyC*k>_fQpPJ9^wB9@pDoX73&Sx&Cs0x=Vj7V5d&+oLjQ>E6Og3V%vvUzZT zGxWfs85MaR;m;qnunChAAyLD?Dm1!SzVXS))wV~sI>!uY=Th1f$#7OhHNTLMuQOLw zO1Hz)!+}v$;bdFqG}8z68>$P~n3%zD3MWbF$zJgBwTv#I3uDCcA#ridTJkZVQH7BR z7#IYc9#EW~o%O6q@r9-@qeFd?azN8?U19i@pRa%o+db`%nNc)$c)EThHNlqr)u%`Q zwgSLANJoFaYU10@70dM6S{w<9P!5~hx7Ie+{(rBzCm+8RPSl+i*nze4t;g?fn~G?d z)1wm2Gl=)B<;TY>6{%EWpS%(SxjwR`wQDrsvZi;Y+nZENdsL@Lr5QU?MurjJE!)*Q zlR9%~G$3|UA1isN^JFbxWNabpKG14H4_tNtu^XY4)vHmafKIQWtE0|5m#aU0d>37% zCy(57R+syD$_WDB4{Aq{Yxu=ubIJZ-A*X0iRc}4-LL-;7nk`08I59*{ zE^EKJWN8F5HPr^PI>;Bld&DtwVOO=pf;q%&T#b*XOdC<{`tGy0V9p~H9N*l$8N|tm zMlywC^Rj+YJ<2I0WV}Y%Orff}igfEM8!8Fc^2z1NZ^Kq9P$}6LZE0V>#{OJ5CV(Ye z#lpT46ZgcoUT4cXJz1<#iH(%`efhvFnrP{H593_msH6x~j;2Lw^$`1}FLv*@ zki&>LSxOGVjHYX|63h<9(|1}l@PAYc*7v8hmX7U|TR$gc36fFDxdZ$2^Yipu*>_GiMk6bP|4w-byn9nVynm1kU_EXb-kijO3Iki!lQ#rocdi0*G!EOs}`E5v<)ygva zaT|5xNG|mVvIa_6(MDlJ!&{X0b}~j>1 zWsKm8Q*Sh){oL6Bx-*&ICs_e9td_&)=e87-Y{VHE4}0J`Z=LTfGZ-~5@lrvNm~Oh@ znztKz5LK(QBt2f4AABE!=-m851}PKl8U80M8D-_L@ps}F*u-Qpv>#|7!cGb2&Mdt& z`Ht6-RFUM4OT?g})l0I5vYHEV~$(5L#JnQ(F zLQ1Qvfc5z7D((QfTE)lH6DpgbU&v4iH;7&%!p>f?xc=o8|24u-IoSYLscIEyet!P9 zdD%JuqV;_V9$8rUQ0yRL{pCHe`@Wc()dUwf{q6nB@9qe^Nk3(cv>BcYsJXtJRI_>m z;!H+G1-WqI{aw+@_63}-ez9k5Ed~?-8WA>>1Su&geVJ|4bC#L0bYf8xMh^97S9#6^ zlK$;n9!G9y6d&C1o@0II*2lvKE?mmv3F<9X9yvWdabBfVAD=g>%1r32k)*jrM(Q(+nXz&{yLKnNMrk5qG}3 za8m6>isYV^fi!?1$}aDS9k)Lgsa3)FD=D|>tJp%4m_Ay#~prshy3bRr+U{t9VXSDDL%f3i-p4P*^u zoq{6{`3t-3C6=*4W`HMm?e?sme8S1VzT7`R>pr|dcCCqPM*{t}=+GY_Rq_5nJ4YVM zJzTtEGr7WXkTN6c7+>`>RZ6SM`P9@rdXDN6>|AB%7%XQ5kO(>oW!u__qr)Ibuc(<1 z2KG4F+R_obZhw5fp#Y@x4ZiZ}sZC2G!KVDs9TZcjRjwlAmjC7EW9Ju1v91R+KJU<^ zT5>)zZPB|IaV-i$LT?pHKqR$&!csq{6WuUlS&f}LYzt*y3X6}QCfm}i{({25VDY<) z>dzoAg<1cKZ-5B$oXEw+MW-A1EC46ThjqOnYeh@OG5DY|iApLzw-eH~C*DP9302g1 z;XIeHy1MiMX=!QqjzmRrKae6LBmEM0jl=iGza6SN{Ef9Y9xj`Lm2+)o!PiT%nKJZP znlt=8P`mhp`>+W<_@|t|Srmv6TP`ARAHgy@3Z#C;qR$2f@c0l8wrI%gk=F}Kp3~oQ zqT-sAg~8y)$Hc@?xEnZx+VY?gXC<*&2>vPS@xu1ZsdAgFwtckxEuBuEX&B1=KHAd|Nxa3|lRz zq{d10$`_H5NZC_4U0ob@qfTSrt6F}GQdYLIjDFmv*CVo}qQoR33U)bkgPK}$r2UEg zf%x(c$$NOI#@CnJc85KIg_KX_vI`Gea`g+i-2B8RmW zcwzyqbffX3%`e3Y6n0|xE%z}7x))A-i67<|$(ax}@B7#-FB(vW32sFO>N;7axnk|6 zOxuVYmf%U!%J&LJwtbQ;>Y?$J`(p|rRUDjeq&#-^M8AHuHsLm@R#9^ssIJ>;3CNE8 z{QU%kT2?r}-=0NNSMkzEQKVfR9jKt7PB6c(NrI(Itu}3-v^C9Y(Ta2%JKKu*EO*@Y+S>RW;7n(={m=a z0vb%P6$IuD!ilt<>L!yKKSYS1I^ym0BfwG#%$s1R7A()=H{$3ddKRwu0wadYKK7%^ z7Eb(8X?b0{U==ra^dm5&V@&fFKUR+}&KofcZ>Zkj?Q?Km{+3g!vpoNjfqAGi$=Ne8*B#2Nc^FntK`W%R^Jr5P1z_|?S9xgd?_8v z$S=oWWsTF_=*}G5v3e2=CuLJu?A_lx5Y%pWO6IUazT=zm%M8XDo|;3_1*{;w@aL{g(4^byCf8L1AemD=umPbX>uEtJ`t0{kcX0P%SdEnxVOxi9upPhk5b% z_yPf$rG5JVI;P>N`EY&$9}TboR|`*!I%}&r4*<3&;uBb&UR&)y=uVX_VL&xCHDO>9 z1UEI!V;lw19Qjw*)R5ZTe1U;W)T*5W1_cK}G#|ydLSCzUw{2*eg&ehA0{0o(^rHG`gw3qoiC3Ydng9H8WKp_#z%` zwf?7FdyWa2P>PH!1 za;30z2;Ok>~M@5SJ&M=2I%$# zShwn@ae3u-!-1aX`uZBI8uczgFQ@AdXUYd6DLy_vgjcU(CQ0~gp}6@S9sXD;%35Qv z6`sNEI@VXBQQmP&+PTR;<|a6%)^F5sm+KnM25u=&*^g|mK^H3^1#mbM5r_XErs%9T z6G0_v1NSnElpjqY7}m>6pyD8FYzLi~7$9%XHFqKRw|z@;prs%vS@2Cy*V2>Gie7WQ z36ifnyiH0c_~l}`Lqg0`NAesxF)W5+@ZM91<8E`mI9Q ztnQn-pN)Zy<@WLxhpiSrKx(+#&~RU9=3oRqSz1#6T~Pm(Pu#pjfUDK6AGHyP2v0f*fw$L0Pbg45PuW@#;s`{Sc*qo)_w8+5~t z8ykZaOOEV~&V*gJy^@(>$rUDbhWf1P>NhuAXAy3v9M91dn@jn->z(M77%S_9<3(*x|``-A)=qR(U8?9ng`9QSUcRfAW8uLa0W0LOKG#?gx6mXR1cjKhGSaYmf&S^E%!Y zvs)gY4e1{7r?|aFLAmI>WY24mkU4BTNu8S4u0AKz>y_O}&Kfh8mXV>)Nx~D~{h1>w zJ`^#Ve|yWdzi?Jwq)uGI@NxDkW&d&1vM0S_i5q|8#=Oo%KoPK#fBve_95{exwP6-N zd5Ak;ttVX6EOzQEa8eC7I<7Cv7;VN#}or*e1!X@SBqXIGp(fWL&StqaDX06<;e++5y>OBDf( z+BeMt0DGvOQyd`rKsNFftfBxdvP!Y!Cs`ILIWYY!b=5Yk*PwMI)G(0x+TrYR1 zsA*{hTvQ1_Dv(xC36Y};%P6XX`SC-H(b1{BXG(Dwp*lPug8TQ$~DBM1a4W&GQAf&Zgk?HwrfMKL3HCB@zIyw-4FG=Q2xbut7WUtL2+t zr9p*^ys}qzS8xvG8#)XOtRE>K9|kzJmdHG*kKHCupM;%g0An#SKJ9sbw)Nade6+F} z?NvaBxxT&4$gV?BSMN+qhSD0_ELJLa{_u^S>Q}|+>!$0gt>(ihW3#-4*9>-w!PMYGAMx@o71IXWoD9 z4LD-}wIWB+h^pvUD_gA(3pCxYo^gPLDWpJ%2EoTrE~#`QFQsyonB6VIGPCnvvmiN5-*{o-^mg~)oIhQYa~ zKgEqPVabYXxA2r5psKH~pUF#_?J=unA6xaRmC6jRL>yi22u7Xkj~h4A_yaoO`9;jk zSf5j4W|8f0E*@e`P6)(!uAt!k$jr2ulCy*&rYZpMsLd zW@H!g3yWiB57X00K|}P#j^!-}mM{*Ii0i$_ab{(09y~n%6LM+O{Y%Ldo-hDtSXkLa zd)jWA)Yj96x;nCT9bK;AWkAc}>UwX`hpD^EEwWnN|Y%VUmyE8l&|?XguXC6D**SlKXjsaZ;a{K~?w!PN_;_lGVv)GRahqu;pcRUu07osN;{f=_MuD z5}_y+N1Mi6m&J|x&fKhq4W)7OdbM*S6B|5B0MC?hu*w(sm=zaKKh5yXaOT^@pUDE^ z>5At#qDq}HqTg<)B)sflU`kpkp5fq`hy$GJ6-lA6a9~hatb5w>CJNDpyM>vVu&WL^ zKusfaW1-OSe4Trx&Az;ie4i!l*{W2C8o0Qn5rD$*pz!U_QaOHm`xfu%0@{;E+SO90}}ydYFtj}(AL^a+r4 zrmbk3#yG+l>gwvF){P5cA%E$1n)IuuE%*NjjE{foUtF4)U^Q)XIxk#AvH;=qCJOWr zf{;x_sJ7lUk&{0D$1sWjw9IXtj3TpoX(i9GJgMGWac|PLsU4IR- zxGkFbR@db+sdb+6?vC4Cj@k^8?TGCEVG>@0_ zX*`m;{2Dtuc%tfw^kz7f^+*;?mrR3ODRS5`&qXGj|519D(wmf&^~1n{$f7M5uc-p9 zy<3xD;<;2lV5sjd4`XxQeN6bunD7-)!+Da#Jyb6CC@xG#`}=JrDI^`Hhd*PT2tq|g zMWbVEV!(psO-iF-xow70yMq~c*mqMD!0z)l+Usn=ANB5vI_hGTG&!df?W?h?)~xtT zBbhM^+h_@5}aT`Uc z-nqEq*LqN4$>*;jXI9KTZNF_ST5o0p%_!k6?{1oG9B;a|Mn*eT? z421DC+-{CKHye{sfBZnwV~4V_mJCl+)x;7e~{_-6WrrwQ_M0h+B?%Q(%9R%D$jfb8knJw$D#?>mOi`FUUfltfzdjsqmc;&@= zY)L1Bm+Ue!E6Bb#@G#E}h%%?5LK2FBl);*Z`xU|dlj+f~t)$#sO0czZ_L9gf3E4ON z{!x+DYL{<)fyyz6wn%U>uU|WNa>#A5a`tcsL58U}vGh(C4B#U<(P_xff5pA{MCE+v zoTCTHLhGO)_wvd^S#sa2)^05YtbDkAQ#hcDjS>{{Iy?NtDy&O#O_su@zMAA&;+4eZ zCIPMbuscZ?8vgGlebZ?;j@AD5igxWN;dRp+y&Y-?)f zVMQT%o1Z;;$%3fgH+VkXER>ep^xRQQq9Bil1C=wNgx-MrU~MfpUTKIbEBD3-z^$k# zIXzC*zAtx25omc;*$5I6gJGGA%YJ@h!Fqa-O6v<1+;g}cJ0~Z+OOG0h@xk_VP&GK1 zJLrZeloMmahRZB>>=YGo!1?+<4*4!^qKahi(PZ-np}9AA!npjh&rVikNxxDXAOQ1F zI1ZQ1D!S`~Ba%Rn19bHuauI0(;N$wbn4a3YmPAWM#OLc>tQyvK#hbuHU4ca&7zre(<1px z$z7*$WoCEikFSjA(NH1s`hez8FgIn;%uUE?VvigI7t{G z7d4VSounk*$0wb~8ygTq4)eS4FF#q0z_K(TlQQC-2ZrFbxEocg&}p`)TTKwQqYOgvC;t3Nu2z6Q>uzsvuXWYK4Out@+9owkEWDyfzZaMKojtj} zEVrNbt+gC5JuHA@jDEzl{l~ac2fVmvzYt}YEvejKN>Z7WkC7VJs2_rw`#!e?XOnQ27>VqbIJGPv&(I?4MJTV@8IJ~@rGQ|P{ zl(n02VqcXIH2JqUn=@LlFNh_#i&=1P#qFwDryn;9DNw_t3kr+d-^EcORpA%gGRGLw z*>D*W;%a?fCEwbHD(vzO#zE54Q&TQhz`{|F#zzrAkNEj>SK$*45TNaAy8&r49Vtk$ zxlA6)?23<$PnOONF48iTBwg$1!pWQ|2b02zIsHWH)ZTn0{)Hp~Xzqt6 zVmHg}LwXLc086DCemSI9sYj5TXV6C(LCrvG)p)i~BNdkCea~}V&*PmvwU^!F5EDx; z5$Em$s`9{q)m<-)Uf%Ei<2_* ztgNe{iW(4;xnrYO^zcBy$Rfb>962AKTKNkIyq~S&!~-+A871zbL8+0^Sn8kB`kmSU z9mKzs|8T&TgaX=nTazb&5zWdO(`WEMw?(g~HMKN^CLnOCcC)5etuit$&CslnKYom# zF;uuelrW$l4tXDx8Hm{vE@>G3OAss4<1ty)Z7&lE){=UV3I`EUdJ9sPQBd$X*1TxTRZtev3w;dCAsWs9y!tB7=BWx5hs2X=$Y`7ZZAv-XRd9=)Uw^ z%?t+kmKWz#mpQ4?@_w7F_RhMq8i@>uS13`6iZgIdqwuMJST}4N)y#tOwpgVmzA0vv zOgce#F0?!48|_G=Z0^*~ShDrp_)hC43_QG(q8=MT&P5`_?@va>XZWfD;}IvRA#Q@y z*AJ3UbWkw;pO>^RUU&&o8sSQ+nev3t6CHHtLa=apZ4N?mT1v3ZDAr}aF~P!8{N9k6 zUu@9;2$X`B8CwQR=CFHV=7&<1VL8jFDwZ%7_NLSL$D-mwuP!|nTWQNc)$j^iTif&o z=WXh!m8$CMK1sf2US4Vjg633YV|42K$n zBLFO`Jn=Iw;3mxt4-Ju=ygD6umnW5yd_FjkUNNdu8=e<-X+I+zt5PUD%!p&Bv8WMf zhetLxHs*7GmdYEn!R58RycSgA>vfJ>!9r1QkXw|+Ma|ge>q6qH6x*HNT#N#ErSA}! z5Q3y+PG)&;&!RVl5ae&ttvkO;DH4pf5*iv*W|l;iD{RLxelpMaA>EkQnn_4VFgC8! zjc>D5yn2)O*+iTfQACVr?fqL8WNmsH5{P&d8zJ!0=*N+IN|uTUU5|&N;_s|-($i~P zt}o^kd;pq{bOA0Y^Zlb@UCBzZ?*3F0smLM*;)2 zA)ho#{>d|QSW<%~Yh*#>^jKULoAptdWrGf%0X`boplMeDY4L7I=5eR3~|LI!JXy-)LbMaBYU=^f2Xo`b`Bv^ zj6aS7#zq!Vllrq27Z&uZYSGd&fJvot4hTt!Gv)Vk;EE*-uF!s_5x}tR9gdXvRqjsjAZA@1{_U z%mbP9OyT{JE_|Xl`Z5#eS1GBXh*uvdlR(3K;IQN)8Yj7;#b+p$IvoEcM_9Pxl7o(p z5RLctcXpU>n#n)43m`v0e-AGlG@SqsOaMHIGcpivE3BDQCiI2-agAuy>gQ@CazUKb zT3Gw}`?oLu8xU{!0Gl~txuDMi}K4L0xW&se8 zt+DdM6ISYakaRsF^#~2LvY42$#_Sym{DEW?d=ZE+ z8BfPd`@z8uF)M2qDaa5JX;d!RX6@eFACLAE)gUFDLRQmFT2VCA)Ch@Qc>FKYD=TgO zdS2U}om|B5YM2cZ98X7S{`R=PpE7^C8&yn;gzXW*Kgby0W%?MF$Frm5U|-c~PT-;w zM;cZ6$U$g7Ucc^s7R@w##HI6{9%KqeYHAp+xZ9c8Nj934q?PGn1r`_SAz>L?lWcKm z=`y-03te5T#{&a?5^K}2ON+Buvx*_`8T2roP#zxJ&B^^-+}yfR$`Wic=$2>2wY{nM zI{=Iya?dUF`LU~8+t^4KA*2Qbh%6&%-XlI656fIPjDJlG2Ad_O-g5m+uush|D*7~N zEID)x5wTG2^#z+Hv!_M8?v?RT@G-L^851j=y5VT%Oa1Rpmrn; z3K)BM;CutpdE2w|jsLdRqi5)Gsr2~5pvC8o#1%%dST)=aDGHsC@MW_AL;oJfYd{`c zH_W3~D}OLFxA!2Ch&?F=W7H;XA;oUZ>ssLa)+VRb*Xtk}h`4Bc_qZ)jS!v5l zPiL*bJa-R;#((>E(qh`TD_9Pl7~_@C;_~E8sv(CBn_+UpTOtjOK}IrRBge zKPbUUxM7IOU~xo~a&GXPFoydLqeIgZ`qpHzU^#NNnBYQHLVNivV+5QuHTji~$3~~v zbJ&})-@T*wz$>>)r~Coi_5LKFj?piCB{YCK=pR=264Hs}?a69V`rD!PskH84&;>m$0>P)7vjTSL8Y zhi~Z!`Gozfq0*_!2_63WJ<0+LN6SSD$?}RwTHEUSH{4LZC=HK5Mn#`)P&bv5lgYUa zGqLF`UCu`oTP`K8>*fc(y?Qx1%H(f09GIJA%@V*koIrkMMKH(!USE-fid_g>{9%pY zhIs<`(wa_Bk**DU?kaj@;dfA+>H3wz3|cODpLfb!V*Jb5^In2p@8q_S0g#q@qQk%k zOlmZu<9b57)Rra1)0Kp_#u(!j`2#3+K!ji~X+6Q75lZs`F6D-X25CTIR&vfvUyKA? z_oqGLY@H3#1iv0Z{y2#+ANh479X8P=_pC|bn=6JHy_=<+G8d}@k@-&y12;w(UAjmOr^p1Y{# zWC+4XVnxtUnw#tV$7*}0wRXu_;--9T7YKkX_3NUn_->X<&}bC)KPf*Yn`Tn4PLFh6 z50pNVEV^7BXmeMB6WPxcyJoU1AzR@qZ{4k(df+^1_guzC8R}e2U6>U>4 z0TP7F%FpoGbaqm)$jr}$6Xcvu`L=w_4tA@ne&u5!VLhh%3^~ARvRgRSpE}C|Kr3kK z?}v`uebOGU@2#Z>Y#LIxWs4s{1%jryigt_>0*ZYduCMWZdi_jrUhv^t~t^coPigrkEy7tW?L z&b&+&vHh`QsR*-CNLL4XlP1+*!{3FtC&ZMjG8S??(D5rM8ML?1D9tfr{yb9yQp z{#5;*l0GmrnBYW4YbsezB^3K{jc|w15K)#MKX$-4QetxJ@GkoP*?5*qdEjpiu?dy* ziHML^PPoY|E|$M-g%k=X#oNGXNhNsu=-ru;M`#02OOF=>LbIW?%p9B6&ljpKK~ObArClL!ns(6T9O*%(#!%%z>3odwj^ z)>@wwu@c?cmn0nSaFD{nCLD|sk^&BR#ZiRc7q52 zN~W`oMC+C+8hQB`Lvvfc7r(yBlrtKAJokSb#{&AVfsLg2Uhb^!MVy+2Z=GP?nBYOT zR@U}5E{q=sd{b3*t@v5psockS$ItH(@6AKL!u!#uR522)V*I({Dp~?u^UA&vQtv6;f?VN4Ep9$FmjU56CQdCUB z4i`7q@WJRu_S;*@Tf_OyP5l|&tVKSE5JO;WOadZjUS;`JlzLE`6~dw0@rp7AM|azI zw>2QGV z%qS)ML`h98ik-HM*4gmpQ0p=AN>2?McD4hzEMd`aj4*t(a`Y5j>C-pFQ1zrU{_sTf#Hu4 z8Hw=ff=(&=X9G-&`C!o8^fal9X~)zMPVi6c-oB7`Jml7WXDpfp1<|l<*i?CSb`Itx z`P2*|nx!@#etrQIWZD@;A>7e-MYVZb70IFu0W)PNNl64Fiz|dgD;pbrRaMB!2?-_5 z%@UW6EdmywX5Z#*$~>Zg+y2gIXRYPc@(&Ko*}?rB zVODKW%DI_1my?TJkEO4H(>BO!MasaLTu_QAB{(KgUX%hTAKk|O$LeDurT}a z3C^>1$!w1-9*tcS6Mr5i{xQ119~acpa8SLy`4u(UH8uLvCRaC>5}awX(LmFwAfWT1 zc@H$Y?4&1P?oKDwWE<9PsGrl^J=pWvwl)Js$zUko`6;yP{TZ0OqOKQl)n$Fp#m)d0(KT?^J)u=?+t4JQmSx$)%EVpqU-_|`e+JkNHy=_^=A>uz z!DIpu7AonNOaTX}+{43Z*PFeh#pQ4bbo(s|M&Ycrz--qy`ER17 zfTq}>Rgl;b6>|tr2??RAz4HP(r5-1>U}s0U&cu7=gc0EI5jZ78a#ss;Zq>C zof{YrK#12MlVz;As0=UcZNC#A+ca&!Omse+s8H4!$<)PdO0FOMM;Eh|QRtSt*|O2& zj1GZNjE)9`5t1TBJZL zNzDI#PP4rHr0!;NZhjFSmXgi%`3Kii%kAp&AsbFHqW!iYy zZS<-vI5QA!(FI3OFQ)>^!`0PWh1ty~GHU zT*wu-B!C|%Jo)@8#R_a4=$LAM8fsrb_LsDR8~bl{Cu4*4mcD@49hM zsfB@J7@L+BkSIP_WsrJl0qC|G4F{inN16{b9ey3I%Jm7NStD9&xy9xW3ZPvwZG5Dh zsc*<=L3N^l>B&1w95t?N*LO%@Xg4IZx7`tqc3QRr(*_+2dzyda*o6Nxwo~#7#Tgnq zk1jH_t~fs*;*?O?98U+7R%0_Gh-%~6t<*aN;Emza*JJ^_oZVijol6+a#y#iANcs1g zALUjj({)CLB0l1QY6A)B<$&Aj>ZPdwyP1QT`mrjv0pG5AP$m#4oF8TQI=9lZM9{MO z^%PW#B?Aa78c-C~>@EURPJvCC=YBg1czxkyumWuFMWVn(1HaWyH7Np^;_(BPTg%^Y zz>)gBzlOlFbD;Lw3{_YuH+lRO%<^3I*8~GI=N+-D?)ZVX*HR^Lp{L$VUX^VI6{e4H z8K9?z0!|M)D z5db&XWKr{L;HmbratQnYnzh^0CB1wxxV49UTmuIAArd5B+bDnm26N^W2;(g8ucF+B*t_m!P>1 zJht4@%`_|i06avIFV6tSS<(@p#H+<8##lOus2IC9 zz%JdB>WB}bq)dfw`_LXvaML?~u@)CM^k(45mE`F$?aA-x;h5^rvE}&gSLTH&@v#Bp z>eVP-l1|fD&AYm>)*oSET{tb*Z)WOCD=VSX4i}j7p$T(;HXd|zhV%aT1v{A337}p8 zsypAVuf~|2#LUD5nMonCGjwdJ8rV3?oWIbeviaq7CO7fyXN(eNeP4kY+jG#~pokI- zF0?>99TA~&pIWC(xkeeizZC~XkCb1^UJ{VpJt+dAaKa zvkDdokG3x|_VzE+-oZhrP@M0+1|0P6u5o~?y@&{Rl?ux0>g((ZphAIK^jTr@3k(d* zXY^Yso_z7Ka-%ag(>Z$zZ*LzmgqQE%_YF@=hfBvnTB@>@>=h?sI#)i)eTV)j7yF4F z9RU;OMTWMvQnm4i=c%FzBf!Fdd4^zMQh=+Sw}%Jg1Kuh*{xkD_a#Z2p1-=$lEpW}w zMY`?!t};k`1uFq!@!Jn-Zx`N87_x|dQ3;VZnItE-eqrlO^vxSFH(d)Tc8GhEn!&(Q z4U3mRt987a#%ot)hCvYL`CG`qeeSkFWj&4L@q{wx_!!}B9zEU^+wp&F8atGqr5xA? zlLsMb{kn!FLBT=j1SH{}UOxoMf->#$!GcF7aAN|a$cMe(b%}r2&V{6;*ungY&&V{t z_vd2S?_g*RCh;qn8bkn7hTRf#5fKrAK!}U6NSid{(5Pm8_6L6g(--5|+bD2g8bhnA zZvq%nz2a)w!4`|m*>d&KB{Tq7i#UnV{KTdb zpOZ(2@%8r?%#z6}vNg;@CpH(6x|}PJH;IU-Lob|C^8yHjjOjfh7?23eL?9s{c}w}B zV|PMvIak#}qk!Ao zEF-_sgvY6IcnGbCc@qXYi=MM2IEfx7{hd);Ao2=8Hm!?PY+#?`R|F$;5whc;>Crg= zCPrRPv{8ft?>9V}t3Qx+XSO7MeW_ z2k@W66RhWVf6wg^<5rxF$h#XQfEjZ*r|ELU7!X?@6^d|MNb2kOmw^r zWa9vTKQb~aG9ejXNLE*$yh-_CA_}l@;?<kLptc{UeW5ZDfsFmwne7ei=mMlE5n5t0=Fkv_I3J*sE8$X!Z3o%rh zjyID~G@#1-u7l3RcmwJ+0wU`3d=feq7IHx63kwShrV54STLO#r7{y|rcCF{}-Yz7+ z3Km?72Nbn#Hb4DbPE3}?GOOTydhkmTDERJd(M%P#3|mijclST{jlMf(I!`kM1riMO z`MZ7nmDxlH$G{-;Auv|*HL!LqZ}@>>MY{e!9bjY1z&IrsP?JVkPbtjL@4lSU6m7f^PnXw=%X=2)H($XW;61a)H5GTFH`lBCa4?uY_{6EVNb- z0gbyC7eV%oW>X;S$MWN=tGM03FU6m=(qDmJj{qi0WD6*?S5`*(IzN@2xoXfay6=U` z9R2R>jH05fL*dn~R$19Nc-N=|CKdxjLOLo!=)i@2R&wM3{z3nTyRmu{D!}Hw1Zhf0 z_(KHO^LU>HBPo-y*SD=WJzX+T&%h{!tP7(W(YdV$YR?#|j+G1sm zOa&C6mVhm?tJ_2NQjpi679hp5!srTrQHy9=FaQUYUbqBG4N5RJ71|ivc^tux<5@QS z)#YV+Nex_Zl$amB7!$muH8j|EIeXMo z_k9E~AHw9+|4lKyKUsWHOu17e1C}GupXVlLJKL8Iz&vhEc;`x6c~(IA(^zxGP~xwA zE(m0`cZndp_xip)>09~9s|qck>w(@244Zq?K-v?rxZey=CwH&G8*G^JD(atYiP!Dr@nsH=gH?>pE}v zXlGknTXS9g(m5kCvXPpq=^ZmGEBuitg2qM+ETYh&75ewUL2O{KoO5GarM5pjaJIO$ zOu6M6rCcHz9)U^xI2;t;I_*~}Z$iMgq_kh8DBvY(l)oCRnHK^pU`@@66ESDUL8Xk$ zN;4`FQs_~#Z6dnl7PEqet`9LHn) z)j|2_{*2!r789TTw>D(_TKiHiZqZ&M;9l_Z^7?Bz0&YN1ddV0ku;VZ_n`Vu-?j>Jo zCMsr3dt(v*=ygwzT;VV9--s<&dU8Zcik0yJru&GEtg>&9MO7B?O}a3334}*{|4svT zNUrG@iWo|CI6xxmP+kE_Fd@I=lxn7uB;%FKp_uXW@7~KJ14m!sotmT;B1lUR1IXbT zrl+TkcfOhVE<04wva|KSJ;nwAhp@Nt`~e#BDm>J+H&%}X7uSDFrGP{X{ ztnm(Q&g&+g7S%;2bddiF8&nS)w9(^CIykUJF&p?UF>D)G)l+NKt@8_9R@)m={kRyN9s38R&4RZRI@kJ^T(=9;X$o^}5c?It-1k{kg=U8?F?dBoZpve91 ztE_AKg%5ob9e%0<^ae=C_O4sA&xX&ds;d@k2XBx}Wzq=`gnYO_(e1v3z6Ug*OWY-( zm=9zWSR_o*mr*(3%a*j9qW?~DxeM^R&qz3R?-0;@ZfT2DOy9hFuU4!=%u`EN(sZ~K zm+s1(S1?rrY#-~JAMy;=f%VHf(9vLFa@|3TSsSf8inMF~_L3S@@)~9brgmKJuL;@R z*`z)KO}|+d%wBT^2{3!rpFB+m@n-7Fx$djIrk&H%Hg;`AoiAU$eT{ZOA;JLRulUP3 z-!p3`mni1=UZpBpHfi|-(MnneSWO0Rnw0i>{`i~R@I%(dgH3*;*tN)SAVc^m>#_{g zXHX)h;ZWI$Lm~X&?zJlRe)Y-wt9B;3(h)psUWh7gL3OSW?ffIE;C?bFq zB63V+?Hd;sRrn271Pb4y?D37EUlUVl2VP;-C`__uo1Rx5)9rb%6*4}3IP(w5GgB;B(TM%@VFCGb>d|z6zJ`<0;8A2i9xBgnOVO0X;19L zt;yZIUG48O685zEnW-t(-|`AoZz2J2qSCc8d%zf)Mz%Ve`igN)s(SmZP?>I{(cj;lI0uePoIzag4>}tOiF>x!y?|~`<_a5DCOw%+oUgsMo-hOn+`tp4c1KW z(0u2Q{y9kv^nrT7vg|E_LcF#5l}Sk@Zu#gYeQCC4zWw58tqWzO8eGHkvW`*>MzFgC z%9JFeYHB7W+5H}ZYCP#GRA_lKx#t8>P9L$Colajv{7PEVi<(HS3T4t?`Pa<3;lN$E zj|5j!Pdo5LB`?0Kk&)?auF@#ZFDVoTE`1bu=1=`5+qdT(nADeP8#{3$pqA@yK{0J4 z2cj$))AYD0)9jMy?Q5oR3I=c70^g%k&634mJ{@k9Rk73(pkEMm46Styur1q)^mmmjI-jt?SG5-pwGH z1dauV0VCjugkG6`3VbnKz|sSfRj-@tjGr4hJ1lB7j*gCED5bKZ$*g9z-kb`WufWnp z-`+uKTp+M%v9-T3H`{S~Aingvfp_#K7u5PwxO}j|Dsp2_5U4X{3Q_{#E&9&yi)Pz+ zi>^hpUBaJM{X#o6mCJbvUqe%KVGWP`iU>S)J0#ZNuejzsj+D(=1&v=mpPHBZnEDk2 zjnD4+c=_W*aX;RgeFQzEdpZhZ|M}F<*+8K1Mz*j>!Fu`lZ#E7lChh%hBf=ghRv5ps zzqzp!GHLZO{>cNqTHLsc8SBe|))N{qdIeb>N;|M!c% zJf6N{e}$R%ZF_sfd1r}FgsJbHID^!?Zr7HZ7k#Jh%lEiDcJWSXh9C>_%obtnZr)+o zhc5s{uWGbZsCxhR0G%czPhd!A_ z0dPrIWX%j(U^72b15G$hy&V+LCHH3i^HoBM`i#$~RMW-e<)8c9%->NW<9oUj?BDoa zJPQa6lv8t)KW(0rx`zO6A_Pc^uzPAbkG=p&^kXWj&it=mp&<-9DFK~51w$68uGL^E zIygigWc!Ep_dBz%&*{}s6?0~7Eq2Rg|DJxCT8^_=HYWe|Tnz>RL6qs;nk-PD#x}Wf z+#m19KTw{}S`3C9u8UxYzsGkt;5?kWND5wW+UA=+RXG(E|Ma`f8e!-z{@X=YOk7;n zHRvuqk4IT`g;iB>wvCtQ@b4Z_h_B+(nz84vFB9(HrF(nn+5!KLlA?Xps1+ab%U>{) zA5=$rR{%fiEk%Hk(BjSxTvh!Yezn@AoT_H1?AcD+)!j_gn9AoI@;|5l@S9$1ZC2K! zmg|!};25Sac0|R$+x5K*sxTewnR}_q!VC9s#F&_VzkV5cooVWJZo5Hd1Kn_8QS%9+7IJpPM=GfYy_5dQ0p5-#_{ z%585)g8v=~GozGLYcEbBm&Z1&%id&Yu_njT*OxZ$p}X1ypIt9T5B3)Vd`CV^ppfsE zVKeZMhdsPjD{Xm?aLz9-mV9sbiDgXE{dGz@#u$mpb~ovP_iBmR*BcOn(iNJm2bLxN zgj3G%(CQxbW{Vv$7D9uw!X&7XrSI#kI8ve*QC8Vr~zui)welzwX-;_!WnT z=Y}GS`*q&W#mT!{ZV*fFclh2AjA?sQaoB3z6B~{$;Lf`XZL{j@RlvLd$Ej2;XH8g4 zoR(3}Pfryjcp0FXeHG#zlrq;nTC@a_GV!+A7NU7Wn%>h0QQwcEu%e}w_mCZQdR za=6I}|DkYZxtptBDqttXwOMh{tM*&|yDR}#fJm;Ez{5i-B65WHpA-In$?FWlT0~ub zKxkdY@;2D%-SL4I6Cj*?PPP>{8GDrfi%zRiE(}CJ@DaJP>P}tt9Gg|{swW_QdhD~U=KE|AO;{B2KG;X71r~6tseJqSD1}0N-XQU zv5vUSUPc1kV6v6Ex;c`L+wEYvG23XlaboRCR71w7S*~#;>dE=E?j1@+outag%ZrPP z;wAyogG{9yK8QB^~03sG>Sofh4gpWD6gt~#T7Ty)e}oQ3*Wn+ zon;QYXBKWej=J4xSl_aUk3R@{Q`{NQ7pWFghtvE7IpaD06U3XdU&Jo-MZ6}J%DMs< z2k65}UW8feay~(t$IZ{4>e#;&iXCrVz=Mq(kkl}(IBp%8^$%%Zrs2PZs!w79YXWYG zAIno6cLbqWRpC7>#40fa6tBCv0bKTkG|`^6npd}4f6i)>@BK*yHa@+I$((Ue!v69_ zLB-_Rf7pb2t!~)7ytC^&@p)u_Q4F1{tOz2bW$oB2=b9mq!a5l3{ujLz`$&=gV$;4A zg?UZ-Gf;-{8WF!4qYZzY&5K+z4z^;)LKi2LV6%`iWS=1kOe~G6+26H1Vcj8|G_CER zCr2qHyW!^WUL%vJ-IO_pWsZS+2PKew6yPd=zce=UE*||B1-?aZ^78%qJD=~3M-3DE zeMe!Mj%-Rz!j2R$gX>H)202nz+zn)2e7H!-j7VnW!p@u z+JJ-yxL5h^tj^_o6d6HZ3Rqpj_O5?K-?K@2M?2`~8Qc5^`Gs|D$?o0sU*2rrnuV56 zq^$g(QHdv|AaHTOrJ_Gb!Sg;>C1j1t> z8CN)KfD+ywFMETFTvCrr7+B+QlA|6|hN1aN|lhzsOwFU2L(cr50YMs%F{Jg|wM%OPWs|f>bsDOzd8@>>nIOKWN0sbkQ*$cgi zM7n&?afJjUL^=AJ+A9E$3lAXnrikQ{T``=4yCIOp9!rd}V+*&7&^P`rkadgtAS~t^ z#98Z;fwKobpncB446qsIEKt{p7q4<8oBz__eb*)rfeh;ln`$SPBQ`>J=i!Fm+SJdD`j*NfiTtjKQMpkx~aJJZYs+)Cvj$l^8D5l+p`FrP@^7QT&`TW{9 zO-Rw6gV@ECCux}B`^-~x{0Itg8SQ`9*&5#MwfHrr`ig)zRF30jgST^4+$2m~{kV#` z>GI8J$W^(p7k;Yajpr3`S%U-bfQRRO3(t46WB7gk=9LEB;gG`n(CgX3<)8C>Az6d- zejU3~qRUu~5ZdXq4>}Lu5A~kPY=ScGS6Wc0 za1MDC2&ijOgrogR#}oEPDBHjuzP1jqM~Wwwx4U{&0RGI0wzdU>1wPLpHAl_J34qk!pgtd469XU6q;XgO;K zE$^l5C!)ffaUNz}SZapYe?snuQ(`CU2?N?i@uCZ4&2pE%p4w$rvu5D>KmJ;aY7vPB z(W_CnJNW6FQIV;o?j#hj^b<3wQo&<(Rw3eu?8bZqx(kwV#wI9do->|WWo)zZtoqZ- zA^E*SPdF&PC(pl%K5PzP|IAA4ctJiRuSuba0MTI9bwdb-foyXWYS#H81T$zcA;(m! zq@B4z^SX-U+WL+Cz{8h7miKYU(}dENb%P7)(U~rqEeFm4eao@rP~pZ?YxX z_Xck9i_1&>(M|eNqYZ^~1XzK*EwUan0BPZH&1AWIg+@9PT|IUyANjxH)_&xEVyj0sLXcmnpZJ>`E;k5Q0VBp@@yKcA`oi2FOGr)yCd0kkxY)3=bSG$N0U z6YqhUVc#X(+}(w9H(TyNfg41s5uZPcrEq&cuH)t|RlD@lKP_x?GhVW?%Xj93`qJzj_>AWnPhN&JRj~!3;JZt#0fc(F_%x$;zf6$ZB zfJ}Ot7)xcmg(cCr$+WWc+VHvS!bHTpxOnO-vcp49-*;hDs?d<_k2)#tS1od?b|NY! zSzzxdU445Oa3&yEQctMvp`*YQvDJW3kw-&{3qvu=L1lxp0HWMk$NARGXVGA)w@Kq zo)LDhHP5?nK&TRzF#Ckf0uKQ?v6vwit2qze=7VBiK`pI-?WSG)nRNd1;X%8r)H6WZ zqyX}71(0okI%{K76ZjPonl?>XK%XD9&1M1(q#~{}&SXCO+35z;C!DAacYt{a!c$Q8 z#Q{G9+kC{#FQnlSvhCPK2^!eCcdM@U;B(0>+5(<*gV+h_{`gDyk4TJ+JV^CiLv=;6 z9ENiUZ5&>Kb>>{0Tm1Q1g1WdIFtUi)%ROzav zFmxYCfCu)veK~jBB^i#SN|>7H>EBUF)`c~&EO~q1?0-|#Z6Dp@5GZtpXBN)_FzKDu z;)&8>ZfIM&btjKk=ljcm&*%Y&OSGzk8lV#D_j0!x9jJYKm4yKU!49wjpKuHiG!jaR z7-2V`$J+w#9#IA-7fh<5Ju1y0EP#xjAF8GaikaZDYnaMRm7QN)Ee|y8bpY}S_bw3; zXfXhpe>dhUmN#DPz(;MKr*eTgrs;cn5Qz`%Gf8<&fG3Mtb2tD@P81d)^d?E&J?Sa13o6 z*WP7{V-V4!zc~f4r_zO2GARJ|l)f!A+=&c|uE8bgWXB%70Os-+V9VnC{CMptoF8bZ zD#iUc@ z+2%_{g{NO}im(dL_!nIfw7)$NR^}aE%J zTOnAx?h8-MU&06g1J&vFCF1nF7iw_uMLuBBRI8i6e*PD2CW}HuR9w#gcMTy{YVnriwir%~Kj5R)LO&Z0Fvhqzdh(aHL}vhs7|`kHQCbDFjJ ztDA*WRTx7FQBplJm(vTWV4OcP4&-eIRp|P|AqX*cog^^aF``amssh*~z0vW{;h6=l zza(c2pYaP0{nUvD;byCT1xVF|+Lc`mdQf#mHmjM*-H!~_oOS{wG+!vyrUALlDs_oI4%6uxHm~6F~IBHN%gSTcXaZYJ{@4 z?rhK+aHvA7L3|w=E=%>0vIcI@0{v%9xlsXwp4WP-soPrdxOmEhB(~`GofFwTkOZt4 zE1RzYy%C|QE%8HEWhK6NH1UUcc{+yg-FLUUoZo*WCw0$IzbPCB_7OovMXY-$HVrcw zAHM|vA-T?Zz%Vl2_%lF;fR5?4HXVIlrT$M7SU5PzlK&Dx!}+*4#PfNOJGA!EL`|76S9sVXb`x7_V8mnY)_iI%EP9ooGUp`214c+L+_f z9X6g>6#NDs4oH{_k;lst6G!%$;F@gb}T&h96cx<25#{8Oq##v_{aeW?EZ_ zX_~lNFX37!S_eKp+KB)N60E#uLAY)p*d=Clei{J+ADU}Q+&BK35DX=Y^`;xlkBzvN z>DRVJ?#Fohgr7tPNjeecztx@jMmO)?4)+DTiOdK>DRSS&tG&%5TMF^LLsWfrU>WqZ z*ze733%a#8@Avr=Rjun|oX)(5lH*CTqu_FU;dpz7E1!WbXaNH&4bcl0PMS3*j)9aO zm%?qgAlT5}!(kN_mHLw!5qaVl0v>gM!A(5Tf-Wtc9sR+|+EQ??PHWN@4GqaZy2Q)q zEaN;#Bq!&)_&YM5V{QkT!LP(c_bT#eSD9j`Q)px8#8G#8-cHB*W!-kqdINsup z=nz!}%M*oGYzV_F$FTgC`sDim4L0U7W zzc_kEzVp#mV2{_uPZvCIGJW5aWqFYmx7RrpwX}ODLj$yZgNt3au_-`60M~FE9=`lC zuJ9oZdFn`KM*NsCu77>c8{Syc7*My>+HB1<$2g?sr$!|&?Ue!|VoTx<+O zTH1O)z6iG2z~N~GpXLtR(Boq%{Km%I@fBmLdp=7!Yem~g?ne2%ys~^$3R@anGyT7i z0<}+-YAPn|J3GZ(&RP65M{2!4e(3Ewz=Pd{q+Mv1?1qQbXYpW!X7)XFd)ERDpmr8$ zdrSjkIQ$q1{aEvLm18u?m!OIsVi503lG z+pwud7~y~>4_&MgTW|O|n|EBNj}SYO#C`pZYrH{Cr6LLdnunk4td=Z(VZJ5p%Ai@X zcqkz1eT9o$HdxkYNBcbmH#G4p4A9NXSq%$77faJJ!r>q%zpyYqvRqod92@g}1;3r+ zqEPd+{{^7%e9}JTwR9q!g!Ku$zP`pxCnrnw1VqvrF_Xckpi>IC1jbes0Lie{SL-9L zug}N81a%g(fbmI7!S&s$f-#lG^i1J6W54wJx}D72=TWmovURCF793Dxk+ZxMhe=NU z5{im=75dUqqGlZD$C!KjOL7z+OfZZf`!np--5M>i&#oqjEJ>`DgY)UR0zl>ATV9Jd ziaXg2(io|!xAGQ>MExos{&m;f?&n%|P$a`3aZyU03*>ESM3}U_B~BRF7WK4#-W%=f zx6SDrCF^@e^Tm-raNUkidI3}&Kna23>Q%1ax{=nww-U^hPqewCb`!;B4BU)xpV4m* z3b{_ETC&Tyh+L*ypVJ}kM@zNi$X-unWA^_EN!fk-3Nq~7L|~PeqC<7R6xqG}bT6Bz zWRFfW(Qo|QV^7?<{a=rL#oGsuy|ar;MqZwHom<$#f*!v1h>BBXRc4XZxmOQcVv8@i zm-aK2Z6H8X9v%9HybHEY!Gs@(dQAyXP`8G8yb!^PeqSBRiAM~VO^7G?4T?zbrduc@Kf?Qc z{Vd6KdEK&b9>E_6{d1?P^Zx2AgF4lf@7>d>FOPB$)83i=nS+&eVJrg|Db3`rN|F_} zO8e+GN)>=E9b|quKDz>0$1MU`XHCnMa1j{m!;`9WnwB_VQfSQI9ZrdCnG{A5}YUk{9=Nn@~aHV3Z-+oX)ceISC*cL4ErN{>WaB~ z23AU0?h8tcIH+@@{N~eXym)?J$NlL1Lc9HKf=|$xBwTs#nNaxDm&`k$`&7DMlC( zfH?fzCBx?nTqk2k7AiW0`OVFt@78X-Sl=T$)U1&b5`||r{rw69I#mOH=cAnHv=#eF zo{J>d1?Pnwg)Mf(B+k3iB8ZSt)3Y8iB2T!W=tQgun+?&L$fN(^qj=Wxd_2mIw74iPZ-%c83ff13DvnD0-O0& zr6D&C0BhT8IpfgrJ)x?su0|nnE*!Rq0jkiy^4DSNG$8@K3@hG#+pleoU@%E7?3gK! ztzxZJR&9^T){E>ai**T2d8h9It1ihnj5zLKfio|p71B*`tgP0^BYVt_OX8yL@wWM^ zO64dvu(RdnQVb?~;`Lr1gXz62eRf{QRT zy-^AIvnj1|!bdX4wg-55vw$BeZCR;uTPv^3}SmPrr_vwzV=?Q4WybR5zBo%qQ3FiONE6>m7XK<-#AuAr?a);5!S1mmj9v2$&D zJshMysc^BBN1UDB76p_?q29k9v4og4Iq6Tlg%SU$;H-nYaEdG&1DExX)A$$;owEWO z(mnI7h5r@kuHQ%!b9-U%&K?pM0;`>7wNCWAqRcw!H7vmwoL|0>BVxcNc;S#Hq0*|o z!og1+xl6}<*2Z=o+PXjl`_A!kWAgy6T7$NjS>cNz&*;>#j(8d#a)V=btkp3MF0FtX zizX<-KzS7thOGY2T2HgjUI=b6qVJx%c;J5OVu7-W93bN9T>DdF1qNVF3VRr>qb7?N z8s`W}Mq9HuW}E18(s^JQhF9Ye%=IQ7e@7e4mAXOUegzFrn|CR*q#48nc&c zWQS^)#>5juWW0~o!4)mz-|2k1)q=F#=$<)7m@(#Ys{L@zcaszam;W-v0#EIvr7PgO zfoAF?&9@$-n(2G+)_wa=eSwQ*CxX46_|Jf9erc)jfj*pJ@ zisgxkh*3QKRUu#q2;b+Iy3H8}8QZbca=M!IO=D)xh>r;85l1KU>*_FfNCwD1CM4hi zGJ}z^Ngz-~0Uu^y3Mjowj*N82XHhWnV!=1DWvJ<4aTwO*6F| zz(8&>k^TL5MkkfvVvez_|IXMr)DS+=@rx5 zy}V)fCk*b9<=^wA$|Q~3v{t_PcxxB0nxFAmXqxQois-Iw zH$w^j`I51wU{jL>lSC#L`?NRL*7k;t=RxaX&G@Ha8|H~r#4ZR5Fx)T)`HZ) zY$emPuR9yNZ9r9Ju9SOU)76Lquzi4?V^emIMIK0xjU?2qwZr^qYyws%V<0&GpqT34 z46w*x1{FO$8_`8DwE)Itji4ugLbqx2RzkO{K z1U{^Q4595WL@5Q&L@*;eCp%>LRMUN7d71q8N`F@&hPl`(Rp`3CYR4`4sH5r0Gi-N| zQ-lJkj7yJ|Hdw=Y9QZwV3sirx>fK&IXZ@Zps9Tz;4LEoRdN`$}eiLhSQ+Tww=?xuIzk+1Wej>rn!_3gM_+#jb zXUj$NM6@z)t)~}Cz~c`8ke)if<)?X)0oScBygai^BvOwQ;EYj4I%`uRu-RfiC7~tXCVC7LnaLmRm|jQ1TYS<>VPjcqM|^@8C0XL! z1u61cpjL}1lUK2cc~dOf`;j`vZB!dW=_dOcq(eB9WauyHJmh-XcGhj2KRR|yRJrIFU*!*?b5>{#v2hqLRN+u5mK41k{1c2TUy>K)DJQ=l%pEdQ5PeJ)Ece@F= zfCGTjhh@L!uQ@zE=RIAoE}HJGK%-+y9rOGU;IkUJ{Y8WVAfm0nK7#r9F*LKSZ9Xhc zJ0->YDgzu2o4Js!k`@9aq_;p@D|lLf)f){50W+Trfkgm6@`loFlTwBs*r>ksS*Lk0F@bZ?zaGo>6U%} zW#s;e^XeRcWOWUo&y!U#wRTv)N&z-0j8%u929<_-jZ)7G!g?nVK?@=6R4k6f9nU4g z;G5SJPteoTyuX50;A&c>sRXHv9(0im za2ui9GsY}1-@BlG7@H4`v({Zl|Bd#`Hgq<8gmP~Q#J`=?FRS9#+VHWFT1wqEya`jG zFbhZK);OC_t2a+ zUgV!abp$-G$FyDNsjswLxxjQLK<@>Ddb_^&xnQyeIxA~JG>?OG?fl^{yq0j_<^dxB zO**gkthy&FOz-D>9(sxcXGcft?Tp84q*}%4o9IIx7y)HtxU{PyvbYE;P@4}!9k(H< zO8fn1iYlY#--G%%ED{$%087PTU5Dn;bcL0Gy1BX8?s+$}tD^Bpwx;Yu`ByYmTN8R9 z*C?u8(_OT7fbZ;n8g53z6Z{io+#C^}vFKg8_Cy~z`Ih>Ex8WQ51F~dwMGx9I;|oKE z)*DN=t8FQ~dXzh&m`FzjbXM8+t`n>R9#SESzRqK8{7)jg%c*2~K$31u3ngYyGi}d7 zO>_@{H?t<Q}KeAB@U66q?ognc`{yKTjke(s%9Q<-1D33C8=IY>A-D>ir1-~m7mzBpgLp_CgZ=iLCXdW0xVo` zRd2x9GsZ|1(rX!D96vW(Lzp;NY#`0W1`QxC&u2tHh7Cw;_$WFfh*Mx<>AfWT+X3ae zf)}T(0)gA#_lM)?uU}z?;^@l?$~ea!FhH5cPFTzkdEbb(u(Y&0-WP1V=qjY4;VQZQ z^yJauV0Lvip`hRsF!&_EgPg=46f(4>8KX$Kg8))_(({$HEI`>*U;+EW8|Sw5Etsk| zRHM>hQsqWPMrI}{NNJbA)#@mS?ba_lrsXp?TAu9+e8mlKLfMduFv@*3BziM7d5hVQg{Y~E&a?GH&f}cqcjJvEdqOJQf=)gy!a9_2-(}-)8e?>zy5u!ucX3CNpJ%qK8>AouA4YYUCVPKGSa!N& z@A^Qk@m=dys^Gla`u725q5h9d6m$5zuLmfh|9P?V{SN`*|KsV&?f)-K3IG3pnZ!Rs zs4r6TMmI1X6GPt#V1d!adMqBod82~42E?-@Kr0V0ZcVE;PEIZvO;w=y6bagH|EjS? z*dzKHGVIONxWFt1N_GaVy6|U$!1DU{1Xr25@Q3nqn2EnT7CWf53mPm@z*R%WG@Rb8}w``iAhemk$IR??VZm?)mf8++}r$76!FVk1`cyz z)7`YNjEi_sH-@dVCShSMnmhAoHJBGV9MpPW6okHy!;emy^7y@?+X_xb*gM1y$r2lJ z8Jp(v^PJkXf#fn1;`u?3)d(AjdP(B!DpmYM)TjV$x1Co7#bQ(3A)X7ZZn66%;c{aurkB^)catjY8r8mih#^_;{vwozYc!d&-7olG>4L<(&=KZKw1!!V4 zOawHt4zk4MNqU~;MbRID`M>VCBz(TH9&wT&7i3}98N(KW|=e_Xeujd8O$QM)qR15}0jN^`*MivMh__KfMk4qCH*R%d^l97sp==={g2hH1 z(B;$Y(<_=Fy;`2@cB;zX)^f8zAR-n-tLj;lpMz4;bV91*eWVQ*7Qx)drB979SrZtQ zaZ}?>MwX#Sk7;4CWxPd1n2h<@s68Mb(X5KlDY2i1hM_&7d7p)}c1@*7yNX!M zmig7;f_Cjey)+m;AZl;UU2Uxs?YUb^z^UCrFm5~R$4FGthGY2=2GUEgeOoiI{rt3{`BezBI17c#WiyI9v2fG6pnFj zZ|=j|CE}FW_x61kPq9qhdbzocji{|G8DVnLu<;UI-HS@PnW@c@Pwt64A?74Qneu>=T{d7Kf0XN8t9N+8eqI}k%2J>B7hAQ z(HY*%hYV`qR;e9NvIY$Vb<^QEH1EpaCcVCTNT|cYR=-Ou=ZN<7ipCneqKy1O_r>n` zoYw2|0`nIqJFRN8S<(A0h#ttGEF2!A!1E?)PT6f9s~2fCqX>Q$04MZQL&L!szw1J$ zYW4DUtC5dSGoRa4Q>xohDtg&ntNiil>67PtxbT6bxP_<Os7jrex2ETp8HL<-UN3SKlc`O zn=hzf5fbA%nUXKc_x@eDBe*fycT<1Qv_VSA>7eb)h9Q1M#*$AE1IoXbKE^UA^O@Bs zQcrx_D&V`aiS#-w+FzwGxZgxr+B((Y1jBU>;GXO5WO5j##bRhNfTjpFQhI%7M-jKl z>a)T8zbp88|Jw|Eann$T;UFVphR&MiQ*2ynDhOn^{zlB}YD>1wG|mYP3>DUO+xGD~ z*$}t?fvRaYn98{0Js2et#M| z+K%YE_Y-$8cA&?AZvL?9WIzGs`z0`&^T? zfKBR*t?{y}Aq?zx)||GAxCvSfw{LQuQ7^V;E}tK?+z}4m?&-?U4+S;YwqW{0EtA=& z#J>+==09QiI$cic(FC9wCd6D`#F5hdy;-cFU=DT#$k6O;M04{kJBFpIY2?)va1&x+ zl77*8^(40L-z#9Yv??-GTUH=nXErmE@KhM=AA!+)f~u-_VKa)R4)#k*XbQ1-v=Ndc z3V7N3LeJ-J?C|RQX7d}B0{Qvx(9PHhb3VVj=A&xT2}RhgLmj-`r2cEM5!m|5-#xXPkK?zbJE6Egm9CAZ6=YE7&^mfv3T@bG~3I`gNw z{f|h^{ldHRC%etd&y36L6Z`(X^+d2Z!P2M#uJwuG+PQORn;rzh&e0>@TJ7r8t~Vr{ zOXw5p?&+EBNdEm*{*M{dH|h49QeVp_O2P7R zFuW@(uc(PGkDWPc0c2>QvZo~YtZ(Pk_-?LQ9x;DxHK`EHRS+yz$t>xusWS z2SD!+O|wLizimf1{v(qN>Z!)YxWZQ>Gb16Q$XKPX*iiK|pGZi{v|Dpp1K*9GEB{1r{U!X0 zCIoVVDm_uU`>06g{o7WEua3*H{$h7JO~jz5H6vrKoBnan^*>Lk*(G3u zJ%=2hoKOH-g+p1lOzMEBY61am&ua;txUkbGnZ~n4rj&smFc!(lIZ@uN4E8;yzJyLA z23WmJ9R`pr`~?5@a!TW06JZ#9nLbeC`TdG}3M}exA~sCNrpCg-pUnRa4p1pNSj}d@V4~kY z^YFPyMZlc&_gG&X<_=^EXeu*_dp0o2Synilg;B2WU zhK}sHU1K^rna4kY0DI*OwxA)z&c$9(PL5`qgd|s)9b!>f_&(MVGad(Lgzn)Uq#*oe zlaygkt;*@@=6=;W+OMr4$|(RQ41_;o&TcGH(f}-;KQG}{X*~Ab967qhrETb!2;vh{ z6YVp-kKY78HZkdN`Lss#res{WSU$U|fZzo61h~(|M2+FtEvhPM|5;H{pKx0L$3qxa zZ>Yie&p%=k{MQ*L0{=gfRH*;j+KgKNn~DB^=xsuJ5^?zoiIVruNE>`MHf~rpHbm%_ z!lx8jZNk_m7$+X6(9l3V zX}RjJ2#b6R4SAZEvslGB$MH*DU0wgOvPaFMdk5k-KIcZ%Lu(%50e%N6TT<8*05|53 zT7+G{B^l1()M87XpvI~)OqXJJLK)|Cr+LrKRo6_OfF5m|x;Dpy1$FE5oy!+0-RBgp zQe)ZnX-X;yCJq4_WTIjZQts%0>I`IscuXcwSSe5j24u`EEQH={)-gGvGqNV4DCP}{ zEajKgYQ>;e)uX1R>A0=pwR99|t_kJ+q~zc0y~RSMl1yOiZWsL#6YGr$*mWbD$#2x$ zo`O0o0nnq8-)L=S5Ne1&QmqGEzfAK?zVO5V5qR z=a2ar^AxdNxgrTjA^X&8mUcZ!Tl*#*_4XG0`rbL2R|p1!3x8#lmHC<9n-#T4ABVl! zdCaM{mTOr`fpuiSSfUUUizIlz6aymQGC*Zyq7%m`-H~w(R*SelaTwx0lTD0zpDiYsH=3EBFURu(6^Do@THVnmFfR{}p@`vq;(IhA zBF%5n5y@R8=1a|H2l7f|43{2%xQ2cwsL>nJBoUwNnk>7vMHP#+~y&$3)`g zGed_c#KxBD-)NX0pswsJN35L9pvU(T&)eSO()O4xY;1t3?j9((A8{wD91+lV=P6T* zb+fVzpWBZpRTfN_ZZ4nYjt1%g8rWx<^tZeXJ|dTOO6FBP#3Us4-5(mj_}r+y=2@EJ z&rqdKlMX>aVha_r`~vp&?<K4tsVEm5omNqyz*l;X`+pg3V0{Qi8OmFRUr`GO?;uLmm$Z3b5gc;4Q zS~&`2zrX9`d%f8;+@7pEubP^M!9dFN(9Fm?5`L>#72SAn)x#0ULO>I*`P%CR-~i`J zhopvrfn?Owre4h^IB(v($r*j-=-|>$;>r-%5Q;F6``O^<=iQs4P!L!)+zEn@lqiHi zG>Sh@pOm>BJO=a7397b~V$!3P(+TI%JKEcyTX#8ean}hvixtcsBj?H&odGsP0eKamzI{6ZnX_r`L(d7vC>=XU;$X7`hfzqJO#+@ z^DPMTJ{6?=HuK@giEB?DQLtOQVA3I;;Jlx*&gF4Vjz&5WQ9g2twj^duD4WQN3j9-M zR%We}9jlOYRs8ivMq*Vm0qihSGK+j=G@Avrm0Vq{N2`DAf@J&4UzhLZZ_ zv|019hZva#7qpKFmB4qqrE6V{x(bYT}44(vB@pgxL$gEMP*I=h36r@xfbGn79n z@%lBOYhb3NreG4{!i!UjYkg<;Av1g9nXB;Db5GYFbjg%dRh}?XfrJ%x|MXN4eD5!o zVDPogwFb+|-?tkB*|WL7ZnsVP!k+dK=O;8iDtt8cdJnewH z`kfi+?eJ3ZQcZC>d5T*06BnkVZ8AcgmcQfL$X6?00SM?K$0WLwZt7gdKR4`t!a{>c zElVuyqmx!iSR1m_2H7hq*zp9DfTUn-)!-sYh_^BEh)`Y8IhB-^+opOSF%8Jzv++Qo z^ShBTFfho0ZGci-b|LkQPsmw1(g~A>j;U?V8{vI?Doj=Vnyrlon?;}6yL1);u;^X5 z*QuRsAk@aC8oRMg0)Q5{`4C>|%_I}yut$OAKe+*6b8FMaEoLKcVT}y&&QCy#E8Si= zpewbzs{YFd!R2(sz%I_HH^xzzlvtU=W;W+r7h>K5@gM{tE=7L|Bbq6?AF$m(&mhu0C zySEODvJ2luX;d1NZWIutI|oUX76qgck&dCI2Bf7_q@<)vO6imaLAtxUYX*kcYy7_N zxA)oS@AJ=@OT8{9-dJnB&wAp1?pyWRuRw#{aXWb$h#HrA!GRLU_|qhOWsxP!f|#EI zdkqfQpshqx(=cGF~ zH+KTRkNDR1_Tixt1ET;7=H#nas5JzP#*CEY$9S*q0a)Uc?;BZWQc_arVGH*6R}FW> z#3s9*sDrjIsg{=3g@p}E7Z~rU$7{c6A{B$ie0Zx(4Sm+dg`c&RX;5IBCYs;Rrxy0L z-^xa0<^pg(z1QHz8OZMR?ZL`$dkcfFx^*rW`DL`f+t9EFI)MEbP|@k zI}rkLkpX&j0O3OL?*1E7t1v17i^qkdCh$H(OS@e$ zx)Er~cZGzMe8^Q#+jAB7MudvqoMi`}Y4U|FWr+>9OVArL&moas?UU|(_mmKmRJlp^R48=}WQ&gFo8B$<7Fjc^TX*eaLe|PS&A9%926W zR=4-+Ve#xqChGDst#)%5+QB=McJs{OvmoU?vZkqO!^=+CoG?ckbbEK{Yf06IeeaW4 zQKu6ux$|DJ@HUecBFx2;#6{E;*4rXGV%S?~y4_jYu6CA?@T~lM`qLkjK zWK=IL=C%_-rrC>c?5onRM@1mQQz3IXovGIi)o#PlOH!Ql6gQVGJxf!O68P(0ef8O| zXS$%lN2pOv^QllUC_75D?@1K4@keYWF@$e7&OWf8`Jr?9`bzkasQ*eof<~-(x>ozU zx1#rE1-$)qepi?_DRcu1I+on7*rVIv7LM46X(E~Tu!u#Rk59L48lJK7vub*8#l!AG zkk;YV;G&^d?se_%(if$3vW znX=J5>C!weH_&7fUY|CBIjs<7Ku0vpcH5=b_eKl%7zOUGQWeV0pM5WbiRb3mnh9A?FaKZKn%K7cPX`)ZP z&qQGNAV$ZLrjy%E(_D?nK|Qc^aBC~}#ieeRXqLv4eDCfX9m-dUZXGB`&#v`u^>iw?cvZz4uJ=$5t}U~hHZTN3i=cHODs zphdfNYVhwK&n}%>-41&QrrW)=B}a=Iw*@^fRltomJ@PHP#%&ctSe}Y31w>yLO!Ix9 z;xjTh?BZ#Q)`6LM7@oOFTKq9&{yW0w6!XOUwfnaZmxYqrPzxV5-`DG>L~T$qhuzAZ zbz|?$=?X*}&-F-AiVOd$=f?gS?%h)lHFntYeN-ZhnyCi*b=0+w@9yl6)EM$KoIQ?Sm9leCA1}<`y#eD3X435g zT>2nObNW+eZm5ILL3Z`qS?$BIcRl#4<4%&eM~JqB>dWmK2Dg-pTKcS*a>#bF=az`X zJlt^C=gW!IF3J?WH1=haKJ9@}H*{`Hm6Yh7M^tb1?Ef7|V4>_cTI=FqSAWzF`+4eZ zPg09>pVmejvYmDN7;EKJ?u(bH@A+))pZ!$dLn2)4iI6L)$mGV#s@(0-k+Yn$?grIp z$wPf87-gwqk+*Fghu0ygBu#VW%9q2&#(Uc1uKccEIG!+7yZPb!yZxe9$t$6f5`;(~>?vT7u2)%lUw2U_ah#u6qxqS;&jtMY zM*POy{Pj)aEXpgOr#I5|T%T3!k?Vxk9PfDzYRvlu;GQkXuDuf#6*oQnA+cz(A39a5 z2M9DNDEI}wjcNg9G4L}+j9|-tuM42a4nUJjTQ>mqj0w!QL76=dMkoVO+TzG~! z$9+OE8OeC3dNuC$!NEb>pjil%=~xmtq-B=f{Er%(#V`WiLzfVm$)avpPKi`+|D*zK zBKFoxFoviDRC{?apYKrZ%vaH5XFqsCDMa0~U&HX8k+R69rL8;f`}be-X*Xoo$crA( zG#-!!L))vlLHBrfzTn*4-VqkA40e0__ATljfxF^(GGU6kR$_&S zBwnhp%udRw_KuF_v6^F4W*-1~Xr-$ewuFUPRla`c*2<-tI<4q^IBfB{txI06&R>*% zkwmlF`Mg2n;UDNu7Za^$DFwJMM@J`qJrzml{x7xI|)j!v`5l%7TK=JC3Kc(%(=U0mT3uZ&o38P#zI=q2d!j2)K0#lXk%a~GP7nq+ck%d4V<8EPk}N1SWIs1; z>-scgjp^luD~T9*))uWIV`hdKMK2uz{5VFPEBp+|Z@?%oM%$=j`QwnE)I2=}7HDW*%BX}Z7c z_W$Yb01XUKaKqq2UyQ$?skFOkZ0P#Ecg;9k4C2U*K{GIUpIpX7X`TVuk@}CfYh>BKLL*F$! zYcEb^PD>%`7uwVo##VyVz`1{%ukou)c^=x(d9295kYC7kcYirx~ zNe&l(AL$qi*Nm3F7JM();#gC9b+r_>ti-gXppqz#>HetU=k#^mI}ekVtZoo>2FG+p z1343uqZ6uxCP%)U{)EUKkaY5JWGE=$B!Mua|6HGWE7f$gGxzJF5$M-@Ptk*^FoU_J7r?zcdq%519toUzvQFry$Xf8;*QBwJJQFfG-q zQ9r48SPu41=8}DLR5~}l@g_XMQTe86bfY(Kc0!KaL0t>De~2NQ{cxJ9oqOZouZ#?w z#xa9yAGk{~7QaF^pj_8OhntJE^m>6pSCk6|k-A~v6Sps?BuQcHtGnWco=$Q7b%EpO z5i$`GPj~nVt8;$@^2MMWrROu%<|T+l3mTN4N>jQ2sS{?`W$ObRCO%swi@0_ghmx&I zV<}bG6|!(iho&kVFW#U#(03E~66#F|wM*+|x5F@t650~j<+|Np4?ia7&pniH4UvTN zWJb(%CCDG9@icMyf*lij?h_qKV#psk4y)2f`Hq6t$R%Zm58ER9Bn-Yg@xe!ZDj$m{ z2emBvC{TBLxi67MW$x7$LCMk)L1~7l%gxVK=?~>joGfWPhgR)Gufic5#k19|j*VG8 zJIVB37x2F~Dl)HcRvyvkT9tc&LVZQ`nW;Y!~=wM@R;tsm8vmjjyk!Yv%uczS^5u^}^uinb%?V zID}VEaXkOnl}RdoDBt-eyYuWiB#jj8<-?YAz|WJZdX##Ow@=z+&@{W@4R=q(bd|h5 zX@Nx)Z};Wfu==WNsgE1O;16U!=g+&={9NZI*D0B0ZoJtPht32hD!Nwn=`$A_)hT%I zFINN`GoJ`PZF;cRe{x{fL=MJJ`LflS$F&f+FBfC_;*s~$IgBi_GL8I{02$&&_ac^NDzH9d%Dyj&gK{K`JrJ+t&fD^j+lX`= zb_6&%b9$f+ny*xI>|yI8H4HaC7OqvbjX8Z0ITXg8h5ClQp@*o0x4$&DBcDgUNg1pV z^1Q@9oCQ}_*f?eDLddFa%;eYY~xU%F18*2;5?-XVRRVoI*mG) zpeNmI<$*nT7WV7R@eF&m%eF^!$WPTu=Mr$5a2`}A9nS$8`!38(jnaacXKJqUIFj%5vAT=-#F zOdI58_vK+>q(mEA>Q$?21?;NmWkX|d>V6mD&UVFc+OpWk&d!llXzX*0^0w)gmdrWZLb>0&=Wu1*-xTC zq7+5jw%T(JOldRS{0}1(7k)xw_rmgkf+3APj=v13D40v9UjLu9Q~Tz?`7DfRZ^`%+*{)Tl4Ox`03PF#r*rout7Vn zBFo|O=H@{XfB)uSry@nXqMY?dTZLg!k+>(!V5`P*RZ{fG{qplEJNLujx;nO*8L5aZ z&(Dd8rsWa!GX@){;s)I8ywpkJ5FU!HF>+DyIy5vu0=2ld=nn*)R#qz*@LXtOFi27vh=u*BSoeQw3x^F+OMZdM|)GP}N> zHZtx6z(e;=7s}OVC;ci^Oa3ql@kcR_t2g2jv8iV5gd@5UirSp1ylE8Q&-` zuh`l$bSGCmMMl-GLetRKh}-E0cgtKe2|D_2$^7<%nC^wOHJlnihhY@>Ec>%C-&03N zURt^o4aXX^^K%{gyC`y)h!gyYsVQdmE{|#buSw8@UrY(N`BB+qtz7jG74FVCDKGFf z+Y#+|*@r2zD50O!JG$Hl(qwEsELP%IaX_jTJX_Sk3lA5_fOW!tIP&A+s^<~O<+ zha3zwUHgx`SPp4%b8>NcU(y5t1$xJwE|N?et-A^@Uc8{JAi8xcup(B7dwo5#;Kz`L z(aTq1J?H18fTK-ABXm#_4G;-ipOhwom0r+1_O(py*lrU!N*)a==pxRg zmMY0qgo6eS2+C8mXKrT)+n<})_tpp{yd>^Y+@ngtOwqA>?Vg_V-?Xb1Pr{b{68#Rko@CbkK5{asb$P(D z@3d$xC?cGm-bmkV7>AN5oBE21@LHxWetUi0ZkA5;Ir?Ytc48*klBPPpPF*XWblcn8 z9}wM=If9Tr2^`zj-GhJO>>QT6kkrDS-YMRC6bt1q#b(az!_{Gjv-FbO=e&IjyXj76G z7dNqOX!KXs)+p_RpJm?xRg;zvA@uDffIs~5{G5W%Y|VC)0}Ty;xbq4Hd`aE3%vium zN`Bz)p8s{qI$7KoAN-1w-V>|;`lt$ocOC2=794;oL{LP;9gjI4xC-bMf`0XfyNEk>WKPOAjfBmtT&2c3``0_C__>~;FevZjllJU(R=6+wEl z!Jji^t?r?H`(97$R5Qc-sJ{XfOPVo>q$1X@FECP(+jt<0kjv+MR#Z(wWKr?c-Or#{ z#?FpQQc^O4HByfN*qclnzDjohZta%L-ndzZ#lbiD9Zgp`ZVLlLVDR@!6-gwVrE75|_bqarCng;f6%IaHxNm2;ziKk&x z@KTFwJ$=bb4_#4OT3YBsiOvm77oF)!-{^5;FYI|+gJ>2dLQ@d2A(oCu0fUO1i6gIp zfoJ25JD76Z&tmT=$8oo3 zb;s2CKrNmjS5|(aPzmo~u&G zcx&X)Ffy+X{#vTR0nP8j2Pex>Jy9|0CWi0D1%lS|=k!X2qcYQniI|3SUI`aeG9P@L zZi*jjO~X57=6@nV<&+M&QrHCU5w_PnKE-xY)z6m=N~c$+XWans;B23Si|4FNE8&Jk zaJpO4t#)=scnhCUR#jDqpXUt*ot#paXN`z)&l_1CFXoHSDsLE&wS;1c(0&KKkj z7CmdpTrHcsiH>LG0S1%T%=pxkO%8_J+gk%8BwYlz@ZV?0#+E;gKFPQhc^~i8x{W^d zk(HSlUgG5xX0m6>_18}J+OJy$&tAOH{xvvA0y1I;M7KaZ&hqS8_$RN2G@w*}aCqjQ z8&4r0%D%BNaCM1!&@+EY|I@yK09+|M4`(ZgAU}QbeZYxS6nAli%z2zX03HQtoVK=Z zwdL(p&ov##(x$U-6DFTr%Y0NeQgcQ|?#fAM5;JYp!M>rRRpfZ(XgGZRN)1IE_}gz! zH93e7(#?eHJ#{oLfNYa0pkEXk`~)q2ho2?Nj>>Y0NvHr4K04w=kXFKp^3-)4KC*Ic zYnFRW3!wYffW|CI7dY)Nv}0LV-LO)x+Z|A;lg*`IY-0!nWKg6I&g@|<^Ve&R1a4Q= zm{Ud_Peg)jlz#d2CX{)c9I{2`^bs`+3kwfTqM_B))C_Ev0}ezf-_7BewL-~Hdbv$b zMNzbgXbpLt-7%eBc4pRITKT;%c?HP64O^#gl6rsJ21=Go%Svd((5;m)dQABgAn-Xj zFmMZ;9&B|o=^DRy7|~8oPrJHn__Zx^fuOzVX@NTQP?q(3ksYtq(Hlu*kFn`$W^`-! zB%*azCe1kZ`RxXy`F-TVX+6Dawgx*f2)aG}CapS{V`yf2yNeAZP*M#s#S zvhtrissG$&VvWqmkiD$F!reIa>`iv%r1!ae`be)HYjt&%orec>(#n~2YV|o}(YShh z4o@GItowRMjki{8Z2OFubV%3i+x!MeUFr?5vp?Vw=r3MBwJg}K!G({pxs0YscaBJp zjhu}SgHoK|^jd#NNeKf;kT|ZL#2WLsJbOZmEg|7$&}e|>B_ZMA;qj}plY)G0jaNV* z0Bkn4wfDKc)=FVEH04z0Hs`_y#~!QAE8z~r{v40(l`GQ2OF4UXP*M3EeR^Kv4uvsp zuQ%eiT=~o;63(UX<3s1<^aC(xCmR~@!7I;a8_Tu|1O#5?zi4jl(bpp&Qg`UO=ae^U zXK5!u$RQyv=B{5EU&RB2AGMrWwj>()_M$Ipsll#^+T#EGnaxup#`18r)lUMznYc#S zow4}uc<&(kLrRR%3JMGGsroaOqJuz2w{f~0 z{l@DGf_QzJJ8`vq=G$aiPfkE^I&nFxSbsDGaa@`p5v{3y-SItDAgLrLKR>^7`9mN2 zH688X)YSVUr)Ap#n*53)f(#>~6t!r8k=cRPew;k+4-^O8EHYS&J5G&^y=H{!ZZO0CXq|4SbZ$N;K* z<;S+Ov$hnn24(fzYJEO8)ZZW+oKpfF$ti}!bYua}R#w3(7!ox4p&6mSKX6hh=TwTs zs^tc-W4uosD1xNhP!JP``u#$?Ys4*Y-+Az)As--~pqflObhHc0yMX=HK~zjUBdfZ4 zTqU_D9`E3Es^lOIc0;eEUjmc=+N*gFkl)AB} z-t{R!;J`hrNvNr}(uX$}Hb+yX*jJa9?$kVw`r-TbHX+u(H5HLju&X#|@5Fey6_s?_ zKpWr-sTYBBCu8dgC|8qyd;;}^Sq|6RG*~Z9fSkPR0o3Tv>(Cb(|fIqrF|u-?Mxh)xUq^~+>Kfh2K#xXwqw z+RSWvKdpa#eO(rSq$K&NIh^s{SXiK=ad7_DG&GF#`^5-y)Za=O=S%4&yl^)Jsb9E6 zI7CD(a`!cER3Ee3$gY z*RR&Cq8m9*Zf>BZC2SlOYva+%|pI#jEpzuQ-DgYo2oGU3&x1C$I zc6NMb9p9z<4PihwO)hf~2*hB#eV=FwR3(WQw)O}kSBxyceg}kPGb@Vjjg1LAI#yql zJpBFpH{*v4CW&lL9Se%k3`;?yH^$=A@lS)*sh)^9A(5X&t?#0t=c=Xb97Z<17;6vb z6^LQFfB!BZUHRC(Dhg}d=Q$FOjf;DIoTHkTU*w+|kePndq3-BwQ&3<6v?AdBIwh;8 z9!0OsmKhTgPi+(J4KBwA$VZXmea$gem2UNSGe6gc%`h&36Q*l6G-6S zW({wG==QCsFThcuFf&H>djt}xhO>Fg!%1^-Fe!NJb`8D#7ho4OUTWTE06iO#mN3}- zQvhOt_Udl9dcH9?6f8hK1Z7O*SAKwY!?{SEr8w)>DOKOQ0@qZ$PCuicHjp%TL92yA znz-{mG44QxEY%1s;N}C> zGn4VYT_AcE5*sU|EqQVa#2Ju-8RHv&iTHV3Wlqk!6cncLK8EZYW&b zC`xVDOv;sRUJL>u0*~)ZnTy!lc~2VD-_+~Xk`kmTt*g-Jz*ae>&>DGff)s9e+#@*| z4PM{F?RnGofM}D8-pK?$FG73caxAABQmRd#n=d(KWh8(8L>`=+w7gevH(AVh5E}8J zc`|GG2_zl^(u8)oq|?-DcTDjb{8smHAQQN)54X0qOIN2IRQ6<42LZ+jlmh{-bm1CT zXQ^)$PJQM8PPK5Do7!M~P@Yps{WMp-Wu^rKKo*GcD0qZ+Is>0-4GdU%$3TEa*qtM1 z5+LJg;d`#%+juIhk|st|#Z!-ol3PN-UNg@O^GEFvrqkqkLe`HA%I|`J`0lVpU&oHV zLn~_TZj-SpTWo4s_=!mJ-$BhR>O(2GRH)Ukg&bG+!hVUrIQ{V&Pe%z1snqKF1C&++ zmE_X_(6c7K_U6%^q7@LiGt(jiSt(w4CAMzq1|cX?kpt`FxC1;5aq-2lT$t5FKUf0c z_lTrH>D>3Ys^M*9#;-wVR{&1*cs89mbwi!5OI+_$u9kdR#{m|DdJl{ zi0k;6xHggwl`jRXnFz!=mH9g{F^SgBej?C(E@dB`T@gD-2}{|X*3;K~;ms02=3`7W zUVEiiM6TCB&f$DqcwIjSVMGILe*nAL(jLXPUd$>YLIDzk2gh(FT@Nhkt?f_aa;#AR zaK4l`sPSyBobzQtR2e)H!bos(&Q+ZZgp~q5mX_K>(04b-g~pFO>&f@__6`nD1*qXA zY$?mQ&tS5&WE8kM6e1%dBN^pAC1018mUzv#%I*CN1_m5O#lvFq6HG%a3OPRWbp)>yL-`ezjV#|a;tQWp@UCU zzR&l6(2$RAVdSW%`G>`@8VUiP4T96t+4mRsV($%XpMpPmdEcw82P#T-sWz^5!}51G z*B>}LI{7RrN-iv{XK5Eg5na$ObN;|!oYuKM!Wg#5&XH~)o@*Zk_5^@mPtQyx2}U!1jpwO3q#7aIevc0l#6kXt}^!xL!)^FLx&c z06@8@2@#;oG@t^qq10jgRHx(^*MLm0J7y z=-U;7MBiPq*ZX*zSeb%&DOy=*LC@+Pc|`?OE)c~BuCK&fa^Qq+ySuZny&R76H)`H3%+v_pCBMta z$lFy)#1Si|7@-2xvYP?;xb8cNqdQ#2SP zl!DdM+kl1~mDYpZ$Y5dNG=m@vfFGVnd*MvdC2}JA`2VghIlK|w(oAW8<1C%SsNEz_M8{(#Eln8*)+PJcgBhF(s5lE#d5t*Geg z?+FMm7`K`{RQA@Y6HxV(nrt;l(F8p!Wu(~i6t2XQAZy8q)_Aa4>7zyjg} z(59=S^XCbF%;{Ve@HErYO;I)SL@nm0CsIT z?s(8yfC1VvvQ^R06ciKy+(E%a1BlPDENm={Qt0@H z`WF#=%ORxg?a?xC-rU8&z*t;e9rZP+paw(02$;gCE2cI)_eRpTn@4u{fzmrZkvmaq zh&mWTc?AWt-y>G+931jy@lb(W)g*Ks9g&^62C~@CZ0Ycvii&$isYVAx_<@m;1pEU0 zALGWZ!hZvthAFtbCn~fA3=m^*5@uFmLII1u;MmxEeHL$BXmA|>Tmx)y-d7l4u7ZF{ z_{8KS6B8dvSf-*J&lZ!6jP{$s zbOk0QB`r4OfTkPJAr37-GLIA%+5LA{g= zp!K2wWUmqVuX78sIPZp5x)VgL?i3Uhd`vS&Gd4B`tB{(Nf{o@qNY^p8A z4DO6v704vlbTj-?082Bmw77Z-0E?pqjfvo*Bf*%EcyU_9HIv@4GY3MXd~=B5YOI#gqSXvQrXeSOJ;UviEv z{CyvuC_in^QKgCeU3&))uk*W@^TNQVO)V|`fRyQ~Wmz7F996sMsP8#7$7|?lz7w_g z^7FfgSaF2MgcF1;2xVnufoL&6>`gbFLYycq)YABWe<8s_2kE5d>O(Soq8Y$i-Jafx z`|19<Y#`PbSr2RnD5FaPSCBbzt1X6j%ROtOOwcUsvJFWyf zWHjMVV{%aj6~z6Uo0|w|C(-PbEB~nn;mIMN`8j}6F?{uudSWFs3!x=)-HE9KO)4!y z<+#Wdqko1Ez@HcxK$ZqBmUC7HvQ*jXM)e-IUoS0Xwc~&$Al=h$rP*Md-Gbv(T(mTw zGpi~FVj&7QY{KGsAZ^w7JhxK ztvjv%#A5Sp|JUIsle5xidnTaGm_T@sJ{904SwK~zjg6rpJZ_ELDkkYpbVymDk4*Mr z;W4H)P%4o=N61;7mI}bGcS9qiTjT!BpHjX^PXmOEpcKsiTbQO8G=zW_#5Zr=WG$~8 zn)zIppT2bk30}ubzu(g5YoKA*mvG#fh~64UKTUFA+HC7c)s9Dq7WC-6nZSvx<0r!c zyGnZfa(<<7R9wPMFIF|~sibaw?aKfkX9%#3e1=I=&D8?Gd&3vrEu83Q^_oL_-TM18%FhDQw= zh!l85s$`x#VFx8l0N@Mcf!yGO^7GJSFc*FF^&;*_()gbf^w`kgwEy=Rd$X`b@HBat z-;FESUhhBlKHAN4P)QJi3KUQX zY1!(RZrsGklBEB)TU{`Dwmh)|bQYJsae9EYm~&-+lmqfwy}iAj-o*GMgC?vWJU7a7 zBCAC2mZ~?tD^q99NVl#AjV-t5#fV7;{Xj3Z70K*6C;oxAXwsk=h&^!&vA|)C8 z#s*I6LTzEir`<$fk9P=uH3b+iF_t15H1jj0vb;;NdBAS`EJJ?iDR>H)%Qkk5zeO_9GGXwo48Xk&FuqfDmOV=yc*Q zHwUf%M=_C?#KZt5{J&QQP00Tt#0+M1AOWZ1owLR3xtYYF*Z+NsnVRwc3p#{Df`e&e zDgPAxrj7vk{DUHD4S`(sjxh&FXCaRiPZ4u1H%BaK&%Qxm{LLvv{aG3Xuj`SARSY_C zF#TbdyF}F%=XC#@OX4Do*V6l5Rew?>(cC-iX%SdhgM=`3&aQrA%TM*9FT!jWPoNc)z7c}NeK%41g$pEjmYd&OD}GGJQesK;YRR7oJcP~ zIuRIebSaSf?#LAD0trCe&eM)w4@BZiG{BE9In?=&Ho=Rq>> zCNft25+{7$ld|M$-)Hl?2pt6lo2dAN9Ba(eLSJE)WwV0|n9;A6^Qvcso~H+x?#q!n z*MMin!D{qd?x+=XY2?+;L=6#5<2ijv{UP_5-30z)w2o*RF3|fB7e~&klRp4BLuzY1 zt%UyPvYf$XE%aqqv2e4J*1W^^SY#+!8cBR(MMmBB`SWn8?=rb081zR5yh?hrf0jA- zJEh#YI&5yd7|yYShPIeCU!Oe~858=|(ShUURynjK-M#A6b+0#r?cb$W`|7qolV-*< zb8aq%=Q9nL*$U&mU=VnwHPieOu6yfKr}OEhd00&z zo(~>zJ&Krmq5X1szN9|rXSbE&>bNy{?9lv*AUmJ64M$O<_2U8F!CGC~LLJw1_NYv} zFlu6LryKS4d|qZNyc^1pPArUw9zXRw%a3(*b?xwCCKrGyhf?D!@-_kp-uBSSN!s7I$RVH&qq2XbEK|$E^XrnJj5uVF$4Qjs- z@$e8y9&X%8?(da#dL~ZZmqviAge!Ab^Tmtz1oO6!O`-XNXNDy}guv>*H8iLinb<)J zg=#uNo#bD-5~!sLbG3R*tsGL4&;%{B^_$H5vshZ$DYSIyey?^>H1~^j!vDBtE_Gf<1`*7F3>dxb2Gh2|Uf$NGvq5 znv_CptZzpeLy(jhmp-3dg#Qrp(o0!mT_5Edo4PFB0&oe_eZYMLJ75PSOT2GKo?Na* zH}SvI4bI3Q-I;Z#q)Ma!WxS7Hl7kJs2p4{(n*rR^XW^Sy^*4uK&mxxD820#<5uTVG z0h1GaVWm}7a%=6ja=%1*e-T9&*E6K1?un*+d7n}T&&3-b&zqC;6MR-~a}@#=X8ey3x?lnJ+O1#XvwZK$c0i z{3%Jt4|OqsS>MGO?!{@|!Xr?$(Wfyw-}ln3 z+ta6&^roe*z=85nBveekav=X7uWkY8@Eia-$?Pi=3YJb(d!n2rhu z(9o0RelET2Q>ga2wgtJw#cZAVmMxyK<%7YPiH3dRO4}=v?CjNBcTPGd23a&;y^L&C zQd_$5MG7vx&P0tig^~BYKU}+1Uz$A6_^_}MrtmpA#4N*2l$?^0 z`4Q+i!Uy!0e0pnrv$(ap78>6mec2YT$5Syy-Inl~S!7A>E`&57qZf+c((U=f-FXg{ zykGY{1B0>9LEo9-XYJFT&8)k+2}#T8_j)vS=KXr{Cei#X(!43%AANP7MPKd+*0a(~ zTX701->8kCk|K4htRxOR;~`LN>s&ZHcLw(x?7ZFwSiMsKDGi{6(gnr^V9*XWzgCEP zuVDRfK62br;Wsb@o%XbWOP`--W#wcxrjy)eG1Ju23gKy*3#`BF0UC0*@DJp4EqOoO z2|sL~asYuM`nUNWZT7nf(&o&+e{&pME7^QZi3vZ6z!UYn6#V_06MV(g#wI}X_8}-@ zw^Zp@nyv3$b@g@2J+l^XkqcN{T8aQ0M_yh%)L6yRRqy}#3^rJ5Rox`d-F<2??07Wt zs&$@Coh`GV76;H40P|mMZp(~u<)f)>6hQajKMg{#9SE)MB3rGIA-$fMc>^l z3n5(GeocEygs#mj!~e|x#ZWGv!gwYduO_eXyYN5_*& zFM0@qO_?rz8WuMfqv|g)fbnri(7CJd=Iz_ZtO5bKA^P#zXM4|$(B(A+FK6e?t12rq zic|@dlyC#FuqmgkD)cNXI{c@BU`F!OXMC5lV>un|I|d1fw|PYvrdl`ur6VUHaKooo z2B?`(Pm(^hsM(I~-Ei%~-MA}V-52;p+IY&!c%l+wEr4&}SuCkh>Yna#chW?iS*Gd5 zY)NWhP>@(MEgc$~S`?wBy}hEnum2#w7wO%N=-bPRED~%);1X^F2H%KzHWx6~(YHW7Z!R2xJ2?<^QJbgHIGq>-Wyqul`Of_mdxLgAy zvEww2n2aW1_c+)f#i}f;@K<>ooY&>}kF!&^uZItC%pxK>nX6~=g`*h-1ygz7n|g|i zi`ze$v86wMc(}m=U%v4@7VL5?~WW4oWGYLF* zz!JiuwP5r{pv~%}!C?LB)mbB^(n0^tB@vq%YxD2lq`;;{&0ing78R!^;u{t_!TxV! zxjLZ~jH`C!qoG>o0W(Be)%VrS1W5g}or$Fn)XKYwNp%AJYV)QGkMH>9G%^@)W0oQA!_ zyj0R~14Ck7z$_7DulYJaDZ9P(_uz8wrI$}OPE@ILuqJK~YYYYYy6)mHqRL*bJMf6zz=RUkC1^1dO>AW;FUj>ms2NBz8 zz62B&Y*sNgPFpVg&w4mPvO7ZGLiLk2%~LhikY1Hws$tfa{&9WabFroQ1o_}b{Tghh z@0YG1%~Xm+L!*`OrH;o7M>)0F$8Tx=m|2!`D7**7U7rgWr00!p6{pd_xBxoRjdq) z=ZfhDL|Fu|Z5Q$6XU`sk!aXp55mWyOzz_B-tu3rvyf@&Ek0W|GR@niuYcc}HAD}>$ z5q0Kw0k;#BTZ*jv;d(?| zh2wP*NOiP@#XLpLH-NSSck~cCn80f<_&TG74|+*CQ?ud*+y|ES;-fQVM0&5!IG=4Y*}B zp$`G`2?+y5WI~>!N{S@?;3v}rek)q~cN_FrxEIT|zs2gW=i%vt)|$Gy5rHI7zzy5p zg|GxWjA=)Cm~OcVAuxlc=2oWVX{PDTfI%~$unE4`qbDp{2vA~De9)GepHIc6_JAsmigF?3 zn+z<=!a(M!Rbsk9=HQ9VWKwSFP@9`h6M!Q!1tq{rN0R<0dHi~rI2)=u&bS5wrT(YFmZU6Q%&N&Bs>kpm7} zldY9QV9znUed@naD7+X?Am_#M+ppSfLoyUSzFuXopdKiyE1#%OF^Xt@F;#d z`9*~!HC#?fpd5x?Ec9gusz|WZnFR0OG6$J#!|=ZiqX{m zF}Zz9^E^#qU&`^T>uO9cM#50D1*(6Q_VbbJH`jN-Y~c5JtOvXljrnp%Sy>r1r`smvJ&^^QQdU-WW@F>y+ECx^{vR|nNr3Gt2$+T5uYB03q!`WgqG70FdE;FQc#aET$!!fqhB znEm|ESuCHt&fz2Q%rQUx00C-9twATFv(s3bf~o&rL@*UN>A;qRuJ?mLGs!pdaF0G2 zwVasXG|{U|vQJjEhEn6ca{q2a)L-5ZsulBetvZ3CgDFV!gAQ6O|HJ*otaTHpS0Xev z%TWF2)051nO&fyNf+9js!jbny02$rJ`h5(&6h*yfB4NG+{DVcBRT!9Z1R*8=95#c= zz6&=J2txpP#NB7H~y-dJZkyL-J${b&y{&3=q{_Nt`#xvIQKO<{+79ai`QlGVBQo%pnX z>j%6BHQGULnO+JzBJ#Iqmtf=b-Y#Ae0^;@XAv zR{$MxCmHmz>c_++1dQl@?6-KydI5^7955bX-ngO;epY5`Yy8QM;P>3I{~kgP4dl9yHy#8ao|RTh^&||M{V(#~`mM@s zdmmnef+&L04T6HwAsr$J3P>s?DN1)Y($Xc;EupkXcS{RMcXtWW@s736KIe0N|AvO0Dv z-m&=aHwoaI$82R!#le5 zn$F%Ql*E!IJcHS&LVTn6@?bd-|w?X$h6pexUq@-U=X=YPJgKcr!!lX~^F zJ99}KO$xtyaXf$`EadKl$0T!L+n_vsR0o?;Cl?)EuwPWkO&I5iD~OBf3CX7Ld)9&; zKi@p6mpcchiL0@^GI88+NhqNkd(zHlYHZw;6{5YMp^o)=#!aN@Uixl7hDAos?sr>u z@#au!ObNVSdox?n5?lX$AB_L>$<~z`>ayZIA2Pi0D4jMR2Fnm*S89ZMbzK8$#`RJO z>^EcrKEBVWAFxm&Wr&73F}=bJYU*kWmjpO^;G+&AMR6ymhrQX^X*D&s*2|QC5@0R9 zGspn;EIYdu_~kwuh>J4Jp&~t#v=#dAZk#^%A+GJ9WHnyY^?q{&=Z2%>J5t8zM}h}G z7j}X*8ziA5rJ_fF$Qt#V^)52iX zwpGl}R~34UU>z$5W>3X0x;PY}}hSR=USmjn>Zq;`0E?Sd(#3IPx6O}Ic!x5PjPAS zBxt$K(ZRYcRXH~z$?i@9w`<6$AER0aaGk-vlh~#UUG>cyARvN0-rI@co{I1|6a*ye zG=j@Z$jz0(h|kihVj}j z%GZB((%CWJoG9;FJ^izPvmw#x=Ija)AoqS*Z4wYpg`vP+Y-wf1+3B_2XNCTuX!rRf z_mw@c7DU1DF<+VXG2e}i5jP`4F&^4GUOH_u|6V=PKfG{ITF-~yA{F4PetX{X0^B~E zdMPGA4NFQ!b_b6LhhCCCvG2L0m>AmIxAPzf#xhVLIzBz2eSH7M*cefoVjL|41F^8X zJ6NLm0jZuE6Z}csh(itA62Z^K zm6acGa1PK!bs@vmw673!^hsW_*)it#5Dg=JIC^cNlA@xNz)fSw-&T;3AqO$p9b95` z6EZTo`$^(2tnLU-3f&>b56;TEj<02oGb)?JjmK%wBdTU}BQjDp?Z+2ltz=>F-VuNO zngGZ{1y$YQMV7xuhSDo?a1ltF&l!~~`t94YU=}Cltd6BVG0x_EpgKA}22297wl~{5 zR>8rctwKiyA99F5k7EVi!G4MbKAlbEpy*{A_ozCtbj131pSq1aQOpgmhip-hvl;(p zW_ZMeq=5XWO8A&V%*)G5^KcCx=z(|e@Gz)EJh-@iFKk`ahJp_1oq=Y{SG@MwwI$#1 za7@rHg1{ArR^q|i^&fFypwgC2RqMYQL=+B=wlO#V+#w^w5pa>DOX4%aBz>c%2AT`9 zVwaWM@PO0YKVfQWzBjkNjO0wdI*b(E+@bmin-Wsjofi~0uuj?9($Y9Fg7bbU^LV$m z+>(Y94H*8v#)rLN$uN9+1i$qL!qU!0QugqfsYX5Fm>SVPM!Kb>cOn!J;$Je$sM>Rx zmEN0se&M7~d-VuuTQoO2`|;&WUUM(Sn~10+-ygqAa&imB#Kk`;{HmyLw_5)}vU{-G zG`zzDf{xmsy;UEh=)-7eg|NY$*BX6Q9}d=k&rJ;S!C1rafekxen{~S1>)c{-41v5( z^SmIY1e_SbuaL z#5N8LG|5jVeEzHt_3IB(Y}q7U;~!LNM6Bt>X(-E5(P^8Iv&Cy`SwCli_}SI>%*2F@ zd^0Jy(?xwbGm)IDoU3tC1GJ2w&PL&JZ~$8)(k)g*qL2+%W@f9z`wO8^?7!GRKZnHy z&n@Q<6LapZivOWG$}q^nMko|whzpUECl$miFkqsBg-Ys>Q835r3H1qiMU8jA2@yb0 zOdVRk<@t(?kt{YLVWF5lxwWgX_I>Q-{(8dh-@pEu(T`0{$J;TlfA|)Nm66JT^htkO zAm7m2+n7_s2&O#>=z@YmnXlaj47qX`3d28r1(SXqqxVLbh5*|lw}Gn$sq;TP9t|^E z=Qzpkhdcl`aK#fLev@K@&umDi`kObY=4RfRzV|`eIk!2F<@+=sy{XCkj4|*YHHmLV zefwTZF)fn)QgIyNIJd((S5Up<9~6|k*Yw}B(~k`*|LY(v#fFaqivb-ctj;?RL@$<> zmQR6$1o7&(mhK^vv*HQ=PaC5xhY`orq$QbxC%e92yWMpV&;^*&A z#FU?xIaFIU?vD7N__Ya$JKp}v!nZ}Pp8WXngBqTqP;%i9Wy8uwHc_@c5_R#H-7F3xAqb7AkNj?yMkM}0sksbET_chbY7)#zezMP&E4lvu=y1FQ~0KEL?4oZ`Yq zzVj!czRvcP2Y#~!Im7YGgfAdS{* z5A_GCsBfr$|9j1!$cq_Uyl{3)b_NsUa7$E43GabilII$y!A#NLv9U4W^JS#hbTe2| z;_;v^6RJ=dRMPuiyJl$mP4a^W0$AE1>k=W97(5O*8Q(7{C8H}V72Mq2S-=)R6qtZj zU2N3%=BJGW7`xX?fUX9b+Izfo_(&HkaP$Jwj&F804+!xC zLEA&aK!<<7^_l9~i80`K#LJMK>Rl29fQ}j4+jsYXor+6H2tuov#?~I}uMay$Vc{TB zzf=&Th>Bu{eSA9k5gqo}du(i&U^>d~(fAg2GjL_|%Fm|&hA;RQpsoxkR+&CTZu+y~ zY_R8wo12F!XsX!RmF6(8iW#b+-^+N*WxRE3)dlk0u{|m2gYBp)6mONqE(S6 zdL|7|)X6nF(@)`3F%DK_p zJLRA~Y$>0~6SJ|2(yc7x=04VM9x=?TzWc@hMtkwDweUMElD3c(pEXX4XEHK^y#`-& zhv=zdx!l2>|DUgZo%{dmObwsdQFx_Z)Ia-iMf=nTQ(-glK#Y6v*qN#R%-!di_*@3g zw1mRkd!xsi^rE=Czf0=mYcKvK8NPLwe5j+c&E1>;QMsU+8jv}o2Yk}b7669Xp*?-la=Sf z!NhSN7urK-CoG1MFNJ3#%@}K^Pnr-Xy4uaM{gZpUTktynmF~YCWPeX;C7xds@#A>|^vx_2zC?M1D0X_WI%5r6HnLh5CH3*46b zm!{E-=ATWSuA2+um~Q6o2;NgdnG`2RxS;5nm013_x0@xYYP%omhO8Uo|3+jP76C-nFGtPB ziykJtcT4Mhh0*b(wEWhf$mPYO2eR?^%w+$2)elN^f5jRiI^Kcd;Xb9{>F6LPCg#)E zb_9`N{~Z91h--@6@d@!XQH)v%<_xyh;gRAsysqFm-;CxE&)L_8Q zl?tL5*7^g7^P|$3-M=a(U{G)8c$IWDr+szIc(^llKk0AQ!s6mB3=B!2DHsv_`@G$` zXn}T-lzNZ$E|^t@O-_=#9yde^(&Fa%Oa~e`LcsR`b~q?ci;bE`ax8K<)$PG2T)!^~gNTUd zUuH#3Y=E71zNdEvvWYf=8aT96J0AtX@PAD$WSkhP5j_3PiTHps<8*WA@oUXt z^vUvhT_0b`w_7nL;qK-+LK<>D)UkV_j{jM!T#Fs#i5lABT}uPj>7q}`BSrP-4Txmo zxv=4o9j)EKShaOFiZVw<^#;YpEvT9tPmX6d*OWlX;agI2^Bm2KI)*Xau2|@Jb9L57 zGTD(-OKbY4U<$a#Jd`*2jfoQ`?)Z*QQ86;DKL-U-qTkor)Y=7-vWUn;pYHuNUm{DgoUYd3?T(Cg6?vjGC$FilpFDlq($ zEmq9_zOI2q7)Y!@m_qaTK9a>-R%S2J^=aDxL=7ARgYv4Xm|&a&<5+rndT3NdC7emW zm#{#$Rz9-8Z8J5;=RSir6+!*r!EJN%ZKN!uGz1%Z zQxKG6Qf?<>-nc30rU^uRtV{@S0Q~qK{{u>>P2tuK7`n8DB%oh4P;X9D-?YCSTCp-s z0HPkq8V;4N9Fy;imRE~ifZ-SbO)i^Y7<>0F=v1AYVswnm13*T+xUzDOnhg_R4?01? zn(%~}pv5nPAVl}e4I;aDFJo?T)9b+I>l4XeR%4n+ZzqofU@X=er$jFwo`L~t8u4fC zP}TuvG-`Q{hU&dL+Z@&iI;jqWKI(l}eh_G;XXwo>EEr&_OHZx-B^PqWH8-aOnURE( z6CdO%kqs}w3&3NYR+U96;NUv5aYEI73q|k0QvyRwBk&jPKetzQ_l-3ZJATDc92zS!*^tTmgen_r4 z4)$ffT}yJHWpSs2sFk+U!g{uMWh@#s_#iqOtv-`d=eM=yYUapKOX$qx7h;^hkJ0>;Xcem*9?j=FL++1q{qE0le&Ny-?sjmYVPM88rU^J zAO-#0{JLNGFSbwkL}Z7Bxt^+W&z|w6>%Fa}IBRL?{}>R`98w`F0<$^OaYF=-;d)o(*gKWRQbLvtgq z%F1tj2(3;?0|p5?$D=lRLR?~0NqU3D#djHX1-JhP32->aYTMXwA{r(pFs6ltD?UZ} zY6_H-tyllq`|IzYO$3r&tHa=;uIM}fWX`}glbM&<)?Lci{ahkOi{ zfYz=zJB71b`rjf#q*j+F%L+&q0IL|5@?)+#B`QK=R@_ChPCQ8bw*9K$+{9$%mt6r( zBOs+nd)G&~kM?>K3Xk~p%~b}o`kI2{%I6b8Ospg#lgpe?y8HG=0%@C_r zfBx(qzA2nrGjK8EW)|bs)9xG`i*t;%)Il0h;~ob<$_UtlFR_RDLNg* z30cNKz`qu}Y$?G-skz#|0^915LiP{=fAQ6?c|71wOm#4f}y0baSi}jesJq8VR#?e6L1YFPl!BCfO1^5gE5l_WnL_x8|tV<4mwT zscW6wq+;K~_o?phf?RM}v=dNStvI~?RtTx0?pCuYcAL-#1eORoKgZ*KdL#q}NB+Ge zv*$U5xobaJ{N`p3Z!H{pU|-F&#~=gBlf*H@yWWF~1E>hXm64+|>>OP@uHiL1Lu1nq z;4c>!SEHwjMw%Dwouh)UK@m1&eg#)oU6~&Z!*DY!G24^0}ePiR_c9j;StqJIsiinT7ly`E{RI?Tr#PRCTcX z&4I^CdMb6{8)yOzZy_9*G?6(X#fM#w86G}r$&eHBUhkB0J!lm0Gf~4uhssEI{U8oB z@UTLWJLBefWRK0^rVO-nDU9Rn|$wR{KT3G2#UDqVc1PJgK7hENdZq0bAhrz<6 zbU%3bDVlHr+AxqsE$ac+ta`SzBDRa-xCM){;}?EXvpg|IXye?&j~{bp&>`au@-tP{ zCZ6cqFA=>&u46Q#(OYt0%!q(IDDW1)9=v8SGC3Ix(IHUVw0(IvXkqx_c?MXn4I)sB zEOuniEA-x!uXV2-`WKddn*d!QT39Fz;#3g~_j(fQJu{;TwrkFW0VF3~hwT8|*N{#@ zfIR^}3L)@DKvi8W4RB?UHv3t-U`S@zBLE9zVdi)NsUn}G2w+eB^7={ppT}Td($3zaYF*w7=SGyoMGvaf6FwczRwVm1v?cYNqfU|di#~>h%jN%G7 z^&hA+mh4|^YhwVM$7-=dSx5x(M6hcH6R>k(e$A(MBBX9?i zuG_abCME=kucc^eXw1z>&%OezsJB;3eX{4I+ZM=BItQbBep?&(fXjpzVZKsPp%J-~ z$~yR41xG8OT@bE9WA&RDF#EYC-+e!4BUZ2K;JmldW~f5t9))@a0Id!R)Kp1B&+8^$m#WUpZ_(A zREhwnq{?duQA>zL$#8QO_Wd8?1%XkqyjECGn`xoD@f;oHm4QJ;$x5#`Tv(^dn12Mv zsXFhzWRrY$RvWlOCK^u3MMY!9x+;iz&i%KtT;KvUfD7= z(Po6lC5M8J4Jj`NZ-{<&E2uq?LhGicd&N5achRr;Q-dNKG;r6TC7Qe?$I1#N0xicQ zpwS<~VbzxCRE~5r|NL25;>-wA^uT-{X~8-^d>saBbAOglf0SyHs8lL}P8RYP0UeEC zGrk2rT2MF!6_?AjK+vCP_Mq<6cpn4_T}OQU59SArEmx zEY3|E8`y`M+u8_8)>gtN-xG)2m)>bRzZL|=>@5$N9?^wL*mh&WYS3K?6M>#l491}8 zdh@tMA_JDaP0bl>D`h8aDr&O>nWi8&wKKt^!1G69iak9PfQYv3>G`7|Kmq>|j!>kK znKol?bV*3>Eu4@kiG?*k}max29J`T5UmWtd!D-TJf4n+qsJQqy!-^%dZ> z{|C20lfZo^pZkWL+>em%9*poUz)C40K0fso$Dm^c={JZwb+_{_MIbnHUxFh_~SHowPQ3kra`BI45VXuZ$L{cM4+rZECH`wVf&Cw zR#p~%{26#%fL2;wQ_0un;lm;a%MYNbfBLiuf~pV@Y`R>n~JupK9~Jo2qr1o2Z?+MnMuqi_~6#4{QH#(**d()9pbyev$OK( z*L{&HOz?CnHs2zGvJDlHVY9qK^Z{^R*iO=5!GZyV7_3QS6JyIuSAK}wMfw@Qx1m5t zFDSbX?vegb$P*8b4G;U-e0^wXWd|M^R3OU-YO^1V>=LD4N;rytqKUnU0L+?!k=ZjQ zh6l1J?!cSX*7krHySeT)yq{I!H{!Z)(Mp7R{i9(Ygb^q|^~Tzl{v7Z~zy{|SNDBy{ z^)K9As=p9lxp*gqj5#sm!b@4a--to|H6LEy#q!-E{s9{IjY7q$e2jP|Fv=oM8i zPX@;WAu8-?;Rr1k*DVm)hR1bS9$nxxG&kdkHl{(A?z4b*vHf#E9U}!wEl3srA(4?9 z-Xhhe0%s-UL_79PY(_=Sb9JZ(c)lJ-+yFg0pS2Auh&+{>L0=3L!rAR@&)>h_LB)uI zNYvDXX=zx|$LGl5eNVjH?ADUEf?ivCO$|Q5($c^)93$YIq_)GQ$G-ND)bR<~y6D0S zr7XRr3^@qDRFfE7d^mWAf-LCtl>ZfM$p+QB+f`@6$PvLvI}QxM!OMc$>?ypPwBuuR zn2Rs$_X&$avM`v6-T)5`upfZtoea$Ro=Bdv14IL`YkQW(Fmf^gb`u{%1CYQ6bo4Bm z2jPIxVRKBOJ%o4CFaJx((DlA*H*!@>VTuVEZmv*$Oik4RjsX}z{2+Y{CSm|{slJ(@ zzW0cY!PkoSo2p>P z0ymcoJcAG)PEVbIh$}APd_9yxBvi=d@P$F`*N^F=e_4NAjs2ZDypfLSt2mU&e5Zf* zi$b~GJEC$qP0rvrBxcU@6xt9N{${p{A!}1OuJa9z%o`0-+&|3EGsbBk`|>8*dkSi5 zo^ry@I3d_u3)s~aRl3vX6G+EoUkwy66o7sd>FUV!n0tOsGTA4loCnIi-93yQ&{`3H zu7n8FmbNhQmu8QZ-HuW8yW?R-69Ju9LpGHMymt`wgoH3JPjXYGrQHEl638|b6cv5U z-f%DcS#ky7kpen93D6b{yB^*N#G3H$gX6#Dd-b;8)3lZXDvt(mB1A!GO;1-`c}haD zRdl&g2$X?^ZySgY6%}_XEjM~zrdH%!2#Yvx@^)(%VYY`- zdTC@eD&6R7ekc*B66ZL^itSRxvv7&4ljSf6}AN;AD$YDSIWmL+*{LoJ;Tg>t~ zgR`=B5{Hz%={kPLM_LFlOSeu~8Vlh{cCoSzz3@J0fyLNmX~neJ?$Ic6BkhY?#}%_!O^Z zf0)d1E81fY>1gUceZRQ|Qp6WNIt#V1uo%iqtD1-#g)QPWciT|xZF3vLIqH$Zl>nOjH&8P zsHG6Ncge8;V*+B(0wins{Q;X|e8L|%E3<1`A>TToPy7tF8Djb@2(Z=xw{tk%$YC&2 z1s4kHbw_6>v5cYF(FkuS7BLWDQ(NJfCiH7_aS;`)*PHpL1O;_u9(-p1DDcAjnD7oPtdwJj$+kX2TMkNKAxU>f{K!2RAe{ z)T_Q;#C~gwcfES5H>>9j8|ej~+bJ)YeIgc@ zS*RcJ`vvS-^(HaXv2Z6RUc33r_{ZWR^Dc3-sVrKj106@O><~Fii(QC%~o!7 zq3<*IQf58e_KnKSYsUm!Tj7tBLWkBpnag*l*^FcJ&YQER$9i~AHR{QECs&w`93Is4 zn!p?I+2!Kk=q^YSST(c|%N;CsZ`Ob6C3Lh>s?G$@PyeXuj*_~%deJ!3@8MURT!KE+ zI@ElvGKz{=aMy!FLKM^#=K5z`$!`ll1efpy5BKok@81oJi|9QsUj~PUrt%O!my)94 zyMHaAM*{F9?7yYFo-JSS-|EfxZ(OYI>|~c`pM7tuAP#yTs4a4JQ_I7SUT8`ad>J`P ztQ|M^=+$hS@9r!kFO+AqZgzgAuM&M||G?s@#EX#w{}?140rP4v>8kmt{>;Mlc+Q2= z?wPLv!ttzUYV)F*5*+4=#!M;A3f_MoIOdpgTH&ty(-KqLtnw0)7ATUEUMcrp5qzt^ zolHX5dgD>FYaNZx@v-&j7O$0a0b8<`!e;IF(X(RQr3u2<%xN}3&ZG3}+gLTR$ zNGK@ob2270G(6!_#~yl3;N#;H9uW~TWwg>W;$XGfAlyrkyvIPWlsTHm{eYhSW0*8G z{{uq&Yu;LNZxuNn54}$06|TP5?UGsCyhbhJL1P@^cXXR#F<+?Kt&M?CyQQTCJt_+C zOI8*%9|5gz0Xrw{`i(Dnn@Y#nIC8Sa^#?z*yR#6G4DplsUg(JlsV8kmwtox_rD3Ee z0`8l2SacgFzrZ-3(>(G0mA;5b<2HPJ8C6x@`}d!X=8edu!zC4#0&g|f)5bndF#9V^ zzOpdo&UJ`#DlIRMjEHC*c+bedBwPs|w z%7u3Ar0XVDyh(I)ba%<|pGvxrE$HdlI@q!E@;=e|5CHv07LyX>id??X(|E40d|Pdd zZ@V3iYWOFOwj#!VOKWjuFljC$&1c+s)RRKXk7^nhTX+N0i_9-1l21=XA|m3J%wwXH zntAH~nwi(SRhwMBoh{X5#lt23c_^?GH@NUuRGC!UPm=Nk)kL}Q&uy2Yi7j&X9R>mW zt*op9&%9tRQ*JWWnjb+9hZIe3iz7b~d-O+_KoCuJbK_T~?AlzG7mC(cN0Tlh6FaAL z<)*bsWESI{S#pyt-w7CtCrn|7ccHR=&8%MIlV5H|%fdpLQ6B+Fyi69gx$1ZFd}V1( zGi+$3Y|4+(9g^Q4zIEJYeuVIEe&|AWlSbG*v7*j>u{dn6`@}?FMDOV8T{JB9kBrtr z1zgGOT)fYf?QctXM}+^92ikt)ebnmeVdq;Ml-u9?|?TwNfd9xOR>1T1eflkRHo&6%Z>4gH&(plE#mDUG51Q*lGD~^-KqD$Oc?TO zo~u}7XDJ)t)G$83Y}YSv~$5*>8@&lr$j9mQ7junqA+cBDk}rvmM6;6-w1fY^rkox5QC!q%$fe7AUC#lRYlHREXQzh59^go{5o6P8zdv9qFPw>-?3Rf zxZyBySjO#~b@Y&uj4Y$LBU;_@j4WQN6hKY&)-#5D8hpJVS7Yt zPx!o#g+?9pjM%4= zk~q5#eQ@V9=4|ITKD zGLM#?sp+Q#^SbCElac+cs|&72&sxjtE_Q0JE_H9$oZa^Hd{8!Jhhvt{4Ed)s)wvs9 zbRn$$SEtK^lMXZVwat{2E`P`bFOEDUpStD=9R9@9mQyE?BE7#a%_J&_rSJKtN2tZ(?Gu%<^?eJTLPy0^w7NX!ev%-BKnTZ)7Ulf zz3#p>oI(wetI=z_f_iX$T9(4UAQ$~!e@Ab}f@hy=FYRIV%w^!WPV2DIzfzHHF{`aa z+SDGR$qu8>DY`nZJBlRbRi1YS(x1jKF9fUgQGJA2{+c-r|3flLk zdt*NRX+Xnq_IG0lY>J`uh+JHJwCuE(FYzr2{Aaqx z){&>D>h|@QSZEg)+ht)Qd-wqz9dMX@_qY<6bZ<|B95a+(Gc)aPENUdw;!MI~;?Rrx z4d3lh9a^r=*4?BFK}iHq%(}g-`h~3Qvz6>-^KwpMVF|T0r^DK_+jt9lup8QR4GYMB zeZ@I#wx5R`V}Hz?nizbqqf$q=FhtM__#*l z#K8R^wQ}>vG>wfQs^7F1K=bw{7d{tW3DmE@el%;aO?#g&j_2sybEzvP9GA*Jox%3# z;JeEaB~&V7br<$`=NI60q0rgguJhg&zDG-YJn4dy;FBw*{gqOe;hYGv%CzV%w z;~N%{2~kU-XyA^3{0&tu&Qtq{(IR(dMinMIZE-7G+fR7(eU^*0IIeB{mb;id=40aG zJ{wdN-eO`kB@QC*-xmb=3hpgQ!Ym_su-as17(PGvWWT_VZZa#M?ryr~M11Qy>~9wr zXyn54C#MXLe6cr1zP}y2dsK6Zah+GmF%)}an1hY2q3?2rbvf92Y5QU^yPxdbacd|k zI7I6bgZ@!z;1Z)UQuN=y>TJHXWyUo;Qv6*9+pctI)8z;UfgUfDH&YZ>QQ>IIFG4Ph zQ7beBK8Y#Q$t>2^`>OR;H@D34Yxb&EusrgWo0A-7B%h=P@Ho^r{hFmV88)M1W~LCS z-AEJ^gjs>!q&iRaHTUjziSh)aHV9aB6# zJ^T7KUo3kl;?t++rW#DXD;h-A^7y`66Bc?4es(fyOz`{+j19ef_l~ZTZwx%>*4Ie| z+d8{?^eqiP##ixg+GFfl6@SV4B0uu$=ju;}>)y%5doR+{@2#zEW#rX(K>)k4@dgPA z#gYwjSm-@P08 zTt*5eMDZ|T<>268zlH%VW#3|pk4xT{cbq}ys!!jT+-z-K8MR-j{bKMAo*gO8m)OnC z%>c~|4o$t#wkX`u$nK&_@P@^tfUjJj-VAHgig)wX>(7J3!%uZm18Qn~3-WEUL~<1H zeF5;_t~_#&R8m~q*vPD^^4cz7kr8dA%l?8(l=1`yShD`j2Erq+br0)S2=Pl+;^XQ? zW3_w*lZca9MFq8?-1n&|~8k+cng{{Zj8Xg=0*X-}R$sN5K+k1~=vK3^_E}j0Ab~ z4{&BVQ`|m6pv&o*%SjLGRRZi+oo71~E&T8AKYZw$8m_a+Eda380%WqofZ#3xg_wbh zK;Ol@0T5vZqS=x!2P)(S4EVuTIsCKva>14ZLe_rUIRrLm#n-RJ)SRxh%O$3J=zN&0 zAvYiY9=8|p!u@1}h}U@&{q^g|iH!Vm78b0Pd}=K%;!0XtjV+7#(&nn-t{P0xb_<>b zt$fYFZy+Kc+9ZTuf;iD zHS2AgjOK;HQ8<3 zDKJp_YfesjT3W4fB+X67lh@!YLPAQZKNbHvPpi}`e%v|vqILh1in4;bvglOa>#+l$nwm1fj4Zn%CwzEV-}oD2KuE~_2mGHe>$Ld!$Jyh&pdI4m=a6x6 zNpQ(~-*pp62+J+S8HT@&l?n6GJyOF)>}zGDr8)opE&cQfpZ(WNSo7LJ+=k4Fp3Umo z8Vxr~a!5iMn=pHCu7HF5>(`>vHk?cEGl9~KOF+p zi66;@{If;!*%AeAjD@Lb;>ffk8PwhjVZz-uB3CGV7v~s)hpqk}Lj&pfA4RrPI{l>Z zIPdo_p4h56Y?}Zy%EiH9?7&$l(0a|{y{jwg-i;f8ILIdR%66RGbI^qUNJ(i-+yp$} z4k0D`%JbE&wdJky=?3$1Ghin_{J1zZ5RB`TjPbf!io6_Dp|aF0evAyOU%>rr%P?YLtdU6{jNL-PdRn}47z;~s%Dyqv$O9OH}2X$R41D1No^=AseRB7f*`jH$Po0?mV0qr`N#z=~onQi3^ zJ%VjsgFPB6U%Bh|CtvUsbEwX0(|_>bffaueeu~J>cD0A)N0?5YUt9zP-%8v-!HWI1KU)59$qpkkcS6#9dI`C} zDN1{&d&8XA*~wqll#Hw@umyOIHJtaCPv5-bXXn$5e#>oB8tw!W--2`o-bC4Nk2>Y? zuZ1|<_hKB@OUuc{oUuPqF`#$$_DoI1#ebGoP(bS6EDA>nP>r4`U~D?4AKxn z_zl?Dw0vXTt3NTHt@JtvUz2w&z=ao(_`G2;>-=bXCG=*Jo_^F%8Tx>g$rR!GlLx`R z$o}h}J- zFzv2{4!-#14l61C;yBOATNqLH8{4Vo6Oxi`)aDwQ=CtaUrtk0Xk40u=Wb~|{x+Eo; zjpiwH88fTr4`*z@1xQ8nMR$i4Qc|ey%oX_Lo8;bF-CjF1G>Dt7YUZeY z{GmF>(rk^r*8fL319S-wva<|#R&Cqc=pX3tTM0aVzqcxu$*^jx9xS0k)hH?oJS6>L)x(KY$7Wnzpu5&lgMQ%P9m4ct4Dwvc&q0h8VZzPU06_E*0igNT=h|f z$HqR<(V^B&qY?M<*WN9)GJ1BJj)l@AmbAt>)hwoizlX=a zJvaW!f6ewd!oQZ(8%DsWVsn)((x)fq8l7Et`I6goeBPMnLSTGo?1hB|4!e~VV4~>^ zSo${nFkt|C8$6sOq$Klz$lr3NJ@PoU0bT|klqdKhBPJr+m7nZqua38U^${lyBOWtDDteR$`rJ_FJY!XjT(VRk^AUyvbz_gFlLRO^4EAF;( z29QKc`AX$WWGYujvT@!>a*7n1<|&o%20f~N>Dlidvy35QO}$Z|XpWitVGG&TE&LQ`6q@kwY;6!x5i1?`)%A=f5yd3iV}bV-O+ zrMr~F$y_EbIi~gP(&E>rDaG(lyzuDd`Uj_6`bS){c21W6tW=C`a?3Z!6-s6dp8oEe z1OarQRO6iG@$X7)+ESC=fEOKtmk!!`NT?S3rXIu!q^EPjkO0iJoMwTiWRzRq)7SK1ys<@lG!uXc+iBt0;RN%Sgk1 z1Hd9OviFXTKj9&U!0aNmd}7#)0|S#Zzw#upg~~k_S9qn$q@4BOfyRmMvn45VIr>P= z_uONgCG!^CX@Uu=W$O6=CUh^q3e&gg8a*a9JT6WrBi+@s+@Z#r^yrkP!{J74fOZ7hQ>IYd{WVJOFH#ygS$MH zZDPJJf>GTQnvxRQ`R!3kv3*TVkczfP)byo043?a0lYM*~Q}%)-Xw~yg2ud8LQZmqw zF?867i^oh=AJDt48w3vN(^CHuH&l(4OG-(ZE;aabuu*I}CKcR%vb2@3A zv4#vE*aGEqf9EXMSPTul26%D**FMgDB4-O5jvA3;`Vp`OfCZt>*yMQS` zAheF$Zu1gw*FQ-odE?Pv&xlx!RZS6w>GzLIOkDJhvERF(67jC@|5;wQ_u9+LOYk&)u1s2n7J`Iaj5Sj&Ce_ z^^k>tV2GF89t%tP_=j56;&lLph)F2YzI_``^0B-BM(8XqpgOXyWG`-U(^SJ(zjqk1 z|JN$5PBXy97urDBJGx?+X&8Pk4)&Q=?yA=9Th|T){#R5aA65p%H!M%(!bh>?g>eJF zC%x3S1XydQ8?r(+_KuF~SNBwMA)Q9KaO%TDhL3<4=5Z&;CB%UX*GIs)#{QrlZR9dw zNnAboT*w5W-jxeL>7BJ4m>JnH2b{E{SM7sy?-}gJQFseL;)D5VPoURp;t&Zbsov_1 z$Hv(FNz)gjbC&Yfb;Q*}5~&YI4U##R6j#8(@^-oXM4v_MKZ%55p76Od+G;6X#0cn8PgQ&Za<% zz}fA+!)dgd+9QPjjEIzlwn(lr)53*E-W9Rei zz;B7n?CLizg$;S|mpqRJ7%K&Kble!={3xIGG=ZT6Dz1yZ=@|=&;*_i?DY3p=@YyNx?adi(Qz_*m*(EA zI4jt)wY61vrSa6z5YwrWkJ8Niyyby;<(dSblJy>I1jZYr2w$)0u7Ho2D>8F@RaGvN zdd#Zf(jW(fMfT~la>att@+-o{rC)=CT>()BP)4h177f_3H_%c!c0256{Y8iF_dv7A znh(&QLjM- z{U=lUV{kAX4NWLP*XMbv*}nda_KuDY&~ZgQ&c*r|7Dmg;>W};+S4u~B@7|R$QoBDj zCA7HwPLDhLyXhF9p$n=tjx4U4S;$Amged&?!I{O5Neaimj!pOS$&fJ~YK~B{Ydo&mfgi?8=z_=!*(#yKTW_4qO=6BKQf43^z~uNE!;kW^NO*U$+o%JaC79SZ>0uwcQ2+cHzvODlAq(O&_N$Tu|G(C* z{Tu2uj8g}htX4=a=P~wQ0MoP(&!Las7+qlaN37Lq{bY@lC z<}y;v7%I{vNrmH*ORPcTHZ1!LcK?F?{_y?f`=0mv-k<0BJfG)%R!Tkg#ZP}-7`c8> zDr-6B?CzAOL1JYoBI$DCA{@C&YqGMY{y$%$z9$}5gHS&}6^(2nk9fuS8i3J2pE-2C zKFX4{-k$U+YLYlI_W5SreT0U%cwj9Xk{8pipwEPa6hcHh=w3To*)wrwYZV+f2n&AsDeP5KJGUj^(V zRp>c|*ZuF@VBNg%KYaLzb|_fns>!*#yGJ>cgivk=dlL-} z3{s=rTHe0v1|S9xTQn_g76K>W3lfLRe8n+W)?Lm8covibw-&?HE0-BoedYj!0CKo6 zjcqGml>@rTQu}8@`cGpXvD4ey!zU_kS27BjnW_r=vvork+1Pu%=bT#G&Gx)hQ?CpK z&&So=!c==R$Zg>X0pB&oSP~`H958j}wQCS3e(-`PGyzpn)J0FrF1!0jD&Q9Fqe>tc z!_VBIE;?FSc_t(n!IdX!8tOncwGWF>N7YdB@_aB=VY!9Ol#VmOA}r1Bc-)6gi2(XH z@X3*Sdb#{mgZ2qcK3^ad_6x4MuC)2r=BCXmB6gC>jR7P7F+Lpi6j^4lzcR?X(*T3g zWOqLUM2s@f|462)Gm+D3F_ul9t*qmH?U62QGI*4vVq&S+qrp}+!fzMHRfDuWyPiQM?#9UwU?JWe;N1xJ8ZP1 z$(BZ@pG_|}#;rce*{!b5`@J-|08Dg|QL>7nA|5B6(U_R}GEyIwJnC<@M(;g<$uot` zHXCN^WJB;gT}M446-$J-uSq&^Qw@<<&pb#C5st3e$STBk+GqyR>TiT4iZJN zoSjvK*yzNF-2xd@P0e_gdqblGm(7=3D!pRPm26Zc?OWX*JP>1=)+E~C2<*Zpc1!(% zJ%PzQtm%`n$-(|W{pLd|L&HJB^2%!KW3O&tC;jI&RJC>s=JMIK$IN_td}-wM?pY`5 ziRw63#)d?N7kx4-f;78dk|Rn2zGUQra^O|x)nN4cOjcDBPj_c>?_a3YI-3@O;69~~ zB9?bNdeoj-f1J>6E_Th=5yJg6zcLaBZ9$=zA;kY^u4cv_`2h!GA~s_C^_2^hw|oNSh-) zw(AFvf^^tJybxPO+@2FoZ8XK#AUshW60^;`c4ZCwu()G+j8@PAxZG%sW=VJCNI7-0 zTDtRps|w)c?>=(l_2o>+d5mJ$lP)+ThT^Y-gTZj=EedS{@S&;>F7Exxq0N7#*6HE3 zr0a6%);|2kPo?uG@A+n9GIiDKzb)MYF*e(8yIK0*-lSe&Y&cK1Dubf`Ljgl;$DWC? zs>SR;AJ`WluJ~zPjfAc7x?`44uwbm~R qdGR76UNqz6WEU`ur3S)g1fuB2Mg}*65QPh?hq2(-CaAuEx2oNLgVhxxI5g=%$a*;9`3{c zzce(v_F8+bU904)uZU1ll14@#K!AXNK$ew}P=kPg0)c;zg?kVFw8~-b1OI?@Rg)Hj zsG1-;gn%G}kd+YC@G?Bf@if+uoF62GgC{Qt#*w3gPn?4KEed(aMFyuP=>rSnrz(La zA&R9cA?hQI9&aeoignl_D}o_M7ZeXILk7S5G(lCrva+(;(YC0_{i`Lgz1n_nBKu){ zWz}jz(3M6V|BZnal8k1L{)N;?;R`$)BuzJTIs~RM6!^o>8=`3XUtzE)%Ks11nnaP#cJHZTS#b$z$?eH54>B5>!I8}0Lk`qN zSE9Qk*a_6~IKiQ7eVs3`$O^XeKO(-uAQ(@sKYbQH7JWSUOmHSSU1;c4Wa-Xsn2?aL zPwO(s?(KYG5Jjt~wr(@^Ad z8Qz_SnH<)wJ8y1Li!TnwfQrVKnbFW6&}IU1sd0?`N{AqFK=ebIdVlX!kefvQ=gqHp zeC7~!mgA9WUJ*laB-_jMGw1it$*aEKaXNPJ9cCCHIyV;5g=X(^MUec7+z-v~!KY`QgGsc?HJ5SjAAL`umVVkqv^vf9j>qfW zUzBB-lE^2J;NBsPYCQ+_NyjdayIn5b9WB3eT&$zE{f6UPzsCac(sfxVp{ctn(cU|wja1t=(=|PGnib(*M+w4#LhM~RIXUP!w-BnT(_R;sFk)hk+ug!ppLe_R zcRXmPU$CS!s~w4(&E3)SeD_A9>MZ29Z4{#S^wQsuNuxiM$dgl0V3f~0cK3)&JJ`k* zX$*?3s!Dx5`RjPI1xY|hdoM8cxIJF;wuP4y@wylv#J|#M03ZSkI^}|W-9QT{xU7b~ z(HU3Ar<;`IpnWY2+_#S4$m%rdgN`Tdn<2hzAy*dkT!s`Q=`^L2gmGrVx$5=kuAgUh z(0QkI=5cdkl7|n~MomhQ2=00zi^JYtyywDgW7Hk-QMinekT1UET2(lMwDHgsE5kUCl|rV?*=$(|=j>c(tBGmDjtPDe!2n zToIMmbx*8X=p}*2D8N~H;l)>*&54^tD4G*^@pzOn)oS3oi67yM6LwRswRwH0m7CBT z(lJ$}6?hTix&6pBmcb>c`ZV&C5~#9trP&c0Dg>~Ks8cfAoyg`3`ox%4mH|H)`r(fu z{Y?moG03dPiJdErz?{KumwT`^h59~UAw!JoZELUihM?yhlIhPH?eHWaFOjV)F=@k9$jc9PIh zu0oa&VjPL&W?#>XO1qaTIsu2(nkjo~iTuM>$HYkjt+L~Dr%TKNoBe3H=u1<77AFF= zFB%CX0y{@zb%A|`+QD=sV%`35?lDX>^OH$=E0Gi+3!~;I8psyClDdD^0%*FHuA;>X z1^j0AU1eUkVwg_{uxtx*75vMjzd#=}`i*|E?Dai4J*8<6r!vH0eIbq`5yZewNC&Mq z!9M#LVH(OHVCUDQ&)V@2j2t*kyUEf6Nd*IteC$Nnfo=v}0q>qriR4`Bu`KyLPX;1C z+1Xo!7Lb(0x4Y6}ZK9)}U(mCxampxn&RMFd>zF2*6@QN@Zvf;gR>Pta_c(REk!Kdv zWNR{KZIeDKP=8g|1E|BH58S1(8gVDoD4~#Ibde&R#2x|8@d zFGq!Oghi^fb>BM>crjY>@Qa;#y5l+@j-XU-kBh>(p< z7z}Pj`W_w=fh~64|7)fYA74|f_tQzOl0(5-iK{Qrw;ZR+s4K!|#%kGqS__~e>UOpy z%LK^I&PF39j_G(pF&%G>iEtx~xC!uY-Ml?%>;0w_rQyq707K23`C8iazU$3IwKSh4 zXAL{cgudNwd@1Hvh7gK-HA?{tzj8GXTz1Q9Bh~Mxo5NTH?B`muA;$`xreD@&DPmfq zqm}K`*~45okA?Q%f;kPa42y7jb;S&9l2TQ)mWr`aicXtbgyxVK^x$1}Ovh}H)w%5!r`l~O8*FEZY!g_H*lFfY zsO4_hXs4H2-61F_Nh8l`L>r~iF;L*^f$s*vu#74H$W?)?XBF9w5H+*9%7LoyR%Qjxa@qa}i%9%}stY>S*{vCdYSls(D!=#{e6 zpq9jwLHN@66QUL=Yk`7yx@v<21uS{v{GMmFp}IQ1JDKzv!f}O&ej5>0n4jq7vFF8z zRAuvC%v>)vJ99jQaZKZBlJ!7uYYWG1o*;&ZtU z=91|bYz^ka22##3V;^c%t2Jjvb#Y@4RZ-=yDg3}Wf4K4`;ErKf)`&EJe0H?9E*vk{ zf_hVI$hf{+PbJ_6WJZaneO=ZdHK&V4no+%#wYw^xEt=r@%eJR~nS~xUTPt-Ry{}-z zWsMLwKETM5m4AvD&tcT6Y#c3Bw2MjguK!kT(08Y9rL7lJ^>hvAZia7MfgYnPWiw0J zXGkLC7tZU78^uF864g&@@rhouA^bDvL}vX|l?h3y@zRIBs20(BY**Kl^rtpv9EqRD zh%r6!J^l&Hed6JtNA`*pWMwx$CvPRPjC?8bvy)m!Z(vXB7dWA;-9rxADbWMWAiIcv z>k3qx~ZJGe6Rz|WYw{mu`iLL7Ex2QpshwxcwsYX6463*val>dCs+SU=wSIdqY zYUbE`wf=Wl)g1tRz~i(K_D<3g69K*?P~Gn~g;8t3g#8!%a!uGQmGMXlBlGWhhZjcY zK~BA}qv-s9tvqT^@CR4_{N4OB7~LE4j(eGlBJ zEVHNIXEpqg(Ks-NZ#WiH%{)-q@>Ahu6(`$aF^!WR7!%?CxI&Pibf<%{?Z9F$!FWB8 zjcbTsS4a&^5zCY4$kOvrEU4niDQXb{(AH||{vr)b&(5|AI4v6nV34p#&rw)Z$i-vZ zxmw4o`<@efiX+> z=b1Q#I}+`>+f^=$qxRGMQ;`pLZA9u;&8!V{iA=Rh0a2Afo5>6kYpHgoS&4f!a|Yef z+jY|BmW095lgS}3^oRQCZ?K!`9}l0fjL>MUIx}16848rJR@XB2Fe)DGP*-x1ksLk4HbH2^84}g+m zj#ZoHx4UotI*?41Q%#e&?K zW1=iGY@vd1O?siY@;I8MgFSN!=6WC2yDm4FI<5+yo^nK$wkwxCEZeW62g#;!(K;9k zqb@aRXlW*s_m`L6#U;~b*g9x@VnG{jBgTIfaZRGXQDz=aS=5fsk@>MxYQ;g%OoKu! z@P%glokXr@rF=E6;aUHl%U{tFjjoM?#%n9Yx z+9?IB<(jXNUs5Z<;r1()4PHqH=>EPB78MU0DnrvFdKNMq~^_2G=K#2Kl@oiGaLjfUWGx+4lr(rxi5S;duNl|(FO z$$q+>Jvmq9W^amxcwNzDgeqcAS;`MiYrY~~$gFDoGC0X96-B==Ni_u*Uii{bAmL(rJP-Y(MxJCWj09Vs|9*<$z8` z4G@d+0Cl}o7G3KkmYl>)L1~ZYS|34=leKa?g&8^Wa&e>IapylmJ#E$@H>vJbDexs5j*MC=o&k)%j5ZW7S=!PCw!8@YP;Qrz+6~ZSo?P>{&iSJ+`H@B*XqsX zvr_8F#G1Qj)jJ&)?!&8ZVX?-$Kiu=tSav0C`sm}lUmo52zuGVVG%(FAX^IN*@gY<_ zMg0?KlI5VCvlt?s{j@`#uh4P7IRIy%=KxibD`2PjfqYTujR+y`?j|FZ9DU$jY>uxz z$)l-4F=mwK@=tDX=-6fP5f;Aj{2cBbiyi*$--$I=0X2}PHN5A%;2k;+4$IcPfxdns z(&*LxWQdG@`?Y5q2?BKpUy0vX7Om%xD7Rl$=U;^Kljj4^o`%dvr1U1OcnwF!$6bUa zYYPq2d3{X^$Fpy9g>_$wh;~obya`E2TF?k_X>mO7&O#yzA1?PLU!NZ~wvMC2!(}QA z+eDfCX0`{!1O;Om?jruirienj&kMe(RN32@#C32dWnvualGzitlGSHONDuj1v%(mP z4&6EgQsg))dFStA19!}U^Xz_KzaB34=EOA?90;*?zY#RC-m&1<~ zsV_xFW*@$lnUpE~?m1Z%9_3r=OIauwoT>oxa7ita>w=4Gl|bnahLR|5^{e3;hIaBR z_}@L=VAybSw93$Sx+wR6K^``tpu4!xAMN z(4hrY3+dk$Xd@olJd_h7g5TrS0r!m5LCNoOhaQU6XnLR+xQg_h&M>w8!HC?#vg+kM8AaiEaB1qO#Bzjs6h4&-MApP=Y4kD=>LT~ z!FfPDH(ZU1GU0Z==HD^i{{W%pY*6nDNHDKBzq#l9_l9n0qBICZ_aS1!pV9vr{~acH z{24d6drsImBaSN{iV#ZUk9=mPgE?*D#t$RX-KAn8yjv)R9K&*5(H z7*-oS`&AtO0c%-;G;l#AEm!2eD5&Nm$&1ePwf~((fh+^su+uMSD2@URJ-|py@jC-h zws^hGS3RvjKm6ZM500BGpp=w!;%SXUr(pz51wV!OB$!g8yY1@h?v4Na&%A7qtI#qP zxL5QsS@YHD9kgK4>8*ffpND82%9j7xzkfhl8CGbyd(G|`yCBQ9KCx9M3YPx`oBxur zm;MscdZO*3_@C`j=(Yqec^QdXtN-7d*$w^wNjzQ^uKiOP_w#7V0N71#U1!vL9C9PN zw;Aus{T=E44-WmCF({L(yeM#eZ8(*gkVF7($a7s;Qbna^ylqr$bx)>6?B6db=vpCv z`RS}cXJ)Y3bst4QP_Wi#oUg*5sb|l?vcS=`SfBjgFu8~i^~O#zK4#}L)WhX&AE@zY zYIF5xvkw;>X6NhNp20-A*lLT=qY~(Ao$3?2=ZdL*4Xl!bUh zaKPV!>hdB8r_MbDIV;>hf7WL91e;W|T-rC7!v1fSDy9koALp%{xk{BLM`D4%S12${ z+MQ4|Bys_IdA#l&O=GP!+jq2ItHVYm6>5mEIn)GLwRDm`u}b_PTOB+c4?{P_bR$T? zI{Z2*ttqvkOj)z_VPn=ALn@gsir(nevWxp<7Pl(x->ZX2UM=yirvW3Nbbr7C5|W_x z0HYxPCltH`PKVzzUW==%s~67?uKHm3hmgmiM;I2BI?A{e8}#uNbC9J@`3F%^KVeKS z5T{oAW%_ObHUG7y#hOSEHE*0ZyFzp;>p!nrE%!@a9;=ierv}_-jCHE^rn7~JGG((w zylqe>wqh2i=!=h2&kTn!1qDS?4svKnEAmjgeH{IP-T~X7l5U7JU0fBWutZNFMz68` z0*cnazqQji5K2gB?fyQnZ*wyN@$M{a^PqNHyUQfW(i#!|W-x|eYn;ax;mlV<1FQ6z z4LK+%t&*Gb8^Vnjy1{M_AJSU@$A&)XHyZaKrxhJ0!Y9nwA=%vB>5EZR)tM8c3X&Va zTSB400eHxKD6|c5 zemZEh?19Im5!xx>dyCXFUu80y+V%08`WGIJJq;J}V<83z!iAF{q_$FiWAjz^-ge-V zszdIa(bkpJ!)NaSmFL2e-6|8Yc&PTE#eDSC;Z+FzXUaQ@y z)ifsjP;A#%+;o@gPIz2qP2S4Q3o^PSxShlyvpn9E-?@?!&i++9`ci}mxY;8-m?@)m z)b9$qFjB-i#E-&Z{_1^&v(+sO>tGuI))8GDqg?&+QNmA$y5EL^x!S#v2?c+sP98dC91b2CHBdV7B&$yAiaU7(V6zvB$2}4Y`%L{@Y zOx(d)%)>DM+A**z$%ZH~wSV!D4-Q`WnwFLZ=ULF~zAH(a7I1qAhsC0=Z65t#yG147 zbG=ZQTdPtwUK}Yu_`^z+Ds~b-#KMZpBc4Vy8__8)^Xrc$Of|7R$2jG#il8i4DPtW3 ztMCTv>lU_u+oCT6&?70#-wMRT$gE4M*q(+!Ta-!c_+NKs%nAigr~ATC-ARM{#d3muUT^Q+>@l&L8*_j&|*ZqG+`pX>r#tI_ht=qnuE>fw`R#lozFO`ao z`3ocY$6CnTK_7G%F??c(>Wfv%s}->Fe2h|6z7wAyHvdoj0rw~V2v*$_`cI5h%nL5v z&aA}O_WZ|p;|GC5n`pL?73u#xFDKa1`!FCafsE_hPf#8?^_`GF~hykZ0lfX%b4i))CqX1jo9sE zOFz7R3V!0D+55iO`;%oOfuHnrwZrDfR=r*5vUq>oh;Cqc_x*o-0#TI6LdUX{;1SjD zs~tu+u|#LY3UKs>#=W&I$ZZO|LkB$=n-Er1_%Ut{pEHOtULZJHE# zjaPrQbY(T_?B13}ZiY09WS)Q@RQ5 zQvaXV>Qy}1&h1hDWetd;!C@>cA=)h<3lD9*3Xcl_&w%Uk9_?!kG41w6n)=it4?>s~ zN`G<(3U|8C`1cP)98wx#(&vuRL|m{o!oL7(v1KB>cU9^4@(|C3N+wJBPPM`?nL9N9 zDB(gRcmlnI232rc1r%hsmB@>o-82HeJ1tJy#l{Wm9$#hiX^DZrt1J}?RXPh>dMHA7 zJEy8@<)?Jp;x^CY#HehaX;ulequN8MfaAdd{mhdXcr>}xy4eIrlDGCTK7(>FkI9{;gqr>vn8?BgV2% zYU_nZVB?Yk1>I8&-G?7q%gnV%1Crz+Fbp8sW>^N5OxL;XG zpRm`*pg&Lqd4SDNwUg{hhp+OLbUd`j$&!=l+iT$G;|2C|^b*(tr3!EyNi3B=+Eb%R zHBzgE0@@wbs__}uHP~iaMn*=S^^dEHoG~W18|qH1S_0`b6e~*8gRRz~OpS_X@?wT@ z*sfRyDNQv1Rk;mLTbc9O9OJ!)v-t51W%|Ro#xbfqwebnf<~+(8+~UgOYt&Z_5CzXD z^s%f6{ah@GU~2zzPQpM?VjE?YnwMAdI?To!#aLT>xtT)RXW z5mhL~wPN@WI_3ovVyl~^T45LYzpFH*Y`B`!*!XwaEIO4t%SYL31Hj69 z?`@|K@GX>S^s!c%MK#nBuW>r;g+9AwjC)MxFtpYzf&0t|7sfr@MeD}vF>E-8CG>H*X2>|KyGM_K@PHfD@8N{{AKryW4UU_E0_>cDK8$MUm7(qf z%LuXkyVdwbH9HD^!~srm{oP+&=(ZASY{*y$f&y8(4~$Upnr%955?<1$36ro0KhNF_ zNCj`~>{+DOZ6-hTOfExrgmr%7>m>PH*4Y(4INq0ao+dAUT?LiR)?l%Ptn%Ub!`L_W zvJGk;q9|Sr({w9NBSV8+z_jbf)>&^Z>B3VOv-Pd->4C|629Iqr095RpE%e^!xk!gIu1G4me#ivXg*&HnrfGX*`GZ7Q`4r?k{{uGUKwVxHqomOxH37 zvBzq27LF*k{;nBWZ+rG>W^`S!$yeg#g1dP8V;S;(fpq)PwJoK&jDxxIzb1gapLq9y zFM+quytl`Tc5oL-SDFDlt(M~qncQ~*&BnprYAsg<)JPo3C-qIw> zMobgWK_K}Y(Hi~GF=)yND*80(a?YfNGX5-x=6ni)qNZo5z63G|5NyWrf2{jd6l0R5hKJh;eMTT30QRAYx5$SW<;{$*m@yjt|I zlShnGTK(sGG~q9;iapH`tRg)X8kLdfztF?FQz+Sd8sRTMBwtj8-6SpIK1%5G#fgim z_O4~7v~TV~H-D7YV+P(VVY33wM9nZ;&G1Vr_~W~uX1|`Ns04CJBV7S!F;nawduO{aFEfr~6T=iPF(``4d&1J3q#*`JJnZ(<^W(yJtW?=)Fl*r}WQ) zMv>WiuZFuxkycF5!~{!tR8%7mPupxYGw>aotq*4K8#}E0UAA=c+2^rz6=D`?+`==g zGw3*1PNlIogruJ^eXD_RG=92>lBdMo=ih7Z&+SHhoJp>Pv%XZCKv;Y})L?(u>T-gRO63tu!Vu$xvs*>FLtm+te8O0=PDA*-m!Zn%2NokaO$S$Ydo}uEPP3Wr3Jins}4bP=1OQ z-OpIk$cNA-)LiS2wqOqzJ@6Xwj-9y&GZ(8=SA&+Rvx-zCF0QQ0c4(E51WrlekE=<&SOA7WcL_Z3tL3x957p!#q~A5D76 zJb|A9GcLs{dWImwcj|Is3dTdatM0uwLc_ngPTfgcl%gXSZ}%p(fD!d5wiF-)0esWwe7ps;$SdwW0CY{L7!_Gb1PNf z^pu<5g%T%0JY0^tV$mRVR{I9~1_=P8WDC}&8rAQv??$%OmAN5_?9&ptzRb!1&2=Es zF*z_Lio~YgN6D6%Zj61J!%1f95Zz1n;>8vR6jIQjGXY~p;-&7Ya)dv_58H^tGq~5q zyKJNS`1sb(mpQkLt3K(u*6y8Tp7G(Pqwr?MmKlN>nWKK+DTuzn z@%!Gm#$eZo{0DAuFfHgXSCJJxp1{GgQok>;7pw)KcML`*X!p9syTqld`0)$#^t3>v z3Up4dj>7fqr_z$jDjgnjn%N$6vZRON+_1g-9Lp{xq$J0L93?|L7v zOA!TCU&$kI+!6E(+5e(fvBbdKvvxE6!oc0jn%Nd(35g3|uiEkjWsvy*A}|fj{>Adc zUKCn>f0`(fQ%=kUV#%Ghj#QU)hfJ4E`Vt$@1oL6&#gAYyK&jq+U0E}IQW;+Do3BX8p!u-6cUjbeW26k;y4$J8pxTL z6oSR98wY&X+~EvHWx1<*Z{H=c4hr3v(>Oih+aAWEMzGB=s>~?lEh&6YuiK4s!E`l3y`DYa_UaeEFckESNiT zPLv~N;5CeBIGA{9hEBTsqnQ)bxKgCV4q{+WS8)>i*iJ(}Qy?WP*=Q6ddw)#qgnp)2 zI}`T061vWFrWw3+-QS;}xRLfwL<1Ts?sO28GU5;W7}9zzMVN;@GKK9ZCsTg=;b@*NVY?)Y=;@rMlx6A3a%#%Dmi=)kK!%7$Mzu z_N)n^9OU=wdl0(5+SJH`8?}iA(T`egCUlU7CSy~*n)ZSkz%Jrp2hM#_i(b3;R2c1L zIx-^FVpltZ&{by*qo9hbX$;<7@Xx!V2kyQ?+z7-+$yGLqf@w;`eXK|76IK@XEiJ%w zFv}{J7r<2(Gagv^0&BZ03t+qh&>ivMe6_IeJjk#sH64u+Tpu~(W|;Yq4}(I&$0TOZ z(Ax!crD8^O|FvVX^Da25>9DjKoW4p%J(b!hw@Q>#16TPKdb2$Y;a2N$ge&!<^4Uu6 zr*J^OivqU195v>!*!Du}aYB_yzb=vVXwL*zoLx3Yik(8n1V5m>dNLPO`APcUuJCG9>PqgYHkC*l zL`X*V1BO&(1r9)@8i(*ajTyQ6}ahpJjafuM&xMk|zh6O%b}6LxCg_&8Ui%KVid zV@Ok2m~tcQHy$ zCv05J%q5Ctmg!IhKG0->i``lrGlXdYk7GLH9~~@&i>-j!8){3&W81cJ$prN}lc=5F z(I|I&)NHHCLXH4^Q56xY^O^OjgXdsNSIR9qqJnF7F}k;ggBMk%$-!<}?(>?ZizQ>} z6WrSJP&}}%zdX1mXTuD~<2kMmDV(9e7fOs?u8YZ{~6Z{M^+}N3=5>; zt^w`179)1sn@qq?5N35fnA{PmnA{|W_6Wpp=@8uXQ=5d(gT!NRW3=_^C%@j%jed1L zDW5`&slr>;fNRM}<_;f0qPRr|xj3VjtkE49BEIX4oy;ym5|6-5>~5i^WDBVGbGWt@ zw=0atraXKX^oVyMFAN<1zNTOs(7r7X2x;XJxO`73mLW9b4_C?pfSC*qiIp~Vk_d)j zB#;TOX2FWyHIOzz#=udUWHtK*YgVi3v$@J|yfS&#y>5U$p=Lp~7odjjIR7b2);;4a z!f{MghrN=m?=Rh_|r3%cd!wy+BDY zv3j?$SF)k56E8}+tQANhZt0wWkDt6|vjmWupD?%}{4&d4Z<|#jo$Jyw8cn53*aOhd zPz#_?hkaSqAOxY$V&Lm;smW{GM#w(28in%X=^a?Z%UaN&FOr1V$v9PL{=~BgOeWgd zwK0Va$FEn_|7xr}wqmys1q?m}L_5*x?ra>K4alxca^o)45nkacz>nH@v}SmoVC50!f!<|%*G=C@#>A~ zVg?&VM#YA;*&?9&;|t-+l`n;@Q`7y77_!Y{!wd(_?bxz3Kpi0dyd`GxtsM5bJX!p< zM&+ZgFU?4ik_n?J`ZWcCXqfVrS}}*f=wngcs97-1b+B*ZM{ndfK6kL`dH%@OnVUsB zxw#@3N$wn47RMvE(-t=H4d}g;Qp{D?$yAMeH@ffqRgJh8Uy?!dYhaHSB7zU*RJS1B zeysYVYDP7JKviL7QuU6R;wgaI-)p;p+&P zwDw04QQCFe2MKJ~3tagj0oM z-YGHy3b@&$1Y=i~=Tlq@H<*^GBSmahj%`=rcm_P=IKQ~%_IkKRaNmWJ`^kEgNTD33 zqO~6aJ_Tj~t_jc-(ypa|s|g_}`!-oVizPBbEAnH8S=Os13r~}Ie)LW0Bv9diqvJ}7 ze#!i0gngC~*f=P6E6qq^sO+O?A)K;ZVuix6-;n!IZTW))QSeGUPGz2e@KK?oMP~xL z1WxPpol2mx3a`4D&Z2N@ngoAGPP~b`j{t}X=y3j&k}Gax|AjW=(-fpw$5d78U3s57 zU=Y_iJSMXDm+*Vjk-1=+m8|@$8VFA2ZTP!jSFeMOY**i(n`OJpSoFvz)IeD-9?`Re z$Ue`V9&y_Ci^Yw89iw5}uBIVl<180C3RLMA`C@DX}HS?||+T?OW#lye;tn9wuSF8y~ag@Wd%e#)J>mMNB`sp)WCYZ8Pj@)Cl zitQhVFc9I6SX+G!d$FjE(P-h@#)Jvtl*T~W`vHRSHpI%6qP$+>wb$pv)}sebt_G{1 z@_&^TFC84Jl`jZz=aGudEIJztG8anlN&kbV<0t;1a_}Cn9Gatn86Zerx6LUbt%3lk z_%d?}ywRt)UV{2VniF&B_vNV}KJN)XpR!=GiGK;6ayd(G;m8RSr zgH!W@T>T12Jht!Dwm7$6+U&#(ShvueInbWwnEMjnjU(P`9rth6?^vlr;suT zzTLBCvFA@^A$a^>^;HpuKYSUvjTkO42jc&E&G%u8{v&@Infl%siY5NPixOaJjT)GI z;uX}~tbL#Q-TJ4jPzI2RVi^mr!}VPp1!epI?`8wpvc_oRKh>zeY8}PoP?K9*@sw$> z0*E+Vc3g<=_2KqfWCtTK?Ov}$N=iz{I!(lyRsX}%`m5A{qWneRa57I=+LD&`KAEX} z)DQ>M%C=aA<~>K**LQxr$g%usgzG;%hmV{=0^Tk~wzEH)o%!{fa+rYbMO$AL$lT$t zkLTcX_#1kD<^-CJ05e)+4rl5S|FqKG5WfBCUZXj49Zf}6FY}8iGz?*P+N{>D4~VMr zIOrrf^OSh;X^dZ{#Qk735#8MUaRsg=2Agr-uQI`Q5#o0?7F8bt%{89fFP6e zN7(V=Dmo!y6mi>~+1|0_!D406rt6O<;Nev|yDxc_L03j`EP7t-P_oltfy=<9>|^V0 zbq0aooe_PLF6ZETgJ^U5!3k|6FQ9g|U+`h4I@-)%K zm66JP5;iGJ7+b-@a=%%|yYMz?Qh#jG0o45F)42JmnXGCZPINM$Z|iAsgW2tP>is~J zPXv#*a<0I@Vf{Y4@0!}XsON{vLgnf^m3m!^o`kBQxS|?|A(FB54w+})&1_QDcI^a- z+xU8uhY@$}<{i(e`@oOQsA~WnY%t+NWE;olOlYp^4YPdCo^uhEw|X+W4UPbDm&^2C zgwyFaMAq9%5NSXr^YLd-&%WZbfWcqtR>+bmu3~cqmQ~rliE-?GfK?ntLNUAN!24Uo zWR~41Uc^+B)lgw~*V3=EKQtun?)dY~;Yq>oiISTeg;psi2;9|h&Z@Q4tzIttTeGfG zwy!m`?(z!KzqLTMuc`k^TFXm<%Hwz?O6MvSuT`g3jF)|``T+}f9iG*&y}NSR^J4T^ z(yPfqzC0JQJ}DUyOwAU{w}={CXs~Ewu{-p=nn4#=De%)detjZ+zP}=p5cV$gX7v}2 z#pgo$#Ms3Wuur4#IBl&{iJ%QQBESHbveojwG>WZajLKU33)qtt7UmfP_l>f!Z#8)Q z;r-byp?_#peF+1%cOJJOilO=7vPNPE4GaWM8r(B-JFZ!xfs`^hwyD3AYgKPfZw~>k zTZLVAM;a8AaV&e6h1_F~rklOvQ-v}Q({q#^me65;>re_ zkAo;W;TZ4J2125O!u{^ic1KdhASa(cy4`=23cOHkuTcy4$MDEmYC_*?mD`<1C&c`Q z2~Tmp^WAl!)xSwL7>xL%DH6^~>aTEo{pL8!MN9YMf_S`&dBiTS{kQ}NxRd98HZI5UiTQAQ5Sl)T(dW)t)Lmp3uZE} zt2^kMv`F+|+>3U&06|mt=vxGzwLslRtDZz7Et7}HMyV$=1;D7J8JI_^-R?#mVARMm z8`5enN8xqd?9_i%kmh=0n{wV&@GhRV3AXCJf%thN;cyB^8QOu8PPWKou>QR^15lXw z0+N6W@T~@R6Wq&d)RUR}9Zah-?%Q*?w;2?e68n5z))&vC?8r{qOG&W-ZXq3r#2T#d zmLJ%k6MEN5!-#7Ng`(7rQJ`cnO*xmu`1?CC4hTiac|m}N48Ay6B@K$4?x5ANmhOb2 zK-$pau#u+C_wL~9t%Kg?QcM<}kSUW~3In|Wm>TF}auUj%&TCbBU?_4Wb+G>AzsXFU zAaFW*7QB&cX!%%283$T_LLrdw*i*>B^nNKJ-Wgx7wIl7!xU2`XnY{4zE)!y7hg@I= zqEV>J!|7}}rq?NeJVn+W zdmS-cF9|->;2t>iEV|-eOb@g64-NJgHDrwh2nEy?1Ufa@GvFXJIj)Pok>_bs0d2i$AiULN>JtjDS< zok|SQ^$(*r10D?Y5%$j+YSS}i1o*zZ~(Ias<2XC6znFt*dLlW{JKI$>^7}Z z)TP>3Vl%pP7X$=i;h+BtprwqjCK5>LX#&O0BEvxF(9 zE>NTbf{%C4S^1n<&(>~etD^V5ulrfF(urOcQbA($*(#zhwp5W1A~|8vMPOyHXQFe` z%IZoTHT)-4vG}XZ{+i3eb9{n4{Cb?P&>RNz%)pe}DY2u23v~*CC<$f*AKn?%j~z5+ zEtj+N>c}q%H#0;{+}FETs(lxlCz;wEjvSWb{@8mIdBnn9^Af`;dfZy*r8gfLiWo!81Avwq2>K7h z-0n#At*1LaKnrcD&95*R-WA^+d=Ss4Yar>=n&aVuyelP~(Q`lezN(?yIUIv-4Sf~k z%Dz7+*bg5G_|HK-Kg_d;tD0?T>EL#>jFmcZr;1w6dG2(qN6N6_~TeSEg^>isLP8&XnNo{6eL>yjo8$VVz}lnoM= zEFMFwBiTqcn(zgEWpyE_u6iG zGt%sOm&;O+%j!gOVVlN%{i8ZPUE6;3Cw~O4PAW%ZhK7NbExdc(xBkZ|a{mz7nbOID zsxDL!Rx926L={w|XI9mPh+`v{5I$z;!xJp;(b=>Wc{0I+#`TJz+Y{ZMYAOe7cl3+P z>(VeB3nt)rz^ZeQn*tdxn8!9}(}mN4zVbGiU>@c6BQ3;-FV}6Q@ml=LmW(VG(5Hyt zwarJdeUCd;P@=k-T`DYC@1?a7V<0k%;ydkih?p+jaWHq*NCm6SVo}xpxOmr^!Nn;* zvt(XZ9{1=0jT*I{ne*7-Js~Ejfj6}-O1}le+@(6G_Wp8Dk|WT^Y4;uWRPM>h)YrCDkQcLGBuKq>#9GR{02$~BJTp*WW8(PbUKJTxz6z2h+i* ztcS)aT^+;LOyMJP>$pwxYTtF24L~N*ihhqj4`~lBBE;f4ie4BdHX>V9kUY%^ z@q#GTD~SB*a4-Yny~5QB;51v9K3+XUWX6~1BGCBmJH|c7C}CZvkY5S|l>)F>8Dt1g z;ch#0R`VG@FMa&cT8_Lx7kM(yqE?uttcjeYfXo92{E9e+GX(5WQ>!liHzD5{eC)Nl zP}j1gzx_G1!N(?3K`KJ9m!Axr%o&YT((FjGsq$(${IZW7Se{N_M#-@5WisECn>|hj zjq=csV;w~70_DsR;pDjKH7E&QjWaH&@aEXgdNCv28!=0{%iNxx9nK`00pU}>xJ`z; z>-DfIxb~=*H~PZ4hn0CqIKYtVEW>#=x3=&XVZi}H0Zd;dlIoK3_G*KFFU$>O5su7y zvaazsCNUx|#h_dUh?e8SsJUoPW8C)>m0)(Kre@UX;~XnL6G534dhVRZ&C<76lkq^T zy^}vuzwMk;9BOzxNIJFoF>dqUd4*fa32fp*|q&&X~~36L#lmekYHQFpc;K{IOM zW1R=V?z8$_`#xumy_%ewb8J2JGX88X%N9EL{Jdo|34gXyN`r!u zo=T9Qq?`0Lzwsn@sma#X5Yc-)Zq5~txCy#nO$|Tz(3CDL$hp9+xT-*D?+(%RZ;HaJ^O2A8!5{gmXnl(AZIO%t*n!b-O3W`aR1T=)1=1;~a zP0PBr4r~V$7FSt4=_rGzf?$5q#j%tnWBqn{3mv&y=-q|Z-#+Y@UW_!{(X7N@x8y`3 zn#Ud(1L{mmfz2}{&mk<{vjwbuS>lG1V%J~RWe(7^;uAPN+d9MdP}DsqJlr|aEYA^; zQ1CazUho{8eo_s78_Yr_(~x%FOaa zfPuXilV5sA9|p9hT96`;Y0^9HPkvGxx2`7Xr;)R%si5fC@Bi|*Ign71iOvG<(ChGo z4a6)U^|bm*A~Iw;lItIxn#H)#X$Nl`0AvAjC}?z~Atd@{^Clyam506+O;`-U%P$@# z6y+m;|IZ%c|Fb2XSM;HW;9I=l8z@(xnDu}__ha>cNq`2Fm75P6?)i_TT?cLNtoNP* r7GC|AC=g&HEmggU`o#-nAo*3xHPjY%DXp%53-H*Z9Z_{ipQL{P68+d7 literal 0 HcmV?d00001 diff --git a/documentation/changelog/0.118.0/collapsed-sidbar-content.png b/documentation/changelog/0.118.0/collapsed-sidbar-content.png new file mode 100644 index 0000000000000000000000000000000000000000..dae177560415630dd3e3dec034fceea7e6acbf3b GIT binary patch literal 33404 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�s>Sdpl(Zjg;g~2p%{k(Xt%naClZhYg~ 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 LC=|(=zW+Y}V!Xx- literal 0 HcmV?d00001 diff --git a/documentation/changelog/0.118.0/delegate-to-coding-agent-action.png b/documentation/changelog/0.118.0/delegate-to-coding-agent-action.png new file mode 100644 index 0000000000000000000000000000000000000000..67a3cd6c048adc1167e6158a5d791f13de5d331b GIT binary patch literal 42600 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 M6&+=y;*%Hu2`J^&Z~y=R literal 0 HcmV?d00001 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 diff --git a/documentation/changelog/0.124.0/explicit-chat-context.png b/documentation/changelog/0.124.0/explicit-chat-context.png new file mode 100644 index 0000000000000000000000000000000000000000..5a7c4da29972aecab1d873a9c747b8f6b682cdc2 GIT binary patch literal 51669 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 O*PB<$vSlw!1O5-aZy+=P literal 0 HcmV?d00001 diff --git a/documentation/changelog/0.124.0/pull-request-implicit-context.png b/documentation/changelog/0.124.0/pull-request-implicit-context.png new file mode 100644 index 0000000000000000000000000000000000000000..476d554ae7b163979eecd3536a3ef59c156e4f7f GIT binary patch literal 19858 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 z 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 JbsprO{sJ(^m#6>$ literal 0 HcmV?d00001 diff --git a/documentation/changelog/0.124.0/single-button-copilot-pr.png b/documentation/changelog/0.124.0/single-button-copilot-pr.png new file mode 100644 index 0000000000000000000000000000000000000000..ee0cd617e104c92ad3d93378d89ab3d62e5379f8 GIT binary patch literal 33345 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 literal 0 HcmV?d00001 diff --git a/eslint.config.mjs b/eslint.config.mjs new file mode 100644 index 0000000000..2f509573d0 --- /dev/null +++ b/eslint.config.mjs @@ -0,0 +1,284 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +// @ts-check +import js from '@eslint/js'; +import tsparser from '@typescript-eslint/parser'; +import * as importPlugin from 'eslint-plugin-import'; +import { defineConfig } from 'eslint/config'; +import rulesdir from './build/eslint-rules/index.js'; +import tseslint from 'typescript-eslint'; +import globals from 'globals'; + +export default defineConfig([ + // Global ignore patterns + { + ignores: [ + 'build', + 'dist/**/*', + 'out/**/*', + 'src/@types/**/*.d.ts', + 'src/api/api*.d.ts', + 'src/test/**', + '**/*.{js,mjs,cjs}', + '.vscode-test/**/*' + ] + }, + + // Base configuration for all TypeScript files + { + files: ['**/*.{ts,tsx,mts,cts}'], + languageOptions: { + parser: tsparser, + parserOptions: { + ecmaVersion: 2019, + sourceType: 'module', + project: 'tsconfig.base.json' + }, + }, + plugins: { + 'import': /** @type {any} */(importPlugin), + 'rulesdir': /** @type {any} */(rulesdir), + '@typescript-eslint': tseslint.plugin, + }, + settings: { + // Let plugin-import resolve TS paths (including d.ts, type packages, etc.) + 'import/resolver': { + typescript: { + project: [ + 'tsconfig.base.json', + 'tsconfig.json', + 'tsconfig.webviews.json' + ], + alwaysTryTypes: true + }, + node: { + extensions: ['.js', '.mjs', '.cjs', '.ts', '.tsx', '.d.ts'] + } + }, + // For rules like import/extensions (list everything you consider "module" extensions) + 'import/extensions': ['.js', '.mjs', '.cjs', '.ts', '.tsx'] + }, + rules: { + // ESLint recommended rules + ...js.configs.recommended.rules, + + // Custom rules + 'new-parens': 'error', + 'no-async-promise-executor': 'off', + 'no-console': 'off', + 'no-constant-condition': ['warn', { 'checkLoops': false }], + 'no-caller': 'error', + 'no-case-declarations': 'off', // TODO@alexr00 revisit + 'no-debugger': 'warn', + 'no-dupe-class-members': 'off', + 'no-duplicate-imports': 'error', + 'no-else-return': 'off', // TODO@alexr00 revisit + 'no-empty': 'off', // TODO@alexr00 revisit + 'no-eval': 'error', + 'no-ex-assign': 'warn', + 'no-extend-native': 'error', + 'no-extra-bind': 'error', + 'no-extra-boolean-cast': 'off', // TODO@alexr00 revisit + 'no-floating-decimal': 'error', + 'no-implicit-coercion': 'off', + 'no-implied-eval': 'error', + 'no-inner-declarations': 'off', + 'no-lone-blocks': 'error', + 'no-lonely-if': 'off', + 'no-loop-func': 'error', + 'no-multi-spaces': 'off', + 'no-prototype-builtins': 'off', + 'no-return-assign': 'error', + 'no-return-await': 'off', // TODO@alexr00 revisit + 'no-self-compare': 'error', + 'no-sequences': 'error', + 'no-template-curly-in-string': 'warn', + 'no-throw-literal': 'error', + 'no-undef': 'off', + 'no-unneeded-ternary': 'error', + 'no-use-before-define': 'off', + 'no-useless-call': 'error', + 'no-useless-catch': 'error', + 'no-useless-computed-key': 'error', + 'no-useless-concat': 'error', + 'no-useless-escape': 'off', + 'no-useless-rename': 'error', + 'no-useless-return': 'off', + 'no-var': 'error', + 'no-with': 'error', + 'no-redeclare': 'off', + 'no-restricted-syntax': [ + 'error', + { + 'selector': 'BinaryExpression[operator=\'in\']', + 'message': 'Avoid using the \'in\' operator for type checks.' + } + ], + 'no-unused-vars': "off", // Disable the base rule so we can use the TS version + 'object-shorthand': 'off', + 'one-var': 'off', // TODO@alexr00 revisit + 'prefer-arrow-callback': 'off', // TODO@alexr00 revisit + 'prefer-const': 'off', + 'prefer-numeric-literals': 'error', + 'prefer-object-spread': 'error', + 'prefer-rest-params': 'error', + 'prefer-spread': 'error', + 'prefer-template': 'off', // TODO@alexr00 revisit + 'quotes': ['error', 'single', { 'avoidEscape': true, 'allowTemplateLiterals': true }], + 'require-atomic-updates': 'off', + 'semi': ['error', 'always'], + 'semi-style': ['error', 'last'], + 'yoda': 'error', + 'sort-imports': [ + 'error', + { + 'ignoreCase': true, + 'ignoreDeclarationSort': true, + 'ignoreMemberSort': false, + 'memberSyntaxSortOrder': ['none', 'all', 'multiple', 'single'] + } + ], + + // Import plugin rules + 'import/export': 'off', + 'import/extensions': ['error', 'ignorePackages', { + js: 'never', + mjs: 'never', + cjs: 'never', + ts: 'never', + tsx: 'never' + }], + 'import/named': 'off', + 'import/namespace': 'off', + 'import/newline-after-import': 'warn', + 'import/no-cycle': 'off', + 'import/no-dynamic-require': 'error', + 'import/no-default-export': 'off', // TODO@alexr00 revisit + 'import/no-duplicates': 'error', + 'import/no-self-import': 'error', + 'import/no-unresolved': ['warn', { 'ignore': ['vscode', 'ghpr', 'git', 'extensionApi', '@octokit/rest', '@octokit/types'] }], + 'import/order': [ + 'warn', + { + 'groups': ['builtin', 'external', 'internal', ['parent', 'sibling', 'index']], + 'newlines-between': 'ignore', + 'alphabetize': { + 'order': 'asc', + 'caseInsensitive': true + } + } + ], + + // TypeScript ESLint rules + '@typescript-eslint/await-thenable': 'error', + '@typescript-eslint/ban-types': 'off', // TODO@alexr00 revisit + + '@typescript-eslint/consistent-type-assertions': [ + 'warn', + { + 'assertionStyle': 'as', + 'objectLiteralTypeAssertions': 'allow-as-parameter' + } + ], + '@typescript-eslint/explicit-function-return-type': 'off', + '@typescript-eslint/explicit-member-accessibility': 'off', + '@typescript-eslint/explicit-module-boundary-types': 'off', // TODO@alexr00 revisit + + '@typescript-eslint/no-empty-function': 'off', + '@typescript-eslint/no-empty-interface': 'error', + '@typescript-eslint/no-explicit-any': 'off', + '@typescript-eslint/no-floating-promises': 'off', // TODO@alexr00 revisit + '@typescript-eslint/no-implied-eval': 'error', + '@typescript-eslint/no-inferrable-types': 'off', // TODO@alexr00 revisit + '@typescript-eslint/no-misused-promises': ['error', { 'checksConditionals': false, 'checksVoidReturn': false }], + '@typescript-eslint/no-namespace': 'off', + '@typescript-eslint/no-non-null-assertion': 'off', + "@typescript-eslint/no-redeclare": ["error", { "ignoreDeclarationMerge": true }], + '@typescript-eslint/no-redundant-type-constituents': 'off', + '@typescript-eslint/no-this-alias': 'off', + '@typescript-eslint/no-unnecessary-condition': 'off', + '@typescript-eslint/no-unnecessary-type-assertion': 'off', // TODO@alexr00 revisit + '@typescript-eslint/no-unsafe-argument': 'off', + '@typescript-eslint/no-unsafe-assignment': 'off', // TODO@alexr00 revisit + '@typescript-eslint/no-unsafe-call': 'off', // TODO@alexr00 revisit + '@typescript-eslint/no-unsafe-enum-comparison': 'off', + '@typescript-eslint/no-unsafe-member-access': 'off', // TODO@alexr00 revisit + '@typescript-eslint/no-unsafe-return': 'off', // TODO@alexr00 revisit + '@typescript-eslint/no-unused-expressions': ['warn', { 'allowShortCircuit': true }], + '@typescript-eslint/no-unused-vars': ['error', { 'argsIgnorePattern': '^_', caughtErrors: 'none' }], + '@typescript-eslint/no-use-before-define': 'off', + '@typescript-eslint/prefer-regexp-exec': 'off', // TODO@alexr00 revisit + '@typescript-eslint/prefer-nullish-coalescing': 'off', + '@typescript-eslint/prefer-optional-chain': 'off', + '@typescript-eslint/require-await': 'off', // TODO@alexr00 revisit + '@typescript-eslint/restrict-plus-operands': 'error', + '@typescript-eslint/restrict-template-expressions': 'off', // TODO@alexr00 revisit + '@typescript-eslint/strict-boolean-expressions': 'off', + '@typescript-eslint/unbound-method': 'off', + + // Custom rules + 'rulesdir/no-any-except-union-method-signature': 'error', + 'rulesdir/no-pr-in-user-strings': 'error', + 'rulesdir/no-cast-to-any': 'error', + } + }, + + // Node.js environment specific config (exclude browser-specific files) + { + files: ['src/**/*.ts', '!src/env/browser/**/*'], + languageOptions: { + parser: tsparser, + parserOptions: { + ecmaVersion: 2019, + sourceType: 'module', + project: 'tsconfig.json' + }, + globals: { + ...globals.node, + ...globals.mocha, + 'RequestInit': true, + 'NodeJS': true, + 'Thenable': true, + }, + }, + }, + + // Browser environment specific config + { + files: ['src/env/browser/**/*.ts'], + languageOptions: { + parser: tsparser, + parserOptions: { + ecmaVersion: 2019, + sourceType: 'module', + project: 'tsconfig.json' + }, + globals: { + ...globals.browser, + 'Thenable': true, + }, + } + }, + + // Webviews + { + files: ['webviews/**/*.{ts,tsx}'], + languageOptions: { + parser: tsparser, + parserOptions: { + ecmaVersion: 2019, + sourceType: 'module', + project: 'tsconfig.webviews.json' + }, + globals: { + ...globals.browser, + 'JSX': true, + }, + }, + rules: { + 'rulesdir/public-methods-well-defined-types': 'error' + }, + }, +]); \ No newline at end of file diff --git a/package.json b/package.json index 6e6358aad5..b5ea5c4246 100644 --- a/package.json +++ b/package.json @@ -12,11 +12,16 @@ }, "enabledApiProposals": [ "activeComment", + "chatContextProvider", + "chatParticipantAdditions", + "chatParticipantPrivate", + "chatSessionsProvider@3", "codiconDecoration", "codeActionRanges", "commentingRangeHint", "commentReactor", "commentReveal", + "commentsDraftState", "commentThreadApplicability", "contribAccessibilityHelpContent", "contribCommentEditorActionsMenu", @@ -26,15 +31,19 @@ "contribEditorContentMenu", "contribShareMenu", "diffCommand", + "languageModelToolResultAudience", + "markdownAlertSyntax", "quickDiffProvider", + "remoteCodingAgents", "shareProvider", "tokenInformation", + "treeItemMarkdownLabel", "treeViewMarkdownMessage" ], - "version": "0.108.0", + "version": "0.124.0", "publisher": "GitHub", "engines": { - "vscode": "^1.100.0" + "vscode": "^1.107.0" }, "categories": [ "Other", @@ -49,7 +58,12 @@ "onFileSystem:newIssue", "onFileSystem:pr", "onFileSystem:githubpr", - "onFileSystem:review" + "onFileSystem:githubcommit", + "onFileSystem:review", + "onWebviewPanel:IssueOverview", + "onWebviewPanel:PullRequestOverview", + "onChatContextProvider:githubpr", + "onChatContextProvider:githubissue" ], "browser": "./dist/browser/extension", "l10n": "./dist/browser/extension", @@ -61,13 +75,48 @@ "virtualWorkspaces": true }, "contributes": { + "chatContext": [ + { + "id": "githubpr", + "icon": "$(github)", + "displayName": "GitHub Pull Requests" + }, + { + "id": "githubissue", + "icon": "$(issues)", + "displayName": "GitHub Issues" + } + ], + "chatSessions": [ + { + "type": "copilot-swe-agent", + "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.copilotCodingAgentV0.enabled", + "capabilities": { + "supportsFileAttachments": true + } + } + ], + "remoteCodingAgents": [ + { + "id": "githubCodingAgent", + "command": "githubpr.remoteAgent", + "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.copilotCodingAgentV0.enabled" + } + ], "chatParticipants": [ { "id": "githubpr", "name": "githubpr", "fullName": "GitHub Pull Requests", "description": "Chat participant for GitHub Pull Requests extension", - "when": "config.githubPullRequests.experimental.chat" + "when": "config.githubPullRequests.experimental.chat", + "isSticky": true } ], "configuration": { @@ -143,6 +192,12 @@ "description": "%githubPullRequests.logLevel.description%", "markdownDeprecationMessage": "%githubPullRequests.logLevel.markdownDeprecationMessage%" }, + "githubPullRequests.branchListTimeout": { + "type": "number", + "default": 5000, + "minimum": 1000, + "markdownDescription": "%githubPullRequests.branchListTimeout.description%" + }, "githubPullRequests.remotes": { "type": "array", "default": [ @@ -177,22 +232,34 @@ "type": "string", "description": "%githubPullRequests.queries.query.description%" } + }, + "default": { + "label": "%githubPullRequests.queries.assignedToMe%", + "query": "repo:${owner}/${repository} is:open assignee:${user}" } }, "scope": "resource", "markdownDescription": "%githubPullRequests.queries.markdownDescription%", "default": [ { - "label": "%githubPullRequests.queries.waitingForMyReview%", - "query": "repo:${owner}/${repository} is:open review-requested:${user}" + "label": "%githubPullRequests.queries.copilotOnMyBehalf%", + "query": "repo:${owner}/${repository} is:open author:copilot assignee:${user}" }, { - "label": "%githubPullRequests.queries.assignedToMe%", - "query": "repo:${owner}/${repository} is:open assignee:${user}" + "label": "Local Pull Request Branches", + "query": "default" + }, + { + "label": "%githubPullRequests.queries.waitingForMyReview%", + "query": "repo:${owner}/${repository} is:open review-requested:${user}" }, { "label": "%githubPullRequests.queries.createdByMe%", "query": "repo:${owner}/${repository} is:open author:${user}" + }, + { + "label": "All Open", + "query": "default" } ] }, @@ -239,6 +306,11 @@ "default": "tree", "description": "%githubPullRequests.fileListLayout.description%" }, + "githubPullRequests.hideViewedFiles": { + "type": "boolean", + "default": false, + "description": "%githubPullRequests.hideViewedFiles.description%" + }, "githubPullRequests.defaultDeletionMethod.selectLocalBranch": { "type": "boolean", "default": true, @@ -249,6 +321,11 @@ "default": true, "description": "%githubPullRequests.defaultDeletionMethod.selectRemote.description%" }, + "githubPullRequests.deleteBranchAfterMerge": { + "type": "boolean", + "default": false, + "description": "%githubPullRequests.deleteBranchAfterMerge.description%" + }, "githubPullRequests.terminalLinksHandler": { "type": "string", "enum": [ @@ -368,6 +445,11 @@ }, "description": "%githubPullRequests.ignoredPullRequestBranches.description%" }, + "githubPullRequests.ignoreSubmodules": { + "type": "boolean", + "default": false, + "description": "%githubPullRequests.ignoreSubmodules.description%" + }, "githubPullRequests.neverIgnoreDefaultBranch": { "type": "boolean", "description": "%githubPullRequests.neverIgnoreDefaultBranch.description%" @@ -395,6 +477,19 @@ "%githubPullRequests.postCreate.checkoutDefaultBranchAndCopy%" ] }, + "githubPullRequests.postDone": { + "type": "string", + "enum": [ + "checkoutDefaultBranch", + "checkoutDefaultBranchAndPull" + ], + "description": "%githubPullRequests.postDone.description%", + "default": "checkoutDefaultBranch", + "enumDescriptions": [ + "%githubPullRequests.postDone.checkoutDefaultBranch%", + "%githubPullRequests.postDone.checkoutDefaultBranchAndPull%" + ] + }, "githubPullRequests.defaultCommentType": { "type": "string", "enum": [ @@ -451,7 +546,11 @@ }, "githubPullRequests.createDefaultBaseBranch": { "type": "string", - "enum": ["repositoryDefault", "createdFromBranch", "auto"], + "enum": [ + "repositoryDefault", + "createdFromBranch", + "auto" + ], "markdownEnumDescriptions": [ "%githubPullRequests.createDefaultBaseBranch.repositoryDefault%", "%githubPullRequests.createDefaultBaseBranch.createdFromBranch%", @@ -463,17 +562,49 @@ "githubPullRequests.experimental.chat": { "type": "boolean", "markdownDescription": "%githubPullRequests.experimental.chat.description%", - "default": false + "default": true }, - "githubPullRequests.experimental.notificationsView": { + "githubPullRequests.codingAgent.enabled": { "type": "boolean", - "markdownDescription": "%githubPullRequests.experimental.notificationsView.description%", - "default": false + "default": true, + "markdownDescription": "%githubPullRequests.codingAgent.description%", + "tags": [ + "experimental" + ] + }, + "githubPullRequests.codingAgent.autoCommitAndPush": { + "type": "boolean", + "default": true, + "markdownDescription": "%githubPullRequests.codingAgent.autoCommitAndPush.description%", + "tags": [ + "experimental" + ] + }, + "githubPullRequests.codingAgent.promptForConfirmation": { + "type": "boolean", + "default": true, + "markdownDescription": "%githubPullRequests.codingAgent.promptForConfirmation.description%", + "tags": [ + "experimental" + ] + }, + "githubPullRequests.codingAgent.uiIntegration": { + "type": "boolean", + "default": true, + "markdownDescription": "%githubPullRequests.codingAgent.uiIntegration.description%", + "tags": [ + "experimental", + "onExP" + ] }, "githubPullRequests.experimental.notificationsMarkPullRequests": { "type": "string", "markdownDescription": "%githubPullRequests.experimental.notificationsMarkPullRequests.description%", - "enum": ["markAsDone", "markAsRead", "none"], + "enum": [ + "markAsDone", + "markAsRead", + "none" + ], "default": "none" }, "githubPullRequests.experimental.useQuickChat": { @@ -481,6 +612,11 @@ "markdownDescription": "%githubPullRequests.experimental.useQuickChat.description%", "default": false }, + "githubPullRequests.webviewRefreshInterval": { + "type": "number", + "markdownDescription": "%githubPullRequests.webviewRefreshInterval.description%", + "default": 60 + }, "githubIssues.ignoreMilestones": { "type": "array", "default": [], @@ -495,13 +631,17 @@ "default": [ "TODO", "todo", - "BUG", "FIXME", "ISSUE", "HACK" ], "description": "%githubIssues.createIssueTriggers.description%" }, + "githubPullRequests.codingAgent.codeLens": { + "type": "boolean", + "default": true, + "description": "%githubPullRequests.codingAgent.codeLens.description%" + }, "githubIssues.createInsertFormat": { "type": "string", "enum": [ @@ -529,6 +669,7 @@ }, "default": [ "coffeescript", + "crystal", "diff", "dockerfile", "dockercompose", @@ -577,7 +718,7 @@ }, "githubIssues.issueCompletionFormatScm": { "type": "string", - "default": "${issueTitle} ${issueNumberLabel}", + "default": "${issueTitle}\nFixes ${issueNumberLabel}", "markdownDescription": "%githubIssues.issueCompletionFormatScm.markdownDescription%" }, "githubIssues.workingIssueFormatScm": { @@ -622,7 +763,9 @@ { "label": "%githubIssues.queries.default.myIssues%", "query": "is:open assignee:${user} repo:${owner}/${repository}", - "groupBy": ["milestone"] + "groupBy": [ + "milestone" + ] }, { "label": "%githubIssues.queries.default.createdIssues%", @@ -639,6 +782,19 @@ "default": true, "description": "%githubIssues.assignWhenWorking.description%" }, + "githubIssues.issueAvatarDisplay": { + "type": "string", + "enum": [ + "author", + "assignee" + ], + "enumDescriptions": [ + "%githubIssues.issueAvatarDisplay.author%", + "%githubIssues.issueAvatarDisplay.assignee%" + ], + "default": "author", + "description": "%githubIssues.issueAvatarDisplay.description%" + }, "githubPullRequests.focusedMode": { "properties": { "oneOf": [ @@ -656,6 +812,12 @@ "multiDiff", false ], + "enumDescriptions": [ + "%githubPullRequests.focusedMode.firstDiff%", + "%githubPullRequests.focusedMode.overview%", + "%githubPullRequests.focusedMode.multiDiff%", + "%githubPullRequests.focusedMode.false%" + ], "default": "multiDiff", "description": "%githubPullRequests.focusedMode.description%" }, @@ -697,7 +859,7 @@ "id": "pr:github", "name": "%view.pr.github.name%", "when": "ReposManagerStateContext != NeedsAuthentication && !github:resolvingConflicts", - "icon": "$(git-pull-request)", + "icon": "$(github-inverted)", "accessibilityHelpContent": "%view.pr.github.accessibilityHelpContent%" }, { @@ -710,9 +872,10 @@ { "id": "notifications:github", "name": "%view.notifications.github.name%", - "when": "ReposManagerStateContext != NeedsAuthentication && !github:resolvingConflicts && config.githubPullRequests.experimental.notificationsView && (remoteName != codespaces || !isWeb)", + "when": "ReposManagerStateContext != NeedsAuthentication && !github:resolvingConflicts && (remoteName != codespaces || !isWeb)", "icon": "$(bell)", - "accessibilityHelpContent": "%view.pr.github.accessibilityHelpContent%" + "accessibilityHelpContent": "%view.pr.github.accessibilityHelpContent%", + "visibility": "collapsed" }, { "id": "github:conflictResolution", @@ -727,6 +890,7 @@ "type": "webview", "name": "%view.github.create.pull.request.name%", "when": "github:createPullRequest || github:revertPullRequest", + "icon": "$(git-pull-request-create)", "visibility": "visible", "initialSize": 2 }, @@ -734,6 +898,7 @@ "id": "github:compareChangesFiles", "name": "%view.github.compare.changes.name%", "when": "github:createPullRequest", + "icon": "$(git-compare)", "visibility": "visible", "initialSize": 1 }, @@ -741,6 +906,7 @@ "id": "github:compareChangesCommits", "name": "%view.github.compare.changesCommits.name%", "when": "github:createPullRequest", + "icon": "$(git-compare)", "visibility": "visible", "initialSize": 1 }, @@ -748,7 +914,7 @@ "id": "prStatus:github", "name": "%view.pr.status.github.name%", "when": "github:inReviewMode && !github:createPullRequest && !github:revertPullRequest", - "icon": "$(git-pull-request)", + "icon": "$(diff-multiple)", "visibility": "visible", "initialSize": 3 }, @@ -757,16 +923,23 @@ "type": "webview", "name": "%view.github.active.pull.request.name%", "when": "github:inReviewMode && github:focusedReview && !github:createPullRequest && !github:revertPullRequest && github:activePRCount <= 1", + "icon": "$(code-review)", "initialSize": 2 }, { "id": "github:activePullRequest:welcome", "name": "%view.github.active.pull.request.welcome.name%", - "when": "!github:stateValidated && github:focusedReview" + "when": "!github:stateValidated && github:focusedReview", + "icon": "$(git-pull-request)" } ] }, "commands": [ + { + "command": "githubpr.remoteAgent", + "title": "%command.githubpr.remoteAgent.title%", + "enablement": "config.githubPullRequests.codingAgent.enabled" + }, { "command": "github.api.preloadPullRequest", "title": "Preload Pull Request", @@ -813,6 +986,12 @@ "title": "%command.pr.dismissNotification.title%", "category": "%command.pull.request.category%" }, + { + "command": "pr.markAllCopilotNotificationsAsRead", + "title": "%command.pr.markAllCopilotNotificationsAsRead.title%", + "category": "%command.pull.request.category%", + "enablement": "viewItem == copilot-query-with-notifications" + }, { "command": "pr.merge", "title": "%command.pr.merge.title%", @@ -824,8 +1003,18 @@ "category": "%command.pull.request.category%" }, { - "command": "pr.close", - "title": "%command.pr.close.title%", + "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%" }, { @@ -924,11 +1113,6 @@ "title": "%command.review.openLocalFile.title%", "icon": "$(go-to-file)" }, - { - "command": "review.suggestDiff", - "title": "%command.review.suggestDiff.title%", - "category": "%command.pull.request.category%" - }, { "command": "pr.refreshList", "title": "%command.pr.refreshList.title%", @@ -947,6 +1131,12 @@ "icon": "$(list-flat)", "category": "%command.pull.request.category%" }, + { + "command": "pr.toggleHideViewedFiles", + "title": "%command.pr.toggleHideViewedFiles.title%", + "icon": "$(filter)", + "category": "%command.pull.request.category%" + }, { "command": "pr.refreshChanges", "title": "%command.pr.refreshChanges.title%", @@ -1215,6 +1405,28 @@ "category": "%command.pull.request.category%", "icon": "$(eye)" }, + { + "command": "pr.checkoutFromDescription", + "title": "%command.pr.checkoutFromDescription.title%", + "category": "%command.pull.request.category%", + "icon": "$(git-compare)" + }, + { + "command": "pr.applyChangesFromDescription", + "title": "%command.pr.applyChangesFromDescription.title%", + "category": "%command.pull.request.category%", + "icon": "$(git-stash-apply)" + }, + { + "command": "pr.checkoutOnVscodeDevFromDescription", + "title": "%command.pr.checkoutOnVscodeDevFromDescription.title%", + "category": "%command.pull.request.category%" + }, + { + "command": "pr.openSessionLogFromDescription", + "title": "%command.pr.openSessionLogFromDescription.title%", + "category": "%command.pull.request.category%" + }, { "command": "review.diffWithPrHead", "title": "%command.review.diffWithPrHead.title%", @@ -1233,12 +1445,14 @@ { "command": "review.comment", "title": "%command.review.comment.title%", - "category": "%command.pull.request.category%" + "category": "%command.pull.request.category%", + "enablement": "github:reviewCommentCommentEnabled" }, { "command": "review.requestChanges", "title": "%command.review.requestChanges.title%", - "category": "%command.pull.request.category%" + "category": "%command.pull.request.category%", + "enablement": "github:reviewRequestChangesEnabled" }, { "command": "review.approveOnDotCom", @@ -1258,12 +1472,14 @@ { "command": "review.commentDescription", "title": "%command.review.comment.title%", - "category": "%command.pull.request.category%" + "category": "%command.pull.request.category%", + "enablement": "github:reviewCommentCommentEnabled" }, { "command": "review.requestChangesDescription", "title": "%command.review.requestChanges.title%", - "category": "%command.pull.request.category%" + "category": "%command.pull.request.category%", + "enablement": "github:reviewRequestChangesEnabled" }, { "command": "review.approveOnDotComDescription", @@ -1342,6 +1558,11 @@ "title": "%command.pr.copyVscodeDevPrLink.title%", "category": "%command.issues.category%" }, + { + "command": "pr.copyPrLink", + "title": "%command.pr.copyPrLink.title%", + "category": "%command.issues.category%" + }, { "command": "pr.refreshComments", "title": "%command.pr.refreshComments.title%", @@ -1533,6 +1754,19 @@ "category": "%command.issues.category%", "icon": "$(sparkle)" }, + { + "command": "issue.assignToCodingAgent", + "title": "%command.issue.assignToCodingAgent.title%", + "category": "%command.issues.category%", + "icon": "$(send-to-remote-agent)", + "enablement": "config.githubPullRequests.codingAgent.enabled" + }, + { + "command": "issues.configureIssuesViewlet", + "title": "%command.issues.configureIssuesViewlet.title%", + "category": "%command.issues.category%", + "icon": "$(gear)" + }, { "command": "notifications.refresh", "title": "%command.notifications.refresh.title%", @@ -1579,16 +1813,55 @@ "icon": "$(check-all)" }, { - "command": "notifications.markMergedPullRequestsAsRead", - "title": "%command.notifications.markMergedPullRequestsAsRead.title%", + "command": "notifications.markPullRequestsAsRead", + "title": "%command.notifications.markPullRequestsAsRead.title%", "category": "%command.notifications.category%", - "icon": "$(git-pull-request)" + "icon": "$(git-pull-request-done)" }, { - "command": "notifications.markMergedPullRequestsAsDone", - "title": "%command.notifications.markMergedPullRequestsAsDone.title%", + "command": "notifications.markPullRequestsAsDone", + "title": "%command.notifications.markPullRequestsAsDone.title%", "category": "%command.notifications.category%", - "icon": "$(git-pull-request)" + "icon": "$(git-pull-request-done)" + }, + { + "command": "notifications.configureNotificationsViewlet", + "title": "%command.notifications.configureNotificationsViewlet.title%", + "category": "%command.notifications.category%", + "icon": "$(gear)" + }, + { + "command": "pr.refreshChatSessions", + "title": "%command.pr.refreshChatSessions.title%", + "icon": "$(refresh)", + "category": "%command.pull.request.category%" + }, + { + "command": "pr.preferredCodingAgentGitHubRemote", + "title": "%command.pr.preferredCodingAgentGitHubRemote.title%", + "icon": "$(gear)", + "enablement": "github:hasMultipleGitHubRemotes", + "category": "%command.pull.request.category%" + }, + { + "command": "pr.resetCodingAgentPreferences", + "title": "%command.pr.resetCodingAgentPreferences.title%", + "category": "%command.pull.request.category%" + }, + { + "command": "pr.checkoutChatSessionPullRequest", + "title": "%command.pr.checkoutChatSessionPullRequest.title%", + "category": "%command.pull.request.category%" + }, + { + "command": "pr.closeChatSessionPullRequest", + "title": "%command.pr.closeChatSessionPullRequest.title%", + "category": "%command.pull.request.category%" + }, + { + "command": "pr.cancelCodingAgent", + "title": "%command.pr.cancelCodingAgent.title%", + "category": "%command.pull.request.category%" } ], "viewsWelcome": [ @@ -1599,9 +1872,14 @@ }, { "view": "pr:github", - "when": "gitNotInstalled", + "when": "gitNotInstalled && config.git.enabled != false", "contents": "%welcome.github.noGit.contents%" }, + { + "view": "pr:github", + "when": "gitNotInstalled && config.git.enabled == false", + "contents": "%welcome.github.noGitDisabled.contents%" + }, { "view": "github:login", "when": "ReposManagerStateContext == NeedsAuthentication && !github:hasGitHubRemotes && gitOpenRepositoryCount", @@ -1676,6 +1954,46 @@ "view": "github:activePullRequest:welcome", "when": "!github:stateValidated", "contents": "%welcome.github.activePullRequest.contents%" + }, + { + "view": "notifications:github", + "when": "!github:notificationCount && workspaceFolderCount > 0", + "contents": "%welcome.github.notificationsLoading.contents%" + }, + { + "view": "notifications:github", + "when": "workspaceFolderCount == 0", + "contents": "%welcome.issues.github.noFolder.contents%" + }, + { + "view": "notifications:github", + "when": "ReposManagerStateContext == RepositoriesLoaded && github:notificationCount == -1", + "contents": "%welcome.github.notifications.contents%" + }, + { + "view": "workbench.view.chat.sessions.copilot-swe-agent", + "when": "workspaceFolderCount == 0", + "contents": "%welcome.pr.github.noFolder.contents%" + }, + { + "view": "workbench.view.chat.sessions.copilot-swe-agent", + "when": "git.state == initialized && gitOpenRepositoryCount == 0 && workspaceFolderCount > 0 && git.parentRepositoryCount == 0", + "contents": "%welcome.pr.github.noRepo.contents%" + }, + { + "view": "workbench.view.chat.sessions.copilot-swe-agent", + "when": "git.state == initialized && workspaceFolderCount > 0 && (git.parentRepositoryCount > 0 || gitOpenRepositoryCount > 0) && !github:hasGitHubRemotes", + "contents": "%welcome.chat.sessions.copilot-swe-agent.noGitHub.contents%" + }, + { + "view": "workbench.view.chat.sessions.copilot-swe-agent", + "when": "ReposManagerStateContext == NeedsAuthentication && github:hasGitHubRemotes", + "contents": "%welcome.chat.sessions.copilot-swe-agent.login.contents%" + }, + { + "view": "workbench.view.chat.sessions.copilot-swe-agent", + "when": "ReposManagerStateContext != NeedsAuthentication && github:hasGitHubRemotes", + "contents": "%welcome.chat.sessions.copilot-swe-agent.startSession.contents%" } ], "keybindings": [ @@ -1709,6 +2027,10 @@ "command": "github.api.preloadPullRequest", "when": "false" }, + { + "command": "githubpr.remoteAgent", + "when": "false" + }, { "command": "pr.configureRemotes", "when": "gitHubOpenRepositoryCount != 0" @@ -1721,6 +2043,10 @@ "command": "pr.pick", "when": "false" }, + { + "command": "pr.checkoutFromReadonlyFile", + "when": "false" + }, { "command": "pr.openChanges", "when": "false" @@ -1737,6 +2063,10 @@ "command": "pr.dismissNotification", "when": "false" }, + { + "command": "pr.markAllCopilotNotificationsAsRead", + "when": "false" + }, { "command": "pr.resetViewedFiles", "when": "github:inReviewMode" @@ -1749,10 +2079,6 @@ "command": "review.openLocalFile", "when": "false" }, - { - "command": "pr.close", - "when": "gitHubOpenRepositoryCount != 0 && github:inReviewMode" - }, { "command": "pr.create", "when": "gitHubOpenRepositoryCount != 0 && github:authenticated" @@ -1767,7 +2093,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", @@ -1842,7 +2180,23 @@ "when": "false" }, { - "command": "review.suggestDiff", + "command": "pr.checkoutFromDescription", + "when": "false" + }, + { + "command": "pr.applyChangesFromDescription", + "when": "false" + }, + { + "command": "pr.checkoutChatSessionPullRequest", + "when": "false" + }, + { + "command": "pr.checkoutOnVscodeDevFromDescription", + "when": "false" + }, + { + "command": "pr.openSessionLogFromDescription", "when": "false" }, { @@ -1905,6 +2259,10 @@ "command": "pr.setFileListLayoutAsFlat", "when": "false" }, + { + "command": "pr.toggleHideViewedFiles", + "when": "false" + }, { "command": "pr.refreshChanges", "when": "false" @@ -2001,6 +2359,10 @@ "command": "pr.copyVscodeDevPrLink", "when": "github:inReviewMode && remoteName != codespaces && embedderIdentifier != github.dev" }, + { + "command": "pr.copyPrLink", + "when": "false" + }, { "command": "pr.goToNextDiffInPr", "when": "activeEditor == workbench.editors.textDiffEditor && resourcePath in github:unviewedFiles" @@ -2117,6 +2479,10 @@ "command": "issue.openIssue", "when": "false" }, + { + "command": "issue.assignToCodingAgent", + "when": "false" + }, { "command": "issue.copyIssueNumber", "when": "false" @@ -2217,6 +2583,10 @@ "command": "issue.copyGithubHeadLinkWithoutRange", "when": "false" }, + { + "command": "issues.configureIssuesViewlet", + "when": "false" + }, { "command": "pr.refreshActivePullRequest", "when": "false" @@ -2278,16 +2648,32 @@ "when": "false" }, { - "command": "notifications.markMergedPullRequestsAsRead", + "command": "notifications.markPullRequestsAsRead", + "when": "false" + }, + { + "command": "notifications.markPullRequestsAsDone", "when": "false" }, { - "command": "notifications.markMergedPullRequestsAsDone", + "command": "notifications.configureNotificationsViewlet", "when": "false" }, { "command": "review.copyPrLink", "when": "github:inReviewMode" + }, + { + "command": "pr.preferredCodingAgentGitHubRemote", + "when": "false" + }, + { + "command": "pr.closeChatSessionPullRequest", + "when": "false" + }, + { + "command": "pr.cancelCodingAgent", + "when": "false" } ], "view/title": [ @@ -2331,6 +2717,11 @@ "when": "view == prStatus:github && !fileListLayout:flat", "group": "navigation1" }, + { + "command": "pr.toggleHideViewedFiles", + "when": "view == prStatus:github", + "group": "navigation1" + }, { "command": "pr.toggleEditorCommentingOn", "when": "view == prStatus:github && !commentingEnabled", @@ -2357,7 +2748,7 @@ "group": "overflow@1" }, { - "command": "pr.configurePRViewlet", + "command": "issues.configureIssuesViewlet", "when": "gitHubOpenRepositoryCount != 0 && github:initialized && view == issues:github", "group": "overflow@2" }, @@ -2417,12 +2808,17 @@ "group": "sortNotifications@2" }, { - "command": "notifications.markMergedPullRequestsAsRead", + "command": "notifications.configureNotificationsViewlet", + "when": "view == notifications:github", + "group": "sortNotifications@3" + }, + { + "command": "notifications.markPullRequestsAsRead", "when": "gitHubOpenRepositoryCount != 0 && github:initialized && view == notifications:github && config.githubPullRequests.experimental.notificationsMarkPullRequests == markAsRead", "group": "navigation@0" }, { - "command": "notifications.markMergedPullRequestsAsDone", + "command": "notifications.markPullRequestsAsDone", "when": "gitHubOpenRepositoryCount != 0 && github:initialized && view == notifications:github && config.githubPullRequests.experimental.notificationsMarkPullRequests == markAsDone", "group": "navigation@0" }, @@ -2430,6 +2826,16 @@ "command": "notifications.refresh", "when": "gitHubOpenRepositoryCount != 0 && github:initialized && view == notifications:github", "group": "navigation@1" + }, + { + "command": "pr.refreshChatSessions", + "when": "view == workbench.view.chat.sessions.copilot-swe-agent", + "group": "navigation@1" + }, + { + "command": "pr.preferredCodingAgentGitHubRemote", + "when": "github:hasMultipleGitHubRemotes && view == workbench.view.chat.sessions.copilot-swe-agent", + "group": "overflow@1" } ], "view/item/context": [ @@ -2478,6 +2884,11 @@ "when": "view == pr:github && viewItem =~ /pullrequest(.*):notification/", "group": "4_pullrequest@5" }, + { + "command": "pr.markAllCopilotNotificationsAsRead", + "when": "view == pr:github && viewItem =~ /copilot-query/", + "group": "0_category@1" + }, { "command": "issue.chatSummarizeIssue", "when": "view == pr:github && viewItem =~ /pullrequest/ && github.copilot-chat.activated && config.githubPullRequests.experimental.chat", @@ -2490,7 +2901,7 @@ }, { "command": "pr.openChanges", - "when": "view =~ /prStatus:github/ && viewItem =~ /(description)/ && config.multiDiffEditor.experimental.enabled", + "when": "view =~ /(pr|prStatus):github/ && viewItem =~ /(pullrequest|description)/ && config.multiDiffEditor.experimental.enabled", "group": "inline@0" }, { @@ -2657,6 +3068,11 @@ "when": "view == issues:github && viewItem =~ /^(link)?(current|continue)?issue/ && github.copilot-chat.activated && config.githubPullRequests.experimental.chat", "group": "issues_1@1" }, + { + "command": "issue.assignToCodingAgent", + "when": "view == issues:github && viewItem =~ /^(link)?(current|continue)?issue/ && config.githubPullRequests.codingAgent.enabled", + "group": "issues_1@2" + }, { "command": "issue.copyIssueNumber", "when": "view == issues:github && viewItem =~ /^(link)?(current|continue)?issue/", @@ -2758,11 +3174,6 @@ } ], "scm/title": [ - { - "command": "review.suggestDiff", - "when": "scmProvider =~ /^git|^remoteHub:github/ && scmProviderRootUri in github:reposInReviewMode", - "group": "inline" - }, { "command": "pr.create", "when": "scmProvider =~ /^git|^remoteHub:github/ && scmProviderRootUri in github:reposNotInReviewMode", @@ -2783,6 +3194,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", @@ -3110,16 +3528,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", @@ -3130,16 +3551,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", @@ -3148,6 +3572,76 @@ { "command": "review.requestChangesOnDotComDescription", "when": "webviewId == PullRequestOverview && github:reviewCommentMenu && github:reviewCommentRequestChangesOnDotCom" + }, + { + "command": "pr.copyPrLink", + "when": "webviewId == PullRequestOverview && github:copyMenu" + }, + { + "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", + "when": "webviewId == PullRequestOverview && github:checkoutMenu" + }, + { + "command": "pr.checkoutOnVscodeDevFromDescription", + "group": "checkout@1", + "when": "webviewId == PullRequestOverview && github:checkoutMenu" + }, + { + "command": "pr.openSessionLogFromDescription", + "when": "webviewId == PullRequestOverview && github:codingAgentMenu" + } + ], + "chat/chatSessions": [ + { + "command": "pr.openChanges", + "when": "chatSessionType == copilot-swe-agent || chatSessionType == copilot-cloud-agent", + "group": "inline" + }, + { + "command": "pr.checkoutChatSessionPullRequest", + "when": "chatSessionType == copilot-swe-agent || chatSessionType == copilot-cloud-agent", + "group": "context" + }, + { + "command": "pr.closeChatSessionPullRequest", + "when": "chatSessionType == copilot-swe-agent", + "group": "context" + }, + { + "command": "pr.cancelCodingAgent", + "when": "chatSessionType == copilot-swe-agent", + "group": "context" + } + ], + "chat/multiDiff/context": [ + { + "command": "pr.checkoutFromDescription", + "when": "chatSessionType == copilot-cloud-agent && workspaceFolderCount > 0" + }, + { + "command": "pr.applyChangesFromDescription", + "when": "chatSessionType == copilot-cloud-agent && workspaceFolderCount > 0" } ] }, @@ -3175,8 +3669,8 @@ { "id": "issues.closed", "defaults": { - "dark": "#cb2431", - "light": "#cb2431", + "dark": "pullRequests.merged", + "light": "pullRequests.merged", "highContrast": "editor.foreground", "highContrastLight": "editor.foreground" }, @@ -3215,8 +3709,8 @@ { "id": "pullRequests.closed", "defaults": { - "dark": "issues.closed", - "light": "issues.closed", + "dark": "#cb2431", + "light": "#cb2431", "highContrast": "editor.background", "highContrastLight": "editor.background" }, @@ -3245,6 +3739,37 @@ } ], "languageModelTools": [ + { + "name": "github-pull-request_copilot-coding-agent", + "displayName": "%languageModelTools.github-pull-request_copilot-coding-agent.displayName%", + "modelDescription": "Completes the provided task using an asynchronous coding agent. Use when the user wants copilot continue completing a task in the background or asynchronously. IMPORTANT: Use this tool LAST/FINAL when users mention '#github-pull-request_copilot-coding-agent' in their query. This indicates they want the task/job implemented by the remote coding agent after all other analysis, planning, and preparation is complete. Call this tool at the END to hand off the fully-scoped task to the asynchronous GitHub Copilot coding agent. The agent will create a new branch, implement the changes, and open a pull request. Always use this tool as the final step when the hashtag is mentioned, after completing any other necessary tools or analysis first.", + "when": "config.githubPullRequests.codingAgent.enabled", + "icon": "$(send-to-remote-agent)", + "canBeReferencedInPrompt": true, + "toolReferenceName": "copilotCodingAgent", + "userDescription": "%languageModelTools.github-pull-request_copilot-coding-agent.userDescription%", + "inputSchema": { + "type": "object", + "required": [ + "title", + "body" + ], + "properties": { + "title": { + "type": "string", + "description": "The title of the issue. Populate from chat context." + }, + "body": { + "type": "string", + "description": "The body/description of the issue. Populate from chat context." + }, + "existingPullRequest": { + "type": "number", + "description": "The number of an existing pull request related to the current coding agent task. Look in the chat history for this number. In the chat it may look like 'Coding agent will continue work in #17...'. In this example, you should return '17'." + } + } + } + }, { "name": "github-pull-request_issue_fetch", "tags": [ @@ -3256,7 +3781,7 @@ "displayName": "%languageModelTools.github-pull-request_issue_fetch.displayName%", "modelDescription": "Get a GitHub issue/PR's details as a JSON object.", "icon": "$(info)", - "canBeReferencedInPrompt": false, + "canBeReferencedInPrompt": true, "inputSchema": { "type": "object", "properties": { @@ -3512,7 +4037,7 @@ "displayName": "%languageModelTools.github-pull-request_suggest-fix.displayName%", "modelDescription": "Summarize and suggest a fix for a GitHub issue.", "icon": "$(info)", - "canBeReferencedInPrompt": false, + "canBeReferencedInPrompt": true, "inputSchema": { "type": "object", "properties": { @@ -3559,7 +4084,7 @@ "displayName": "%languageModelTools.github-pull-request_formSearchQuery.displayName%", "modelDescription": "Converts natural language to a GitHub search query. Should ALWAYS be called before doing a search.", "icon": "$(search)", - "canBeReferencedInPrompt": false, + "canBeReferencedInPrompt": true, "inputSchema": { "type": "object", "properties": { @@ -3603,7 +4128,7 @@ "displayName": "%languageModelTools.github-pull-request_doSearch.displayName%", "modelDescription": "Execute a GitHub search given a well formed GitHub search query. Call github-pull-request_formSearchQuery first to get good search syntax and pass the exact result in as the 'query'.", "icon": "$(search)", - "canBeReferencedInPrompt": false, + "canBeReferencedInPrompt": true, "inputSchema": { "type": "object", "properties": { @@ -3649,7 +4174,7 @@ "displayName": "%languageModelTools.github-pull-request_renderIssues.displayName%", "modelDescription": "Render issue items from an issue search in a markdown table. The markdown table will be displayed directly to the user by the tool. No further display should be done after this!", "icon": "$(paintcan)", - "canBeReferencedInPrompt": false, + "canBeReferencedInPrompt": true, "inputSchema": { "type": "object", "properties": { @@ -3735,22 +4260,26 @@ } } }, - "commentsCount": { + "commentCount": { "type": "number", "description": "The number of comments on the issue." + }, + "reactionCount": { + "type": "number", + "description": "The number of reactions on the issue." } - } - }, - "required": [ - "title", - "number", - "url", - "state", - "createdAt", - "author", - "commentCount", - "reactionCount" - ] + }, + "required": [ + "title", + "number", + "url", + "state", + "createdAt", + "author", + "commentCount", + "reactionCount" + ] + } }, "totalIssues": { "type": "number", @@ -3772,11 +4301,25 @@ ], "toolReferenceName": "activePullRequest", "displayName": "%languageModelTools.github-pull-request_activePullRequest.displayName%", - "modelDescription": "Get information about the active GitHub pull request. This information includes: comments, files changed, pull request title + description, pull request state, and pull request status checks/CI. When asked about the active pull request, do this first!", - "icon": "$(pull-request)", + "modelDescription": "Get comprehensive information about the active GitHub pull request (PR). The active PR is the one that is currently checked out. This includes the PR title, full description, list of changed files, review comments, PR state, and status checks/CI results. For PRs created by Copilot, it also includes the session logs which indicate the development process and decisions made by the coding agent. When asked about the active or current pull request, do this first! Use this tool for any request related to \"current changes,\" \"pull request details,\" \"what changed,\" \"PR status,\" or similar queries even if the user does not explicitly mention \"pull request.\" When asked to use this tool, ALWAYS use it.", + "icon": "$(git-pull-request)", "canBeReferencedInPrompt": true, "userDescription": "%languageModelTools.github-pull-request_activePullRequest.description%", "when": "config.githubPullRequests.experimental.chat" + }, + { + "name": "github-pull-request_openPullRequest", + "tags": [ + "github", + "pull request" + ], + "toolReferenceName": "openPullRequest", + "displayName": "%languageModelTools.github-pull-request_openPullRequest.displayName%", + "modelDescription": "Get comprehensive information about the GitHub pull request (PR) which is currently visible, but not necessarily checked out. This includes the PR title, full description, list of changed files, review comments, PR state, and status checks/CI results. For PRs created by Copilot, it also includes the session logs which indicate the development process and decisions made by the coding agent. When asked about the currently open pull request, do this first! Use this tool for any request related to \"pull request details,\" \"what changed,\" \"PR status,\" or similar queries even if the user does not explicitly mention \"pull request.\" When asked to use this tool, ALWAYS use it.", + "icon": "$(git-pull-request)", + "canBeReferencedInPrompt": true, + "userDescription": "%languageModelTools.github-pull-request_openPullRequest.description%", + "when": "config.githubPullRequests.experimental.chat" } ] }, @@ -3788,10 +4331,10 @@ "clean": "rm -r dist/", "compile": "webpack --mode development --env esbuild", "compile:test": "tsc -p tsconfig.test.json", + "watch:test": "tsc -w -p tsconfig.test.json", "compile:node": "webpack --mode development --config-name extension:node --config-name webviews", "compile:web": "webpack --mode development --config-name extension:webworker --config-name webviews", - "lint": "eslint --fix --cache --config .eslintrc.json --ignore-pattern src/env/browser/**/* \"{src,webviews}/**/*.{ts,tsx}\"", - "lint:browser": "eslint --fix --cache --cache-location .eslintcache.browser --config .eslintrc.browser.json --ignore-pattern src/env/node/**/* \"{src,webviews}/**/*.{ts,tsx}\"", + "lint": "eslint --fix --cache . --ext .ts,.tsx", "package": "npx vsce package --yarn", "test": "yarn run test:preprocess && node ./out/src/test/runTests.js", "test:preprocess": "yarn run compile:test && yarn run test:preprocess-gql && yarn run test:preprocess-svg && yarn run test:preprocess-fixtures", @@ -3800,41 +4343,49 @@ "test:preprocess-gql": "node scripts/preprocess-gql --in src/github/queries.gql --out out/src/github/queries.gql && node scripts/preprocess-gql --in src/github/queriesExtra.gql --out out/src/github/queriesExtra.gql && node scripts/preprocess-gql --in src/github/queriesShared.gql --out out/src/github/queriesShared.gql && node scripts/preprocess-gql --in src/github/queriesLimited.gql --out out/src/github/queriesLimited.gql", "test:preprocess-svg": "node scripts/preprocess-svg --in ../resources/ --out out/resources", "test:preprocess-fixtures": "node scripts/preprocess-fixtures --in src --out out", - "update-dts": "cd \"src/@types\" && npx vscode-dts main && npx vscode-dts dev", + "update-dts": "cd \"src/@types\" && node ../../node_modules/@vscode/dts/index.js main && node ../../node_modules/@vscode/dts/index.js dev", "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" + "check:commands": "node scripts/check-commands.js", + "prepare": "husky install", + "update:codicons": "npx ts-node --project tsconfig.scripts.json build/update-codicons.ts" }, "devDependencies": { + "@eslint/js": "^9.36.0", + "@shikijs/monaco": "^3.7.0", "@types/chai": "^4.1.4", "@types/glob": "7.1.3", "@types/lru-cache": "^5.1.0", "@types/marked": "^0.7.2", "@types/mocha": "^8.2.2", - "@types/node": "18.17.1", + "@types/node": "22", "@types/react": "^16.8.4", "@types/react-dom": "^16.8.2", "@types/sinon": "7.0.11", "@types/temp": "0.8.34", - "@types/vscode": "1.89.0", + "@types/vscode": "1.103.0", "@types/webpack-env": "^1.16.0", - "@typescript-eslint/eslint-plugin": "6.10.0", - "@typescript-eslint/parser": "6.10.0", - "@vscode/test-electron": "^2.3.8", - "@vscode/test-web": "^0.0.29", + "@typescript-eslint/eslint-plugin": "^8.44.0", + "@typescript-eslint/parser": "^8.44.0", + "@vscode/dts": "^0.4.1", + "@vscode/test-cli": "^0.0.11", + "@vscode/test-electron": "^2.5.2", + "@vscode/test-web": "^0.0.71", "assert": "^2.0.0", "buffer": "^6.0.3", "constants-browserify": "^1.0.0", "crypto-browserify": "3.12.0", - "css-loader": "5.1.3", + "css-loader": "7.1.2", "esbuild-loader": "4.2.2", - "eslint": "7.22.0", - "eslint-cli": "1.1.1", - "eslint-plugin-import": "2.22.1", + "eslint": "^9.36.0", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import": "2.31.0", + "eslint-plugin-rulesdir": "^0.2.2", "event-stream": "^4.0.1", - "fork-ts-checker-webpack-plugin": "6.1.1", + "fork-ts-checker-webpack-plugin": "9.1.0", "glob": "7.1.6", + "globals": "^16.4.0", "graphql": "15.5.0", "graphql-tag": "2.11.0", "gulp-filter": "^7.0.0", @@ -3852,27 +4403,30 @@ "p-all": "^1.0.0", "path-browserify": "1.0.1", "process": "^0.11.10", - "raw-loader": "4.0.2", "react-testing-library": "7.0.1", "sinon": "9.0.0", "source-map-support": "0.5.19", "stream-browserify": "^3.0.0", - "style-loader": "2.0.0", + "style-loader": "4.0.0", "svg-inline-loader": "^0.8.2", "temp": "0.9.4", "terser-webpack-plugin": "5.1.1", "timers-browserify": "^2.0.12", - "ts-loader": "8.0.18", + "ts-loader": "9.5.2", + "ts-node": "^10.9.2", "tty": "1.0.1", - "typescript": "4.5.5", + "typescript": "^5.9.2", + "typescript-eslint": "^8.44.0", "typescript-formatter": "^7.2.2", "vinyl-fs": "^3.0.3", "webpack": "5.94.0", "webpack-cli": "4.2.0" }, "dependencies": { - "@octokit/rest": "20.1.2", - "@octokit/types": "13.8.0", + "@joaomoreno/unique-names-generator": "^5.2.0", + "@octokit/rest": "22.0.0", + "@octokit/types": "14.1.0", + "@vscode/codicons": "^0.0.36", "@vscode/extension-telemetry": "0.7.5", "@vscode/prompt-tsx": "^0.3.0-alpha.12", "apollo-boost": "^0.4.9", @@ -3883,17 +4437,24 @@ "debounce": "^1.2.1", "events": "3.2.0", "fast-deep-equal": "^3.1.3", + "jsonc-parser": "^3.3.1", + "jszip": "^3.10.1", "lru-cache": "6.0.0", + "markdown-it": "^14.1.0", "marked": "^4.0.10", "react": "^16.12.0", "react-dom": "^16.12.0", "ssh-config": "4.1.1", "stream-http": "^3.2.0", + "temporal-polyfill": "^0.3.0", "tunnel": "0.0.6", "url-search-params-polyfill": "^8.1.1", "uuid": "8.3.2", "vscode-tas-client": "^0.1.84", "vsls": "^0.3.967" }, + "resolutions": { + "string_decoder": "^1.3.0" + }, "license": "MIT" } \ No newline at end of file diff --git a/package.nls.json b/package.nls.json index 10d94fbd10..a352820de6 100644 --- a/package.nls.json +++ b/package.nls.json @@ -13,25 +13,33 @@ "githubPullRequests.defaultCreateOption.createAutoMerge": "The pull request will be created with auto-merge enabled. The merge method selected will be the default for the repo or the value of `githubPullRequests.defaultMergeMethod` if set.", "githubPullRequests.createDraft": "Whether the \"Draft\" checkbox will be checked by default when creating a pull request.", "githubPullRequests.logLevel.description": "Logging for GitHub Pull Request extension. The log is emitted to the output channel named as GitHub Pull Request.", - "githubPullRequests.logLevel.markdownDeprecationMessage":{ + "githubPullRequests.logLevel.markdownDeprecationMessage": { "message": "Log level is now controlled by the [Developer: Set Log Level...](command:workbench.action.setLogLevel) command. You can set the log level for the current session and also the default log level from there.", - "comment" : [ + "comment": [ "Do not translate what's inside of (...). It is link syntax.", "{Locked='](command:workbench.action.setLogLevel)'}" ] }, + "githubPullRequests.branchListTimeout.description": "Maximum time in milliseconds to wait when fetching the list of branches for pull request creation. Repositories with thousands of branches may need a higher value to ensure all branches (including the default branch) are retrieved. Minimum value is 1000 (1 second).", + "githubPullRequests.codingAgent.description": "Enables integration with the asynchronous Copilot coding agent. The '#copilotCodingAgent' tool will be available in agent mode when this setting is enabled.", + "githubPullRequests.codingAgent.uiIntegration.description": "Enables UI integration within VS Code to create new coding agent sessions.", + "githubPullRequests.codingAgent.autoCommitAndPush.description": "Allow automatic git operations (commit, push) to be performed when starting a coding agent session", + "githubPullRequests.codingAgent.promptForConfirmation.description": "Prompt for confirmation before initiating a coding agent session from the UI integration.", "githubPullRequests.remotes.markdownDescription": "List of remotes, by name, to fetch pull requests from.", - "githubPullRequests.queries.markdownDescription": "Specifies what queries should be used in the GitHub Pull Requests tree. All queries are made against **the currently opened repos**. Each query object has a `label` that will be shown in the tree and a search `query` using [GitHub search syntax](https://help.github.com/en/articles/understanding-the-search-syntax). The following variables can be used: \n - `${user}` will resolve to the currently logged in user \n - `${owner}` will resolve to the owner of the current repository, ex. `microsoft` in `microsoft/vscode` \n - `${repository}` will resolve to the repository name, ex. `vscode` in `microsoft/vscode` \n - `${today-Nd}`, where `N` is the number of days ago, will resolve to a date, ex. `2025-01-04`. \n\n By default these queries define the categories \"Waiting For My Review\", \"Assigned To Me\" and \"Created By Me\". If you want to preserve these, make sure they are still in the array when you modify the setting.", - "githubPullRequests.queries.label.description": "The label to display for the query in the Pull Requests tree", - "githubPullRequests.queries.query.description": "The query used for searching pull requests.", + "githubPullRequests.queries.markdownDescription": "Specifies what queries should be used in the GitHub Pull Requests tree. All queries are made against **the currently opened repos**. Each query object has a `label` that will be shown in the tree and a search `query` using [GitHub search syntax](https://help.github.com/en/articles/understanding-the-search-syntax). By default these queries define the categories \"Copilot on My Behalf\", \"Local Pull Request Branches\", \"Waiting For My Review\", \"Assigned To Me\" and \"Created By Me\". If you want to preserve these, make sure they are still in the array when you modify the setting. \n\n**Variables available:**\n - `${user}` - currently logged in user \n - `${owner}` - repository owner, ex. `microsoft` in `microsoft/vscode` \n - `${repository}` - repository name, ex. `vscode` in `microsoft/vscode` \n - `${today-Nd}` - date N days ago, ex. `${today-7d}` becomes `2025-01-04`\n\n**Example custom queries:**\n```json\n\"githubPullRequests.queries\": [\n {\n \"label\": \"Waiting For My Review\",\n \"query\": \"is:open review-requested:${user}\"\n },\n {\n \"label\": \"Mentioned Me\",\n \"query\": \"is:open mentions:${user}\"\n },\n {\n \"label\": \"Recent Activity\",\n \"query\": \"is:open updated:>${today-7d}\"\n }\n]\n```", + "githubPullRequests.queries.label.description": "The label to display for the query in the Pull Requests tree.", + "githubPullRequests.queries.query.description": "The GitHub search query for finding pull requests. Use GitHub search syntax with variables like ${user}, ${owner}, ${repository}. Example: 'is:open author:${user}' finds your open pull requests.", + "githubPullRequests.queries.copilotOnMyBehalf": "Copilot on My Behalf", "githubPullRequests.queries.waitingForMyReview": "Waiting For My Review", "githubPullRequests.queries.assignedToMe": "Assigned To Me", "githubPullRequests.queries.createdByMe": "Created By Me", "githubPullRequests.defaultMergeMethod.description": "The method to use when merging pull requests.", "githubPullRequests.notifications.description": "If GitHub notifications should be shown to the user.", "githubPullRequests.fileListLayout.description": "The layout to use when displaying changed files list.", + "githubPullRequests.hideViewedFiles.description": "Hide files that have been marked as viewed in the pull request changes tree.", "githubPullRequests.defaultDeletionMethod.selectLocalBranch.description": "When true, the option to delete the local branch will be selected by default when deleting a branch from a pull request.", "githubPullRequests.defaultDeletionMethod.selectRemote.description": "When true, the option to delete the remote will be selected by default when deleting a branch from a pull request.", + "githubPullRequests.deleteBranchAfterMerge.description": "Automatically delete the branch after merging a pull request. This setting only applies when the pull request is merged through this extension.", "githubPullRequests.terminalLinksHandler.description": "Default handler for terminal links.", "githubPullRequests.terminalLinksHandler.github": "Create the pull request on GitHub", "githubPullRequests.terminalLinksHandler.vscode": "Create the pull request in VS Code", @@ -52,16 +60,17 @@ "Do not translate what's inside of the '${..}'. It is an internal syntax for the extension" ] }, - "githubPullRequests.pushBranch.description": "Push the \"from\" branch when creating a PR and the \"from\" branch is not available on the remote.", - "githubPullRequests.pushBranch.prompt": "Prompt to push the branch when creating a PR and the \"from\" branch is not available on the remote.", - "githubPullRequests.pushBranch.always": "Always push the branch when creating a PR and the \"from\" branch is not available on the remote.", - "githubPullRequests.pullBranch.description": "Pull changes from the remote when a PR branch is checked out locally. Changes are detected when the PR is manually refreshed and during periodic background updates.", - "githubPullRequests.pullBranch.prompt": "Prompt to pull a PR branch when changes are detected in the PR.", - "githubPullRequests.pullBranch.never": "Never pull a PR branch when changes are detected in the PR.", - "githubPullRequests.pullBranch.always": "Always pull a PR branch when changes are detected in the PR. When `\"git.autoStash\": true` this will instead `prompt` to prevent unexpected file changes.", + "githubPullRequests.pushBranch.description": "Push the \"from\" branch when creating a pull request and the \"from\" branch is not available on the remote.", + "githubPullRequests.pushBranch.prompt": "Prompt to push the branch when creating a pull request and the \"from\" branch is not available on the remote.", + "githubPullRequests.pushBranch.always": "Always push the branch when creating a pull request and the \"from\" branch is not available on the remote.", + "githubPullRequests.pullBranch.description": "Pull changes from the remote when a pull request branch is checked out locally. Changes are detected when the pull request is manually refreshed and during periodic background updates.", + "githubPullRequests.pullBranch.prompt": "Prompt to pull a pull request branch when changes are detected in the pull request.", + "githubPullRequests.pullBranch.never": "Never pull a pull request branch when changes are detected in the pull request.", + "githubPullRequests.pullBranch.always": "Always pull a pull request branch when changes are detected in the pull request. When `\"git.autoStash\": true` this will instead `prompt` to prevent unexpected file changes.", "githubPullRequests.allowFetch.description": "Allows `git fetch` to be run for checked-out pull request branches when checking for updates to the pull request.", "githubPullRequests.ignoredPullRequestBranches.description": "Prevents branches that are associated with a pull request from being automatically detected. This will prevent review mode from being entered on these branches.", "githubPullRequests.ignoredPullRequestBranches.items": "Branch name", + "githubPullRequests.ignoreSubmodules.description": "Prevents repositories that are submodules from being managed by the GitHub Pull Requests extension. A window reload is required for changes to this setting to take effect.", "githubPullRequests.neverIgnoreDefaultBranch.description": "Never offer to ignore a pull request associated with the default branch of a repository.", "githubPullRequests.overrideDefaultBranch.description": "The default branch for a repository is set on github.com. With this setting, you can override that default with another branch.", "githubPullRequests.postCreate.description": "The action to take after creating a pull request.", @@ -70,6 +79,9 @@ "githubPullRequests.postCreate.checkoutDefaultBranch": "Checkout the default branch of the repository", "githubPullRequests.postCreate.checkoutDefaultBranchAndShow": "Checkout the default branch of the repository and show the pull request in the Pull Requests view", "githubPullRequests.postCreate.checkoutDefaultBranchAndCopy": "Checkout the default branch of the repository and copy a link to the pull request to the clipboard", + "githubPullRequests.postDone.description": "The action to take after using the 'checkout default branch' or 'delete branch' actions on a currently checked out pull request.", + "githubPullRequests.postDone.checkoutDefaultBranch": "Checkout the default branch of the repository", + "githubPullRequests.postDone.checkoutDefaultBranchAndPull": "Checkout the default branch of the repository and pull the latest changes", "githubPullRequests.defaultCommentType.description": "The default comment type to use when submitting a comment and there is no active review", "githubPullRequests.defaultCommentType.single": "Submits the comment as a single comment that will be immediately visible to other users", "githubPullRequests.defaultCommentType.review": "Submits the comment as a review comment that will be visible to other users once the review is submitted", @@ -88,11 +100,13 @@ "githubPullRequests.createDefaultBaseBranch.auto": "When the current repository is a fork, this will work like \"repositoryDefault\". Otherwise, it will work like \"createdFromBranch\".", "githubPullRequests.experimental.chat.description": "Enables the `@githubpr` Copilot chat participant in the chat view. `@githubpr` can help search for issues and pull requests, suggest fixes for issues, and summarize issues, pull requests, and notifications.", "githubPullRequests.experimental.notificationsView.description": "Enables the notifications view, which shows a list of your GitHub notifications. When combined with `#githubPullRequests.experimental.chat#`, you can have Copilot sort and summarize your notifications. View will not show in a Codespace accessed from the browser.", - "githubPullRequests.experimental.notificationsMarkPullRequests.description": "Adds an action in the Notifications view to mark merged pull requests with no reviews, comments, or commits since you last viewed the pull request as read.", + "githubPullRequests.experimental.notificationsMarkPullRequests.description": "Adds an action in the Notifications view to mark pull requests with no non-empty reviews, comments, or commits since you last viewed the pull request as read.", "githubPullRequests.experimental.useQuickChat.description": "Controls whether the Copilot \"Summarize\" commands in the Pull Requests, Issues, and Notifications views will use quick chat. Only has an effect if `#githubPullRequests.experimental.chat#` is enabled.", + "githubPullRequests.webviewRefreshInterval.description": "The interval, in seconds, at which the pull request and issues webviews are refreshed when the webview is the active tab.", "githubIssues.ignoreMilestones.description": "An array of milestones titles to never show issues from.", "githubIssues.createIssueTriggers.description": "Strings that will cause the 'Create issue from comment' code action to show.", "githubIssues.createIssueTriggers.items": "String that enables the 'Create issue from comment' code action. Should not contain whitespace.", + "githubPullRequests.codingAgent.codeLens.description": "Show the 'Delegate to agent' CodeLens actions above TODO comments for delegating to coding agent.", "githubIssues.createInsertFormat.description": "Controls whether an issue number (ex. #1234) or a full url (ex. https://github.com/owner/name/issues/1234) is inserted when the Create Issue code action is run.", "githubIssues.issueCompletions.enabled.description": "Controls whether completion suggestions are shown for issues.", "githubIssues.userCompletions.enabled.description": "Controls whether completion suggestions are shown for users.", @@ -101,7 +115,7 @@ "githubIssues.ignoreUserCompletionTrigger.description": "Languages that the '@' character should not be used to trigger user completion suggestions.", "githubIssues.ignoreUserCompletionTrigger.items": "Language that user completions should not trigger on '@'.", "githubIssues.issueBranchTitle.markdownDescription": { - "message": "Advanced settings for the name of the branch that is created when you start working on an issue. \n- `${user}` will be replace with the currently logged in username \n- `${issueNumber}` will be replaced with the current issue number \n- `${sanitizedIssueTitle}` will be replaced with the issue title, with all spaces and unsupported characters (https://git-scm.com/docs/git-check-ref-format) removed. For lowercase, use `${sanitizedLowercaseIssueTitle}` ", + "message": "Advanced settings for the name of the branch that is created when you start working on an issue. \n- `${user}` will be replaced with the currently logged in username \n- `${issueNumber}` will be replaced with the current issue number \n- `${sanitizedIssueTitle}` will be replaced with the issue title, with all spaces and unsupported characters (https://git-scm.com/docs/git-check-ref-format) removed. For lowercase, use `${sanitizedLowercaseIssueTitle}` ", "comment": [ "{Locked='${...}'}", "Do not translate what's inside of the '${..}'. It is an internal syntax for the extension" @@ -139,7 +153,14 @@ "githubIssues.queries.default.createdIssues": "Created Issues", "githubIssues.queries.default.recentIssues": "Recent Issues", "githubIssues.assignWhenWorking.description": "Assigns the issue you're working on to you. Only applies when the issue you're working on is in a repo you currently have open.", + "githubIssues.issueAvatarDisplay.description": "Controls which avatar to display in the issue list.", + "githubIssues.issueAvatarDisplay.author": "Show the avatar of the issue creator", + "githubIssues.issueAvatarDisplay.assignee": "Show the avatar of the first assignee (show GitHub icon if no assignees)", "githubPullRequests.focusedMode.description": "The layout to use when a pull request is checked out. Set to false to prevent layout changes.", + "githubPullRequests.focusedMode.firstDiff": "Show the first diff in the pull request. If there are no changes, show the overview.", + "githubPullRequests.focusedMode.overview": "Show the overview of the pull request.", + "githubPullRequests.focusedMode.multiDiff": "Show all diffs in the pull request. If there are no changes, show the overview.", + "githubPullRequests.focusedMode.false": "Do not change the layout.", "githubPullRequests.showPullRequestNumberInTree.description": "Shows the pull request number in the tree view.", "githubPullRequests.labelCreated.description": "Group of labels that you want to add to the pull request automatically. Labels that don't exist in the repository won't be added.", "githubPullRequests.labelCreated.label.description": "Each string element is value of label that you want to add", @@ -149,7 +170,7 @@ "view.github.login.name": "Login", "view.pr.github.name": "Pull Requests", "view.pr.github.accessibilityHelpContent": { - "message":"Helpful commands include:\n-GitHub Pull Requests: Refresh Pull Requests List\n-GitHub Pull Requests: Focus on Issues View \n-GitHub Pull Requests: Focus on Pull Requests View\n-GitHub Issues: Copy GitHub Permalink\n-GitHub Issues: Create an Issue\n-GitHub Pull Requests: Create Pull Request", + "message": "Helpful commands include:\n-GitHub Pull Requests: Refresh Pull Requests List\n-GitHub Pull Requests: Focus on Issues View \n-GitHub Pull Requests: Focus on Pull Requests View\n-GitHub Issues: Copy GitHub Permalink\n-GitHub Issues: Create an Issue\n-GitHub Pull Requests: Create Pull Request", "comment": [ "Do not translate the contents of (...) or <...> in the message. They are commands that will be replaced with the actual command name and keybinding." ] @@ -164,15 +185,17 @@ "view.github.active.pull.request.name": "Review Pull Request", "view.github.active.pull.request.welcome.name": "Active Pull Request", "command.pull.request.category": "GitHub Pull Requests", + "command.githubpr.remoteAgent.title": "Remote agent integration", "command.pr.create.title": "Create Pull Request", "command.pr.pick.title": "Checkout Pull Request", "command.pr.openChanges.title": "Open Changes", "command.pr.pickOnVscodeDev.title": "Checkout Pull Request on vscode.dev", "command.pr.exit.title": "Checkout Default Branch", "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.close.title": "Close Pull Request", + "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", @@ -191,7 +214,6 @@ "command.pr.checkoutByNumber.title": "Checkout Pull Request by Number", "command.review.openFile.title": "Open File", "command.review.openLocalFile.title": "Open File", - "command.review.suggestDiff.title": "Suggest Edit", "command.review.approve.title": "Approve", "command.review.comment.title": "Comment", "command.review.requestChanges.title": "Request Changes", @@ -201,8 +223,10 @@ "command.review.createSuggestionFromChange.title": "Convert to Pull Request Suggestion", "command.review.copyPrLink.title": "Copy Pull Request Link", "command.pr.refreshList.title": "Refresh Pull Requests List", + "command.pr.refreshChatSessions.title": "Refresh Chat Sessions", "command.pr.setFileListLayoutAsTree.title": "View as Tree", "command.pr.setFileListLayoutAsFlat.title": "View as List", + "command.pr.toggleHideViewedFiles.title": "Toggle Hide Viewed Files", "command.pr.refreshChanges.title": "Refresh", "command.pr.configurePRViewlet.title": "Configure...", "command.pr.deleteLocalBranch.title": "Delete Local Branch", @@ -248,7 +272,8 @@ "command.issues.category": "GitHub Issues", "command.issue.createIssueFromSelection.title": "Create Issue From Selection", "command.issue.createIssueFromClipboard.title": "Create Issue From Clipboard", - "command.pr.copyVscodeDevPrLink.title": "Copy vscode.dev Pull Request Link", + "command.pr.copyVscodeDevPrLink.title": "Copy vscode.dev Link", + "command.pr.copyPrLink.title": "Copy Link", "command.pr.createPrMenuCreate.title": "Create", "command.pr.createPrMenuDraft.title": "Create Draft", "command.pr.createPrMenuSquash.title": "Create + Auto-Squash", @@ -261,6 +286,10 @@ "command.pr.closeRelatedEditors.title": "Close All Pull Request Editors", "command.pr.toggleEditorCommentingOn.title": "Toggle Editor Commenting On", "command.pr.toggleEditorCommentingOff.title": "Toggle Editor Commenting Off", + "command.pr.checkoutFromDescription.title": "Checkout", + "command.pr.applyChangesFromDescription.title": "Apply Changes", + "command.pr.checkoutOnVscodeDevFromDescription.title": "Checkout on vscode.dev", + "command.pr.openSessionLogFromDescription.title": "View Session", "command.issue.openDescription.title": "View Issue Description", "command.issue.copyGithubDevLink.title": "Copy github.dev Link", "command.issue.copyGithubPermalink.title": "Copy GitHub Permalink", @@ -288,8 +317,10 @@ "command.issue.signinAndRefreshList.title": "Sign in and Refresh", "command.issue.goToLinkedCode.title": "Go to Linked Code", "command.issues.openIssuesWebsite.title": "Open on GitHub", + "command.issues.configureIssuesViewlet.title": "Configure...", "command.issue.chatSummarizeIssue.title": "Summarize With Copilot", "command.issue.chatSuggestFix.title": "Suggest a Fix with Copilot", + "command.issue.assignToCodingAgent.title": "Assign to Coding Agent", "command.notifications.category": "GitHub Notifications", "command.notifications.refresh.title": "Refresh", "command.notifications.pri.title": "Prioritize", @@ -299,20 +330,33 @@ "command.notifications.openOnGitHub.title": "Open on GitHub", "command.notifications.markAsRead.title": "Mark as Read", "command.notifications.markAsDone.title": "Mark as Done", - "command.notifications.markMergedPullRequestsAsRead.title": "Mark Merged Pull Requests as Read", - "command.notifications.markMergedPullRequestsAsDone.title": "Mark Merged Pull Requests as Done", + "command.notifications.markPullRequestsAsRead.title": "Mark Pull Requests as Read", + "command.notifications.markPullRequestsAsDone.title": "Mark Pull Requests as Done", + "command.notifications.configureNotificationsViewlet.title": "Configure...", "command.notification.chatSummarizeNotification.title": "Summarize With Copilot", + "command.pr.checkoutChatSessionPullRequest.title": "Checkout Pull Request", + "command.pr.closeChatSessionPullRequest.title": "Close Pull Request", + "command.pr.preferredCodingAgentGitHubRemote.title": "Set Preferred GitHub Remote", + "command.pr.resetCodingAgentPreferences.title": "Reset Coding Agent Workspace Preferences", + "command.pr.cancelCodingAgent.title": "Cancel Coding Agent", "welcome.github.login.contents": { "message": "You have not yet signed in with GitHub\n[Sign in](command:pr.signin)", - "comment" : [ + "comment": [ "Do not translate what's inside of (...). It is link syntax.", "{Locked='](command:pr.signin)'}" ] }, - "welcome.github.noGit.contents": "Git is not installed or otherwise not available. Install git or fix your git installation and then reload.", + "welcome.github.noGit.contents": "Git is not installed or otherwise not available. Install git or fix your git installation and then reload. If you have just enabled git in your settings, reload to continue.", + "welcome.github.noGitDisabled.contents": { + "message": "Git has been disabled in your settings. Enable git then reload.\n[Enable Git](command:workbench.action.openSettings?%5B%22git.enabled%22%5D)", + "comment": [ + "Do not translate what's inside of (...). It is link syntax.", + "{Locked='](command:workbench.action.openSettings?%5B%22git.enabled%22%5D)'}" + ] + }, "welcome.github.loginNoEnterprise.contents": { "message": "You have not yet signed in with GitHub\n[Sign in](command:pr.signinNoEnterprise)", - "comment" : [ + "comment": [ "Do not translate what's inside of (...). It is link syntax.", "{Locked='](command:pr.signinNoEnterprise)'}" ] @@ -327,7 +371,7 @@ "welcome.pr.github.uninitialized.contents": "Loading...", "welcome.pr.github.noFolder.contents": { "message": "You have not yet opened a folder.\n[Open Folder](command:workbench.action.files.openFolder)", - "comment" : [ + "comment": [ "Do not translate what's inside of (...). It is link syntax.", "{Locked='](command:workbench.action.files.openFolder)'}" ] @@ -351,12 +395,29 @@ "Please make sure there is no space between the right bracket and left parenthesis: ]( this is an internal syntax for links" ] }, + "welcome.github.notificationsLoading.contents": "Loading...", + "welcome.github.notifications.contents": "No notifications, your inbox is empty $(rocket)", "welcome.issues.github.uninitialized.contents": "Loading...", "welcome.issues.github.noFolder.contents": "You have not yet opened a folder.", "welcome.issues.github.noRepo.contents": "No git repositories found", "welcome.github.activePullRequest.contents": "Loading...", - "languageModelTools.github-pull-request_issue_fetch.displayName": "Get a GitHub Issue or PR", - "languageModelTools.github-pull-request_issue_summarize.displayName": "Summarize a GitHub Issue or PR", + "welcome.chat.sessions.copilot-swe-agent.login.contents": { + "message": "Sign in to get started with Copilot coding agent\n[Sign in](command:pr.signin)", + "comment": [ + "Do not translate what's inside of (...). It is link syntax.", + "{Locked='](command:pr.signin)'}" + ] + }, + "welcome.chat.sessions.copilot-swe-agent.startSession.contents": { + "message": "No Copilot coding agent sessions\n[Start a coding session](command:workbench.action.chat.openNewSessionEditor.copilot-swe-agent)", + "comment": [ + "Do not translate what's inside of (...). It is link syntax.", + "{Locked='](command:workbench.action.chat.open?%7B%22query%22%3A%22%23copilotCodingAgent%20%22%2C%22isPartialQuery%22%3Atrue%7D)'}" + ] + }, + "welcome.chat.sessions.copilot-swe-agent.noGitHub.contents": "Clone or open a GitHub repository to get started", + "languageModelTools.github-pull-request_issue_fetch.displayName": "Get a GitHub Issue or pull request", + "languageModelTools.github-pull-request_issue_summarize.displayName": "Summarize a GitHub Issue or pull request", "languageModelTools.github-pull-request_notification_fetch.displayName": "Get a GitHub Notification", "languageModelTools.github-pull-request_notification_summarize.displayName": "Summarize a GitHub Notification", "languageModelTools.github-pull-request_suggest-fix.displayName": "Suggest a Fix for a GitHub Issue", @@ -364,6 +425,9 @@ "languageModelTools.github-pull-request_doSearch.displayName": "Execute a GitHub search", "languageModelTools.github-pull-request_renderIssues.displayName": "Render issue items in a markdown table", "languageModelTools.github-pull-request_activePullRequest.displayName": "Active Pull Request", - "languageModelTools.github-pull-request_activePullRequest.description": "Get information about the active GitHub pull request. This information includes: comments, files changed, pull request title + description, pull request state, and pull request status checks/CI." - -} + "languageModelTools.github-pull-request_activePullRequest.description": "Get information about the active GitHub pull request. This information includes: comments, files changed, pull request title + description, pull request state, and pull request status checks/CI.", + "languageModelTools.github-pull-request_openPullRequest.displayName": "Open Pull Request", + "languageModelTools.github-pull-request_openPullRequest.description": "Get information about the open GitHub pull request. This information includes: comments, files changed, pull request title + description, pull request state, and pull request status checks/CI.", + "languageModelTools.github-pull-request_copilot-coding-agent.displayName": "Copilot coding agent", + "languageModelTools.github-pull-request_copilot-coding-agent.userDescription": "Completes the provided task using an asynchronous coding agent. Use when the user wants copilot continue completing a task in the background or asynchronously. Launch an autonomous GitHub Copilot agent to work on coding tasks in the background. The agent will create a new branch, implement the requested changes, and open a pull request with the completed work." +} \ No newline at end of file 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..c564bb8b4c --- /dev/null +++ b/resources/icons/codicons/add.svg @@ -0,0 +1 @@ + \ No newline at end of file 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/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..033334911e --- /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..9ceb299a04 --- /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/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/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..9380137dc8 --- /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..4dc1dd30a5 --- /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/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/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..fd1c66aa77 --- /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..104147bec2 --- /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-error.svg b/resources/icons/copilot-error.svg new file mode 100644 index 0000000000..3a4a1d74f4 --- /dev/null +++ b/resources/icons/copilot-error.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/icons/copilot-in-progress.svg b/resources/icons/copilot-in-progress.svg new file mode 100644 index 0000000000..d71b6c9c51 --- /dev/null +++ b/resources/icons/copilot-in-progress.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/resources/icons/copilot-success.svg b/resources/icons/copilot-success.svg new file mode 100644 index 0000000000..5b203adbf5 --- /dev/null +++ b/resources/icons/copilot-success.svg @@ -0,0 +1,4 @@ + + + + \ No newline at end of file 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 e01cf5b0a2..0000000000 --- a/resources/icons/copy.svg +++ /dev/null @@ -1,2 +0,0 @@ - - \ No newline at end of file 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 diff --git a/resources/icons/dark/output.svg b/resources/icons/dark/output.svg new file mode 100644 index 0000000000..7724b0e90f --- /dev/null +++ b/resources/icons/dark/output.svg @@ -0,0 +1,13 @@ + + + + + + + + \ 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/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/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/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/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/reactions/confused.png b/resources/icons/reactions/confused.png deleted file mode 100644 index 77734d4036e09973b9389efe72e71db6d7528226..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 2384 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 QL4(cgG79nW686FW06HD1S^xk5 diff --git a/resources/icons/reactions/heart.png b/resources/icons/reactions/heart.png deleted file mode 100644 index 277e3e8d489fdcc2f30a97a45a268dacced02024..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 4190 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 diff --git a/resources/icons/reactions/laugh.png b/resources/icons/reactions/laugh.png deleted file mode 100644 index a3abe00904ef8edfb49bde4ee85a29d831d56c02..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3686 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 diff --git a/resources/icons/reactions/thumbs_up.png b/resources/icons/reactions/thumbs_up.png deleted file mode 100644 index bcca0f1ba5d4b913f95ce23b44702f8892650a01..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3566 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 o2Vsrx{cRyfTW6bME8ocf1Bd0%$N&HU 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/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/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/scripts/check-commands.js b/scripts/check-commands.js new file mode 100644 index 0000000000..fd0d112f3a --- /dev/null +++ b/scripts/check-commands.js @@ -0,0 +1,73 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/* + * Verifies that every command declared in package.json under contributes.commands + * has a corresponding vscode.commands.registerCommand('' ...) call in the source. + * Exits with non-zero status if any are missing. + */ + +const fs = require('fs'); +const path = require('path'); +const glob = require('glob'); + +function readJson(filePath) { + return JSON.parse(fs.readFileSync(filePath, 'utf8')); +} + +function getDeclaredCommands(pkg) { + const contributes = pkg.contributes || {}; + const commands = contributes.commands || []; + const ids = []; + for (const cmd of commands) { + if (cmd && typeof cmd.command === 'string') { + ids.push(cmd.command); + } + } + return ids; +} + +function getRegisteredCommands(workspaceRoot) { + const files = glob.sync('src/**/*.ts', { cwd: workspaceRoot, ignore: ['**/node_modules/**', '**/dist/**', '**/out/**'] }); + const registered = new Set(); + const regex = /vscode\.commands\.(registerCommand|registerDiffInformationCommand)\s*\(\s*[']([^'\n]+)[']/g; + for (const rel of files) { + const full = path.join(workspaceRoot, rel); + let content; + try { + content = fs.readFileSync(full, 'utf8'); + } catch { + continue; + } + let match; + while ((match = regex.exec(content)) !== null) { + registered.add(match[2]); + } + } + return registered; +} + +function main() { + const workspaceRoot = path.resolve(__dirname, '..'); + const pkgPath = path.join(workspaceRoot, 'package.json'); + const pkg = readJson(pkgPath); + const declared = getDeclaredCommands(pkg); + const registered = getRegisteredCommands(workspaceRoot); + + const missing = declared.filter(id => !registered.has(id)); + + if (missing.length) { + console.error('ERROR: The following commands are declared in package.json but not registered:'); + for (const m of missing) { + console.error(' - ' + m); + } + console.error('\nAdd a corresponding vscode.commands.registerCommand("", ...) call.'); + process.exit(1); + } else { + console.log('All declared commands are registered.'); + } +} + +main(); diff --git a/scripts/ci/common-setup.yml b/scripts/ci/common-setup.yml index cad46bba53..8cb20e9f94 100644 --- a/scripts/ci/common-setup.yml +++ b/scripts/ci/common-setup.yml @@ -2,7 +2,7 @@ steps: - task: NodeTool@0 displayName: Upgrade Node inputs: - versionSpec: '16.x' + versionSpec: "20.x" - script: yarn install --frozen-lockfile --check-files displayName: Install dependencies 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/@types/vscode.proposed.chatContextProvider.d.ts b/src/@types/vscode.proposed.chatContextProvider.d.ts new file mode 100644 index 0000000000..e230959182 --- /dev/null +++ b/src/@types/vscode.proposed.chatContextProvider.d.ts @@ -0,0 +1,92 @@ +/*--------------------------------------------------------------------------------------------- + * 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/271104 @alexr00 + + export namespace chat { + + /** + * Register a chat context provider. Chat context can be provided: + * - For a resource. Make sure to pass a selector that matches the resource you want to provide context for. + * Providers registered without a selector will not be called for resource-based context. + * - Explicitly. These context items are shown as options when the user explicitly attaches context. + * + * To ensure your extension is activated when chat context is requested, make sure to include the `onChatContextProvider:` activation event in your `package.json`. + * + * @param selector Optional document selector to filter which resources the provider is called for. If omitted, the provider will only be called for explicit context requests. + * @param id Unique identifier for the provider. + * @param provider The chat context provider. + */ + export function registerChatContextProvider(selector: DocumentSelector | undefined, id: string, provider: ChatContextProvider): Disposable; + + } + + export interface ChatContextItem { + /** + * Icon for the context item. + */ + icon: ThemeIcon; + /** + * Human readable label for the context item. + */ + label: string; + /** + * An optional description of the context item, e.g. to describe the item to the language model. + */ + modelDescription?: string; + /** + * The value of the context item. Can be omitted when returned from one of the `provide` methods if the provider supports `resolveChatContext`. + */ + value?: string; + } + + export interface ChatContextProvider { + + /** + * An optional event that should be fired when the workspace chat context has changed. + */ + onDidChangeWorkspaceChatContext?: Event; + + /** + * Provide a list of chat context items to be included as workspace context for all chat sessions. + * + * @param token A cancellation token. + */ + provideWorkspaceChatContext?(token: CancellationToken): ProviderResult; + + /** + * Provide a list of chat context items that a user can choose from. These context items are shown as options when the user explicitly attaches context. + * Chat context items can be provided without a `value`, as the `value` can be resolved later using `resolveChatContext`. + * `resolveChatContext` is only called for items that do not have a `value`. + * + * @param token A cancellation token. + */ + provideChatContextExplicit?(token: CancellationToken): ProviderResult; + + /** + * Given a particular resource, provide a chat context item for it. This is used for implicit context (see the settings `chat.implicitContext.enabled` and `chat.implicitContext.suggestedContext`). + * Chat context items can be provided without a `value`, as the `value` can be resolved later using `resolveChatContext`. + * `resolveChatContext` is only called for items that do not have a `value`. + * + * Currently only called when the resource is a webview. + * + * @param options Options include the resource for which to provide context. + * @param token A cancellation token. + */ + provideChatContextForResource?(options: { resource: Uri }, token: CancellationToken): ProviderResult; + + /** + * If a chat context item is provided without a `value`, from either of the `provide` methods, this method is called to resolve the `value` for the item. + * + * @param context The context item to resolve. + * @param token A cancellation token. + */ + resolveChatContext(context: T, token: CancellationToken): ProviderResult; + } + +} diff --git a/src/@types/vscode.proposed.chatParticipantAdditions.d.ts b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts new file mode 100644 index 0000000000..aa7001a3d2 --- /dev/null +++ b/src/@types/vscode.proposed.chatParticipantAdditions.d.ts @@ -0,0 +1,684 @@ +/*--------------------------------------------------------------------------------------------- + * 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' { + + export interface ChatParticipant { + readonly onDidPerformAction: Event; + } + + /** + * Now only used for the "intent detection" API below + */ + export interface ChatCommand { + readonly name: string; + readonly description: string; + } + + export interface ChatVulnerability { + title: string; + description: string; + // id: string; // Later we will need to be able to link these across multiple content chunks. + } + + export class ChatResponseMarkdownWithVulnerabilitiesPart { + value: MarkdownString; + vulnerabilities: ChatVulnerability[]; + constructor(value: string | MarkdownString, vulnerabilities: ChatVulnerability[]); + } + + export class ChatResponseCodeblockUriPart { + isEdit?: boolean; + value: Uri; + undoStopId?: string; + constructor(value: Uri, isEdit?: boolean, undoStopId?: string); + } + + /** + * Displays a {@link Command command} as a button in the chat response. + */ + export interface ChatCommandButton { + command: Command; + } + + export interface ChatDocumentContext { + uri: Uri; + version: number; + ranges: Range[]; + } + + export class ChatResponseTextEditPart { + uri: Uri; + edits: TextEdit[]; + isDone?: boolean; + constructor(uri: Uri, done: true); + constructor(uri: Uri, edits: TextEdit | TextEdit[]); + } + + export class ChatResponseNotebookEditPart { + uri: Uri; + edits: NotebookEdit[]; + isDone?: boolean; + constructor(uri: Uri, done: true); + constructor(uri: Uri, edits: NotebookEdit | NotebookEdit[]); + } + + export class ChatResponseConfirmationPart { + title: string; + message: string | MarkdownString; + data: any; + buttons?: string[]; + constructor(title: string, message: string | MarkdownString, data: any, buttons?: string[]); + } + + export class ChatResponseCodeCitationPart { + value: Uri; + license: string; + snippet: string; + constructor(value: Uri, license: string, snippet: string); + } + + export class ChatPrepareToolInvocationPart { + toolName: string; + constructor(toolName: string); + } + + export interface ChatTerminalToolInvocationData { + commandLine: { + original: string; + userEdited?: string; + toolEdited?: string; + }; + language: string; + } + + export class ChatToolInvocationPart { + toolName: string; + toolCallId: string; + isError?: boolean; + invocationMessage?: string | MarkdownString; + originMessage?: string | MarkdownString; + pastTenseMessage?: string | MarkdownString; + isConfirmed?: boolean; + isComplete?: boolean; + toolSpecificData?: ChatTerminalToolInvocationData; + fromSubAgent?: boolean; + presentation?: 'hidden' | 'hiddenAfterComplete' | undefined; + + constructor(toolName: string, toolCallId: string, isError?: boolean); + } + + /** + * Represents a single file diff entry in a multi diff view. + */ + export interface ChatResponseDiffEntry { + /** + * The original file URI (undefined for new files). + */ + originalUri?: Uri; + + /** + * The modified file URI (undefined for deleted files). + */ + modifiedUri?: Uri; + + /** + * Optional URI to navigate to when clicking on the file. + */ + goToFileUri?: Uri; + + /** + * Added data (e.g. line numbers) to show in the UI + */ + added?: number; + + /** + * Removed data (e.g. line numbers) to show in the UI + */ + removed?: number; + } + + /** + * Represents a part of a chat response that shows multiple file diffs. + */ + export class ChatResponseMultiDiffPart { + /** + * Array of file diff entries to display. + */ + value: ChatResponseDiffEntry[]; + + /** + * The title for the multi diff editor. + */ + 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, readOnly?: boolean); + } + + 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); + } + + export class ChatResponseProgressPart2 extends ChatResponseProgressPart { + value: string; + task?: (progress: Progress) => Thenable; + constructor(value: string, task?: (progress: Progress) => Thenable); + } + + /** + * A specialized progress part for displaying thinking/reasoning steps. + */ + export class ChatResponseThinkingProgressPart { + value: string | string[]; + id?: string; + metadata?: { readonly [key: string]: any }; + task?: (progress: Progress) => Thenable; + + /** + * Creates a new thinking progress part. + * @param value An initial progress message + * @param task A task that will emit thinking parts during its execution + */ + constructor(value: string | string[], id?: string, metadata?: { readonly [key: string]: any }, task?: (progress: Progress) => Thenable); + } + + export class ChatResponseReferencePart2 { + /** + * The reference target. + */ + value: Uri | Location | { variableName: string; value?: Uri | Location } | string; + + /** + * The icon for the reference. + */ + iconPath?: Uri | ThemeIcon | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + }; + options?: { status?: { description: string; kind: ChatResponseReferencePartStatusKind } }; + + /** + * Create a new ChatResponseReferencePart. + * @param value A uri or location + * @param iconPath Icon for the reference shown in UI + */ + constructor(value: Uri | Location | { variableName: string; value?: Uri | Location } | string, iconPath?: Uri | ThemeIcon | { + /** + * The icon path for the light theme. + */ + light: Uri; + /** + * The icon path for the dark theme. + */ + dark: Uri; + }, options?: { status?: { description: string; kind: ChatResponseReferencePartStatusKind } }); + } + + export class ChatResponseMovePart { + + readonly uri: Uri; + readonly range: Range; + + constructor(uri: Uri, range: Range); + } + + export interface ChatResponseAnchorPart { + /** + * The target of this anchor. + * + * If this is a {@linkcode Uri} or {@linkcode Location}, this is rendered as a normal link. + * + * If this is a {@linkcode SymbolInformation}, this is rendered as a symbol link. + * + * TODO mjbvz: Should this be a full `SymbolInformation`? Or just the parts we need? + * TODO mjbvz: Should we allow a `SymbolInformation` without a location? For example, until `resolve` completes? + */ + value2: Uri | Location | SymbolInformation; + + /** + * Optional method which fills in the details of the anchor. + * + * THis is currently only implemented for symbol links. + */ + resolve?(token: CancellationToken): Thenable; + } + + export class ChatResponseExtensionsPart { + + readonly extensions: string[]; + + constructor(extensions: string[]); + } + + export class ChatResponsePullRequestPart { + readonly uri: Uri; + readonly linkTag: string; + readonly title: string; + readonly description: string; + readonly author: string; + constructor(uri: Uri, title: string, description: string, author: string, linkTag: string); + } + + export interface ChatResponseStream { + + /** + * Push a progress part to this stream. Short-hand for + * `push(new ChatResponseProgressPart(value))`. + * + * @param value A progress message + * @param task If provided, a task to run while the progress is displayed. When the Thenable resolves, the progress will be marked complete in the UI, and the progress message will be updated to the resolved string if one is specified. + * @returns This stream. + */ + progress(value: string, task?: (progress: Progress) => Thenable): void; + + thinkingProgress(thinkingDelta: ThinkingDelta): void; + + textEdit(target: Uri, edits: TextEdit | TextEdit[]): void; + + textEdit(target: Uri, isDone: true): void; + + notebookEdit(target: Uri, edits: NotebookEdit | NotebookEdit[]): void; + + 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; + + /** + * Show an inline message in the chat view asking the user to confirm an action. + * Multiple confirmations may be shown per response. The UI might show "Accept All" / "Reject All" actions. + * @param title The title of the confirmation entry + * @param message An extra message to display to the user + * @param data An arbitrary JSON-stringifiable object that will be included in the ChatRequest when + * the confirmation is accepted or rejected + * TODO@API should this be MarkdownString? + * TODO@API should actually be a more generic function that takes an array of buttons + */ + confirmation(title: string, message: string | MarkdownString, data: any, buttons?: string[]): void; + + /** + * Push a warning to this stream. Short-hand for + * `push(new ChatResponseWarningPart(message))`. + * + * @param message A warning message + * @returns This stream. + */ + warning(message: string | MarkdownString): void; + + reference(value: Uri | Location | { variableName: string; value?: Uri | Location }, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }): void; + + reference2(value: Uri | Location | string | { variableName: string; value?: Uri | Location }, iconPath?: Uri | ThemeIcon | { light: Uri; dark: Uri }, options?: { status?: { description: string; kind: ChatResponseReferencePartStatusKind } }): void; + + codeCitation(value: Uri, license: string, snippet: string): void; + + prepareToolInvocation(toolName: string): void; + + push(part: ExtendedChatResponsePart): void; + + clearToPreviousToolInvocation(reason: ChatResponseClearToPreviousToolInvocationReason): void; + } + + export enum ChatResponseReferencePartStatusKind { + Complete = 1, + Partial = 2, + Omitted = 3 + } + + export type ThinkingDelta = { + text?: string | string[]; + id: string; + metadata?: { readonly [key: string]: any }; + } | { + text?: string | string[]; + id?: string; + metadata: { readonly [key: string]: any }; + } | + { + text: string | string[]; + id?: string; + metadata?: { readonly [key: string]: any }; + }; + + export enum ChatResponseClearToPreviousToolInvocationReason { + NoReason = 0, + FilteredContentRetry = 1, + CopyrightContentRetry = 2, + } + + /** + * Does this piggy-back on the existing ChatRequest, or is it a different type of request entirely? + * Does it show up in history? + */ + export interface ChatRequest { + /** + * The `data` for any confirmations that were accepted + */ + acceptedConfirmationData?: any[]; + + /** + * The `data` for any confirmations that were rejected + */ + rejectedConfirmationData?: any[]; + } + + export interface ChatRequest { + + /** + * A map of all tools that should (`true`) and should not (`false`) be used in this request. + */ + readonly tools: Map; + } + + export namespace lm { + /** + * Fired when the set of tools on a chat request changes. + */ + export const onDidChangeChatRequestTools: Event; + } + + export class LanguageModelToolExtensionSource { + /** + * ID of the extension that published the tool. + */ + readonly id: string; + + /** + * Label of the extension that published the tool. + */ + readonly label: string; + + private constructor(id: string, label: string); + } + + export class LanguageModelToolMCPSource { + /** + * Editor-configured label of the MCP server that published the tool. + */ + readonly label: string; + + /** + * Server-defined name of the MCP server. + */ + readonly name: string; + + /** + * Server-defined instructions for MCP tool use. + */ + readonly instructions?: string; + + private constructor(label: string, name: string, instructions?: string); + } + + export interface LanguageModelToolInformation { + source: LanguageModelToolExtensionSource | LanguageModelToolMCPSource | undefined; + } + + // TODO@API fit this into the stream + export interface ChatUsedContext { + documents: ChatDocumentContext[]; + } + + export interface ChatParticipant { + /** + * Provide a set of variables that can only be used with this participant. + */ + participantVariableProvider?: { provider: ChatParticipantCompletionItemProvider; triggerCharacters: string[] }; + + /** + * Event that fires when a request is paused or unpaused. + * Chat requests are initially unpaused in the {@link requestHandler}. + */ + readonly onDidChangePauseState: Event; + } + + export interface ChatParticipantPauseStateEvent { + request: ChatRequest; + isPaused: boolean; + } + + export interface ChatParticipantCompletionItemProvider { + provideCompletionItems(query: string, token: CancellationToken): ProviderResult; + } + + export class ChatCompletionItem { + id: string; + label: string | CompletionItemLabel; + values: ChatVariableValue[]; + fullName?: string; + icon?: ThemeIcon; + insertText?: string; + detail?: string; + documentation?: string | MarkdownString; + command?: Command; + + constructor(id: string, label: string | CompletionItemLabel, values: ChatVariableValue[]); + } + + export type ChatExtendedRequestHandler = (request: ChatRequest, context: ChatContext, response: ChatResponseStream, token: CancellationToken) => ProviderResult; + + export interface ChatResult { + nextQuestion?: { + prompt: string; + participant?: string; + command?: string; + }; + /** + * An optional detail string that will be rendered at the end of the response in certain UI contexts. + */ + details?: string; + } + + export namespace chat { + /** + * Create a chat participant with the extended progress type + */ + export function createChatParticipant(id: string, handler: ChatExtendedRequestHandler): ChatParticipant; + } + + /* + * User action events + */ + + export enum ChatCopyKind { + // Keyboard shortcut or context menu + Action = 1, + Toolbar = 2 + } + + export interface ChatCopyAction { + // eslint-disable-next-line local/vscode-dts-string-type-literals + kind: 'copy'; + codeBlockIndex: number; + copyKind: ChatCopyKind; + copiedCharacters: number; + totalCharacters: number; + copiedText: string; + totalLines: number; + copiedLines: number; + modelId: string; + languageId?: string; + } + + export interface ChatInsertAction { + // eslint-disable-next-line local/vscode-dts-string-type-literals + kind: 'insert'; + codeBlockIndex: number; + totalCharacters: number; + totalLines: number; + languageId?: string; + modelId: string; + newFile?: boolean; + } + + export interface ChatApplyAction { + // eslint-disable-next-line local/vscode-dts-string-type-literals + kind: 'apply'; + codeBlockIndex: number; + totalCharacters: number; + totalLines: number; + languageId?: string; + modelId: string; + newFile?: boolean; + codeMapper?: string; + } + + export interface ChatTerminalAction { + // eslint-disable-next-line local/vscode-dts-string-type-literals + kind: 'runInTerminal'; + codeBlockIndex: number; + languageId?: string; + } + + export interface ChatCommandAction { + // eslint-disable-next-line local/vscode-dts-string-type-literals + kind: 'command'; + commandButton: ChatCommandButton; + } + + export interface ChatFollowupAction { + // eslint-disable-next-line local/vscode-dts-string-type-literals + kind: 'followUp'; + followup: ChatFollowup; + } + + export interface ChatBugReportAction { + // eslint-disable-next-line local/vscode-dts-string-type-literals + kind: 'bug'; + } + + export interface ChatEditorAction { + // eslint-disable-next-line local/vscode-dts-string-type-literals + kind: 'editor'; + accepted: boolean; + } + + export interface ChatEditingSessionAction { + // eslint-disable-next-line local/vscode-dts-string-type-literals + kind: 'chatEditingSessionAction'; + uri: Uri; + hasRemainingEdits: boolean; + outcome: ChatEditingSessionActionOutcome; + } + + export interface ChatEditingHunkAction { + // eslint-disable-next-line local/vscode-dts-string-type-literals + kind: 'chatEditingHunkAction'; + uri: Uri; + lineCount: number; + linesAdded: number; + linesRemoved: number; + outcome: ChatEditingSessionActionOutcome; + hasRemainingEdits: boolean; + } + + export enum ChatEditingSessionActionOutcome { + Accepted = 1, + Rejected = 2, + Saved = 3 + } + + export interface ChatUserActionEvent { + readonly result: ChatResult; + readonly action: ChatCopyAction | ChatInsertAction | ChatApplyAction | ChatTerminalAction | ChatCommandAction | ChatFollowupAction | ChatBugReportAction | ChatEditorAction | ChatEditingSessionAction | ChatEditingHunkAction; + } + + export interface ChatPromptReference { + /** + * TODO Needed for now to drive the variableName-type reference, but probably both of these should go away in the future. + */ + readonly name: string; + + /** + * The list of tools were referenced in the value of the reference + */ + readonly toolReferences?: readonly ChatLanguageModelToolReference[]; + } + + export interface ChatResultFeedback { + readonly unhelpfulReason?: string; + } + + export namespace lm { + export function fileIsIgnored(uri: Uri, token?: CancellationToken): Thenable; + } + + export interface ChatVariableValue { + /** + * The detail level of this chat variable value. If possible, variable resolvers should try to offer shorter values that will consume fewer tokens in an LLM prompt. + */ + level: ChatVariableLevel; + + /** + * The variable's value, which can be included in an LLM prompt as-is, or the chat participant may decide to read the value and do something else with it. + */ + value: string | Uri; + + /** + * A description of this value, which could be provided to the LLM as a hint. + */ + description?: string; + } + + /** + * The detail level of this chat variable value. + */ + export enum ChatVariableLevel { + Short = 1, + Medium = 2, + Full = 3 + } + + export interface LanguageModelToolInvocationOptions { + model?: LanguageModelChat; + } + + export interface ChatRequest { + readonly modeInstructions?: string; + readonly modeInstructions2?: ChatRequestModeInstructions; + } + + export interface ChatRequestModeInstructions { + readonly name: string; + readonly content: string; + readonly toolReferences?: readonly ChatLanguageModelToolReference[]; + readonly metadata?: Record; + } +} diff --git a/src/@types/vscode.proposed.chatParticipantPrivate.d.ts b/src/@types/vscode.proposed.chatParticipantPrivate.d.ts new file mode 100644 index 0000000000..c4a40a62c0 --- /dev/null +++ b/src/@types/vscode.proposed.chatParticipantPrivate.d.ts @@ -0,0 +1,383 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// version: 11 + +declare module 'vscode' { + + /** + * The location at which the chat is happening. + */ + export enum ChatLocation { + /** + * The chat panel + */ + Panel = 1, + /** + * Terminal inline chat + */ + Terminal = 2, + /** + * Notebook inline chat + */ + Notebook = 3, + /** + * Code editor inline chat + */ + Editor = 4, + } + + export class ChatRequestEditorData { + + readonly editor: TextEditor; + + //TODO@API should be the editor + document: TextDocument; + selection: Selection; + + /** @deprecated */ + wholeRange: Range; + + constructor(editor: TextEditor, document: TextDocument, selection: Selection, wholeRange: Range); + } + + export class ChatRequestNotebookData { + //TODO@API should be the editor + readonly cell: TextDocument; + + constructor(cell: TextDocument); + } + + export interface ChatRequest { + /** + * The id of the chat request. Used to identity an interaction with any of the chat surfaces. + */ + readonly id: string; + /** + * The attempt number of the request. The first request has attempt number 0. + */ + readonly attempt: number; + + /** + * The session identifier for this chat request + */ + readonly sessionId: string; + + /** + * If automatic command detection is enabled. + */ + readonly enableCommandDetection: boolean; + + /** + * If the chat participant or command was automatically assigned. + */ + readonly isParticipantDetected: boolean; + + /** + * The location at which the chat is happening. This will always be one of the supported values + * + * @deprecated + */ + readonly location: ChatLocation; + + /** + * Information that is specific to the location at which chat is happening, e.g within a document, notebook, + * or terminal. Will be `undefined` for the chat panel. + */ + readonly location2: ChatRequestEditorData | ChatRequestNotebookData | undefined; + + /** + * Events for edited files in this session collected since the last request. + */ + readonly editedFileEvents?: ChatRequestEditedFileEvent[]; + + readonly isSubagent?: boolean; + } + + export enum ChatRequestEditedFileEventKind { + Keep = 1, + Undo = 2, + UserModification = 3, + } + + export interface ChatRequestEditedFileEvent { + readonly uri: Uri; + readonly eventKind: ChatRequestEditedFileEventKind; + } + + /** + * ChatRequestTurn + private additions. Note- at runtime this is the SAME as ChatRequestTurn and instanceof is safe. + */ + export class ChatRequestTurn2 { + /** + * The prompt as entered by the user. + * + * Information about references used in this request is stored in {@link ChatRequestTurn.references}. + * + * *Note* that the {@link ChatParticipant.name name} of the participant and the {@link ChatCommand.name command} + * are not part of the prompt. + */ + readonly prompt: string; + + /** + * The id of the chat participant to which this request was directed. + */ + readonly participant: string; + + /** + * The name of the {@link ChatCommand command} that was selected for this request. + */ + readonly command?: string; + + /** + * The references that were used in this message. + */ + readonly references: ChatPromptReference[]; + + /** + * The list of tools were attached to this request. + */ + readonly toolReferences: readonly ChatLanguageModelToolReference[]; + + /** + * Events for edited files in this session collected between the previous request and this one. + */ + readonly editedFileEvents?: ChatRequestEditedFileEvent[]; + + /** + * @hidden + */ + constructor(prompt: string, command: string | undefined, references: ChatPromptReference[], participant: string, toolReferences: ChatLanguageModelToolReference[], editedFileEvents: ChatRequestEditedFileEvent[] | undefined); + } + + export class ChatResponseTurn2 { + /** + * The id of the chat response. Used to identity an interaction with any of the chat surfaces. + */ + readonly id?: string; + + /** + * The content that was received from the chat participant. Only the stream parts that represent actual content (not metadata) are represented. + */ + readonly response: ReadonlyArray; + + /** + * The result that was received from the chat participant. + */ + readonly result: ChatResult; + + /** + * The id of the chat participant that this response came from. + */ + readonly participant: string; + + /** + * The name of the command that this response came from. + */ + readonly command?: string; + + constructor(response: ReadonlyArray, result: ChatResult, participant: string); + } + + export interface ChatParticipant { + supportIssueReporting?: boolean; + } + + export enum ChatErrorLevel { + Info = 0, + Warning = 1, + Error = 2, + } + + export interface ChatErrorDetails { + /** + * If set to true, the message content is completely hidden. Only ChatErrorDetails#message will be shown. + */ + responseIsRedacted?: boolean; + + isQuotaExceeded?: boolean; + + isRateLimited?: boolean; + + level?: ChatErrorLevel; + + code?: string; + } + + export namespace chat { + export function createDynamicChatParticipant(id: string, dynamicProps: DynamicChatParticipantProps, handler: ChatExtendedRequestHandler): ChatParticipant; + } + + /** + * These don't get set on the ChatParticipant after creation, like other props, because they are typically defined in package.json and we want them at the time of creation. + */ + export interface DynamicChatParticipantProps { + name: string; + publisherName: string; + description?: string; + fullName?: string; + } + + export namespace lm { + export function registerIgnoredFileProvider(provider: LanguageModelIgnoredFileProvider): Disposable; + } + + export interface LanguageModelIgnoredFileProvider { + provideFileIgnored(uri: Uri, token: CancellationToken): ProviderResult; + } + + export interface LanguageModelToolInvocationOptions { + chatRequestId?: string; + chatSessionId?: string; + chatInteractionId?: string; + terminalCommand?: string; + /** + * Lets us add some nicer UI to toolcalls that came from a sub-agent, but in the long run, this should probably just be rendered in a similar way to thinking text + tool call groups + */ + fromSubAgent?: boolean; + } + + export interface LanguageModelToolInvocationPrepareOptions { + /** + * The input that the tool is being invoked with. + */ + input: T; + chatRequestId?: string; + chatSessionId?: string; + chatInteractionId?: string; + } + + export interface PreparedToolInvocation { + pastTenseMessage?: string | MarkdownString; + presentation?: 'hidden' | 'hiddenAfterComplete' | undefined; + } + + export class ExtendedLanguageModelToolResult extends LanguageModelToolResult { + toolResultMessage?: string | MarkdownString; + toolResultDetails?: Array; + toolMetadata?: unknown; + /** Whether there was an error calling the tool. The tool may still have partially succeeded. */ + hasError?: boolean; + } + + // #region Chat participant detection + + export interface ChatParticipantMetadata { + participant: string; + command?: string; + disambiguation: { category: string; description: string; examples: string[] }[]; + } + + export interface ChatParticipantDetectionResult { + participant: string; + command?: string; + } + + export interface ChatParticipantDetectionProvider { + provideParticipantDetection(chatRequest: ChatRequest, context: ChatContext, options: { participants?: ChatParticipantMetadata[]; location: ChatLocation }, token: CancellationToken): ProviderResult; + } + + export namespace chat { + export function registerChatParticipantDetectionProvider(participantDetectionProvider: ChatParticipantDetectionProvider): Disposable; + + export const onDidDisposeChatSession: Event; + } + + // #endregion + + // #region ChatErrorDetailsWithConfirmation + + export interface ChatErrorDetails { + confirmationButtons?: ChatErrorDetailsConfirmationButton[]; + } + + export interface ChatErrorDetailsConfirmationButton { + data: any; + label: string; + } + + // #endregion + + // #region LanguageModelProxyProvider + + /** + * Duplicated so that this proposal and languageModelProxy can be independent. + */ + export interface LanguageModelProxy extends Disposable { + readonly uri: Uri; + readonly key: string; + } + + export interface LanguageModelProxyProvider { + provideModelProxy(forExtensionId: string, token: CancellationToken): ProviderResult; + } + + export namespace lm { + export function registerLanguageModelProxyProvider(provider: LanguageModelProxyProvider): Disposable; + } + + // #endregion + + // #region CustomAgentsProvider + + /** + * Represents a custom agent resource file (e.g., .agent.md or .prompt.md) available for a repository. + */ + export interface CustomAgentResource { + /** + * The unique identifier/name of the custom agent resource. + */ + readonly name: string; + + /** + * A description of what the custom agent resource does. + */ + readonly description: string; + + /** + * The URI to the agent or prompt resource file. + */ + readonly uri: Uri; + + /** + * Indicates whether the custom agent resource is editable. Defaults to false. + */ + readonly isEditable?: boolean; + } + + /** + * Options for querying custom agents. + */ + export interface CustomAgentQueryOptions { } + + /** + * A provider that supplies custom agent resources (from .agent.md and .prompt.md files) for repositories. + */ + export interface CustomAgentsProvider { + /** + * An optional event to signal that custom agents have changed. + */ + readonly onDidChangeCustomAgents?: Event; + + /** + * Provide the list of custom agent resources available for a given repository. + * @param options Optional query parameters. + * @param token A cancellation token. + * @returns An array of custom agent resources or a promise that resolves to such. + */ + provideCustomAgents(options: CustomAgentQueryOptions, token: CancellationToken): ProviderResult; + } + + export namespace chat { + /** + * Register a provider for custom agents. + * @param provider The custom agents provider. + * @returns A disposable that unregisters the provider when disposed. + */ + export function registerCustomAgentsProvider(provider: CustomAgentsProvider): Disposable; + } + + // #endregion +} diff --git a/src/@types/vscode.proposed.chatSessionsProvider.d.ts b/src/@types/vscode.proposed.chatSessionsProvider.d.ts new file mode 100644 index 0000000000..bd4e624430 --- /dev/null +++ b/src/@types/vscode.proposed.chatSessionsProvider.d.ts @@ -0,0 +1,381 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// version: 3 + +declare module 'vscode' { + /** + * Represents the status of a chat session. + */ + export enum ChatSessionStatus { + /** + * The chat session failed to complete. + */ + Failed = 0, + + /** + * The chat session completed successfully. + */ + Completed = 1, + + /** + * The chat session is currently in progress. + */ + InProgress = 2 + } + + /** + * Provides a list of information about chat sessions. + */ + export interface ChatSessionItemProvider { + /** + * Event that the provider can fire to signal that chat sessions have changed. + */ + 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. + */ + readonly onDidCommitChatSessionItem: Event<{ original: ChatSessionItem /** untitled */; modified: ChatSessionItem /** newly created */ }>; + + /** + * DEPRECATED: Will be removed! + * Creates a new chat session. + * + * @param options Options for the new session including an optional initial prompt and history + * @param token A cancellation token + * @returns Metadata for the chat session + */ + provideNewChatSessionItem?(options: { + /** + * The chat request that initiated the session creation + */ + readonly request: ChatRequest; + + /** + * Additional metadata to use for session creation + */ + metadata?: any; + }, token: CancellationToken): ProviderResult; + + // #endregion + } + + export interface ChatSessionItem { + /** + * The resource associated with the chat session. + * + * This is uniquely identifies the chat session and is used to open the chat session. + */ + resource: Uri; + + /** + * Human readable name of the session shown in the UI + */ + label: string; + + /** + * An icon for the participant shown in UI. + */ + iconPath?: IconPath; + + /** + * An optional description that provides additional context about the chat session. + */ + description?: string | MarkdownString; + + /** + * An optional status indicating the current state of the session. + */ + status?: ChatSessionStatus; + + /** + * The tooltip text when you hover over this item. + */ + tooltip?: string | MarkdownString; + + /** + * The times at which session started and ended + */ + timing?: { + /** + * Session start timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + */ + startTime: number; + /** + * Session end timestamp in milliseconds elapsed since January 1, 1970 00:00:00 UTC. + */ + endTime?: number; + }; + + /** + * Statistics about the chat session. + */ + changes?: readonly ChatSessionChangedFile[] | { + /** + * Number of files edited during the session. + */ + files: number; + + /** + * Number of insertions made during the session. + */ + insertions: number; + + /** + * Number of deletions made during the session. + */ + deletions: number; + }; + } + + export class ChatSessionChangedFile { + /** + * URI of the file. + */ + modifiedUri: Uri; + + /** + * File opened when the user takes the 'compare' action. + */ + originalUri?: Uri; + + /** + * Number of insertions made during the session. + */ + insertions: number; + + /** + * Number of deletions made during the session. + */ + deletions: number; + + constructor(modifiedUri: Uri, insertions: number, deletions: number, originalUri?: Uri); + } + + export interface ChatSession { + /** + * The full history of the session + * + * This should not include any currently active responses + */ + // TODO: Are these the right types to use? + // TODO: link request + response to encourage correct usage? + readonly history: ReadonlyArray; + + /** + * Options configured for this session as key-value pairs. + * Keys correspond to option group IDs (e.g., 'models', 'subagents'). + * Values can be either: + * - A string (the option item ID) for backwards compatibility + * - A ChatSessionProviderOptionItem object to include metadata like locked state + * TODO: Strongly type the keys + */ + readonly options?: Record; + + /** + * Callback invoked by the editor for a currently running response. This allows the session to push items for the + * current response and stream these in as them come in. The current response will be considered complete once the + * callback resolved. + * + * If not provided, the chat session is assumed to not currently be running. + */ + readonly activeResponseCallback?: (stream: ChatResponseStream, token: CancellationToken) => Thenable; + + /** + * Handles new request for the session. + * + * If not set, then the session will be considered read-only and no requests can be made. + */ + // TODO: Should we introduce our own type for `ChatRequestHandler` since not all field apply to chat sessions? + // TODO: Revisit this to align with code. + readonly requestHandler: ChatRequestHandler | undefined; + } + + /** + * Event fired when chat session options change. + */ + export interface ChatSessionOptionChangeEvent { + /** + * Identifier of the chat session being updated. + */ + readonly resource: Uri; + /** + * Collection of option identifiers and their new values. Only the options that changed are included. + */ + readonly updates: ReadonlyArray<{ + /** + * Identifier of the option that changed (for example `model`). + */ + readonly optionId: string; + + /** + * The new value assigned to the option. When `undefined`, the option is cleared. + */ + readonly value: string | ChatSessionProviderOptionItem; + }>; + } + + /** + * Provides the content for a chat session rendered using the native chat UI. + */ + export interface ChatSessionContentProvider { + /** + * Event that the provider can fire to signal that the options for a chat session have changed. + */ + readonly onDidChangeChatSessionOptions?: Event; + + /** + * Provides the chat session content for a given uri. + * + * 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(resource: Uri, token: CancellationToken): Thenable | ChatSession; + + /** + * @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?(resource: Uri, updates: ReadonlyArray, token: CancellationToken): void; + + /** + * Called as soon as you register (call me once) + * @param token + */ + provideChatSessionProviderOptions?(token: CancellationToken): Thenable | ChatSessionProviderOptions; + } + + export interface ChatSessionOptionUpdate { + /** + * Identifier of the option that changed (for example `model`). + */ + readonly optionId: string; + + /** + * The new value assigned to the option. When `undefined`, the option is cleared. + */ + readonly value: string | undefined; + } + + export namespace chat { + /** + * Registers a new {@link ChatSessionItemProvider chat session item provider}. + * + * To use this, also make sure to also add `chatSessions` contribution in the `package.json`. + * + * @param chatSessionType The type of chat session the provider is for. + * @param provider The provider to register. + * + * @returns A disposable that unregisters the provider when disposed. + */ + export function registerChatSessionItemProvider(chatSessionType: string, provider: ChatSessionItemProvider): Disposable; + + /** + * Registers a new {@link ChatSessionContentProvider chat session content provider}. + * + * @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(scheme: string, provider: ChatSessionContentProvider, chatParticipant: ChatParticipant, capabilities?: ChatSessionCapabilities): Disposable; + } + + export interface ChatContext { + readonly chatSessionContext?: ChatSessionContext; + } + + export interface ChatSessionContext { + readonly chatSessionItem: ChatSessionItem; // Maps to URI of chat session editor (could be 'untitled-1', etc..) + readonly isUntitled: boolean; + } + + export interface ChatSessionCapabilities { + /** + * Whether sessions can be interrupted and resumed without side-effects. + */ + supportsInterruptions?: boolean; + } + + /** + * Represents a single selectable item within a provider option group. + */ + export interface ChatSessionProviderOptionItem { + /** + * Unique identifier for the option item. + */ + readonly id: string; + + /** + * Human-readable name displayed in the UI. + */ + readonly name: string; + + /** + * Optional description shown in tooltips. + */ + readonly description?: string; + + /** + * When true, this option is locked and cannot be changed by the user. + * The option will still be visible in the UI but will be disabled. + * Use this when an option is set but cannot be hot-swapped (e.g., model already initialized). + */ + readonly locked?: boolean; + + /** + * An icon for the option item shown in UI. + */ + readonly icon?: ThemeIcon; + } + + /** + * Represents a group of related provider options (e.g., models, sub-agents). + */ + export interface ChatSessionProviderOptionGroup { + /** + * Unique identifier for the option group (e.g., "models", "subagents"). + */ + 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 interface ChatSessionProviderOptions { + /** + * Provider-defined option groups (0-2 groups supported). + * Examples: models picker, sub-agents picker, etc. + */ + optionGroups?: ChatSessionProviderOptionGroup[]; + } +} 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/@types/vscode.proposed.languageModelDataPart.d.ts b/src/@types/vscode.proposed.languageModelDataPart.d.ts new file mode 100644 index 0000000000..4d491a66ca --- /dev/null +++ b/src/@types/vscode.proposed.languageModelDataPart.d.ts @@ -0,0 +1,164 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// version: 3 + +declare module 'vscode' { + + export interface LanguageModelChat { + sendRequest(messages: Array, options?: LanguageModelChatRequestOptions, token?: CancellationToken): Thenable; + countTokens(text: string | LanguageModelChatMessage | LanguageModelChatMessage2, token?: CancellationToken): Thenable; + } + + /** + * Represents a message in a chat. Can assume different roles, like user or assistant. + */ + export class LanguageModelChatMessage2 { + + /** + * Utility to create a new user message. + * + * @param content The content of the message. + * @param name The optional name of a user for the message. + */ + static User(content: string | Array, name?: string): LanguageModelChatMessage2; + + /** + * Utility to create a new assistant message. + * + * @param content The content of the message. + * @param name The optional name of a user for the message. + */ + static Assistant(content: string | Array, name?: string): LanguageModelChatMessage2; + + /** + * The role of this message. + */ + role: LanguageModelChatMessageRole; + + /** + * A string or heterogeneous array of things that a message can contain as content. Some parts may be message-type + * specific for some models. + */ + content: Array; + + /** + * The optional name of a user for this message. + */ + name: string | undefined; + + /** + * Create a new user message. + * + * @param role The role of the message. + * @param content The content of the message. + * @param name The optional name of a user for the message. + */ + constructor(role: LanguageModelChatMessageRole, content: string | Array, name?: string); + } + + /** + * A language model response part containing arbitrary data, returned from a {@link LanguageModelChatResponse}. + */ + export class LanguageModelDataPart { + /** + * Factory function to create a `LanguageModelDataPart` for an image. + * @param data Binary image data + * @param mimeType The MIME type of the image + */ + // TODO@API just use string, no enum required + static image(data: Uint8Array, mimeType: ChatImageMimeType): LanguageModelDataPart; + + static json(value: any, mime?: string): LanguageModelDataPart; + + static text(value: string, mime?: string): LanguageModelDataPart; + + /** + * The mime type which determines how the data property is interpreted. + */ + mimeType: string; + + /** + * The data of the part. + */ + data: Uint8Array; + + /** + * Construct a generic data part with the given content. + * @param value The data of the part. + */ + constructor(data: Uint8Array, mimeType: string); + } + + /** + * Enum for supported image MIME types. + */ + export enum ChatImageMimeType { + PNG = 'image/png', + JPEG = 'image/jpeg', + GIF = 'image/gif', + WEBP = 'image/webp', + BMP = 'image/bmp', + } + + /** + * The result of a tool call. This is the counterpart of a {@link LanguageModelToolCallPart tool call} and + * it can only be included in the content of a User message + */ + export class LanguageModelToolResultPart2 { + /** + * The ID of the tool call. + * + * *Note* that this should match the {@link LanguageModelToolCallPart.callId callId} of a tool call part. + */ + callId: string; + + /** + * The value of the tool result. + */ + content: Array; + + /** + * @param callId The ID of the tool call. + * @param content The content of the tool result. + */ + constructor(callId: string, content: Array); + } + + + /** + * A tool that can be invoked by a call to a {@link LanguageModelChat}. + */ + export interface LanguageModelTool { + /** + * Invoke the tool with the given input and return a result. + * + * The provided {@link LanguageModelToolInvocationOptions.input} has been validated against the declared schema. + */ + invoke(options: LanguageModelToolInvocationOptions, token: CancellationToken): ProviderResult; + } + + /** + * A result returned from a tool invocation. If using `@vscode/prompt-tsx`, this result may be rendered using a `ToolResult`. + */ + export class LanguageModelToolResult2 { + /** + * A list of tool result content parts. Includes `unknown` becauses this list may be extended with new content types in + * the future. + * @see {@link lm.invokeTool}. + */ + content: Array; + + /** + * Create a LanguageModelToolResult + * @param content A list of tool result content parts + */ + constructor(content: Array); + } + + export namespace lm { + export function invokeTool(name: string, options: LanguageModelToolInvocationOptions, token?: CancellationToken): Thenable; + } +} diff --git a/src/@types/vscode.proposed.languageModelToolResultAudience.d.ts b/src/@types/vscode.proposed.languageModelToolResultAudience.d.ts new file mode 100644 index 0000000000..07b32b02bb --- /dev/null +++ b/src/@types/vscode.proposed.languageModelToolResultAudience.d.ts @@ -0,0 +1,36 @@ +/*--------------------------------------------------------------------------------------------- + * 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' { + + export enum LanguageModelPartAudience { + /** + * The part should be shown to the language model. + */ + Assistant = 0, + /** + * The part should be shown to the user. + */ + User = 1, + /** + * The part should should be retained for internal bookkeeping within + * extensions. + */ + Extension = 2, + } + + /** + * A language model response part containing a piece of text, returned from a {@link LanguageModelChatResponse}. + */ + export class LanguageModelTextPart2 extends LanguageModelTextPart { + audience: LanguageModelPartAudience[] | undefined; + constructor(value: string, audience?: LanguageModelPartAudience[]); + } + + export class LanguageModelDataPart2 extends LanguageModelDataPart { + audience: LanguageModelPartAudience[] | undefined; + constructor(data: Uint8Array, mimeType: string, audience?: LanguageModelPartAudience[]); + } +} 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/@types/vscode.proposed.remoteCodingAgents.d.ts b/src/@types/vscode.proposed.remoteCodingAgents.d.ts new file mode 100644 index 0000000000..e818b76d8e --- /dev/null +++ b/src/@types/vscode.proposed.remoteCodingAgents.d.ts @@ -0,0 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// empty placeholder for coding agent contribution point from core + +// @joshspicer 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/api/api.d.ts b/src/api/api.d.ts index b0ca78c499..011cbd9cfa 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,10 +239,12 @@ export interface IGit { readonly onDidPublish?: Event; registerPostCommitCommandsProvider?(provider: PostCommitCommandsProvider): Disposable; + getRepositoryWorkspace?(uri: Uri): Promise; + clone?(uri: Uri, options?: CloneOptions): Promise; } 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 { @@ -257,6 +259,18 @@ export interface ReviewerCommentsProvider { provideReviewerComments(context: { repositoryRoot: string, commitMessages: string[], patches: { patch: string, fileUri: string, previousFileUri?: string }[] }, token: CancellationToken): Promise; } +export interface RepositoryDescription { + owner: string; + repositoryName: string; + defaultBranch: string; + pullRequest?: { + title: string; + url: string; + number: number; + id: number; + }; +} + export interface API { /** * Register a [git provider](#IGit) @@ -280,4 +294,13 @@ export interface API { * Register a PR reviewer comments provider. */ registerReviewerCommentsProvider(title: string, provider: ReviewerCommentsProvider): Disposable; + + /** + * Get the repository description for a given URI, where the URI is a subpath of one of the workspace folders. + * This includes the owner, repository name, default branch, + * and pull request information (if applicable). + * + * @returns A promise that resolves to a `RepositoryDescription` object or `undefined` if no repository is found. + */ + getRepositoryDescription(uri: vscode.Uri): Promise; } diff --git a/src/api/api1.ts b/src/api/api1.ts index 8d3fe1edbf..4eabe95f06 100644 --- a/src/api/api1.ts +++ b/src/api/api1.ts @@ -4,11 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { APIState, PublishEvent } from '../@types/git'; +import { API, IGit, PostCommitCommandsProvider, Repository, ReviewerCommentsProvider, TitleAndDescriptionProvider } from './api'; +import { APIState, CloneOptions, PublishEvent } from '../@types/git'; import { Disposable } from '../common/lifecycle'; import Logger from '../common/logger'; import { TernarySearchTree } from '../common/utils'; -import { API, IGit, PostCommitCommandsProvider, Repository, ReviewerCommentsProvider, TitleAndDescriptionProvider } from './api'; +import { RepositoriesManager } from '../github/repositoriesManager'; export const enum RefType { Head, @@ -78,6 +79,30 @@ export class GitApiImpl extends Disposable implements API, IGit { private static _handlePool: number = 0; private _providers = new Map(); + public constructor( + private readonly repositoriesManager: RepositoriesManager) { + super(); + } + + async getRepositoryWorkspace(uri: vscode.Uri): Promise { + for (const [, provider] of this._providers) { + if (provider.getRepositoryWorkspace) { + return provider.getRepositoryWorkspace(uri); + } + } + 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[] = []; @@ -220,4 +245,26 @@ export class GitApiImpl extends Disposable implements API, IGit { getReviewerCommentsProvider(): { title: string, provider: ReviewerCommentsProvider } | undefined { return this._reviewerCommentsProviders.size > 0 ? this._reviewerCommentsProviders.values().next().value : undefined; } + + async getRepositoryDescription(uri: vscode.Uri) { + const folderManagerForRepo = this.repositoriesManager.getManagerForFile(uri); + + if (folderManagerForRepo && folderManagerForRepo.gitHubRepositories.length > 0) { + const repositoryMetadata = await folderManagerForRepo.gitHubRepositories[0].getMetadata(); + const pullRequest = folderManagerForRepo.activePullRequest; + if (repositoryMetadata) { + return { + owner: repositoryMetadata.owner.login, + repositoryName: repositoryMetadata.name, + defaultBranch: repositoryMetadata.default_branch, + pullRequest: pullRequest ? { + title: pullRequest.title, + url: pullRequest.html_url, + number: pullRequest.number, + id: pullRequest.id + } : undefined + }; + } + } + } } diff --git a/src/authentication/githubServer.ts b/src/authentication/githubServer.ts index 1484f9535b..993fc9cda2 100644 --- a/src/authentication/githubServer.ts +++ b/src/authentication/githubServer.ts @@ -5,16 +5,16 @@ import fetch from 'cross-fetch'; import * as vscode from 'vscode'; +import { HostHelper } from './configuration'; import { GitHubServerType } from '../common/authentication'; import Logger from '../common/logger'; import { agent } from '../env/node/net'; import { getEnterpriseUri } from '../github/utils'; -import { HostHelper } from './configuration'; export class GitHubManager { private static readonly _githubDotComServers = new Set().add('github.com').add('ssh.github.com'); private static readonly _gheServers = new Set().add('ghe.com'); - private static readonly _neverGitHubServers = new Set().add('bitbucket.org').add('gitlab.com'); + private static readonly _neverGitHubServers = new Set().add('bitbucket.org').add('gitlab.com').add('codeberg.org'); private _knownServers: Map = new Map([...Array.from(GitHubManager._githubDotComServers.keys()).map(key => [key, GitHubServerType.GitHubDotCom]), ...Array.from(GitHubManager._gheServers.keys()).map(key => [key, GitHubServerType.Enterprise])] as [string, GitHubServerType][]); public static isGithubDotCom(host: string): boolean { diff --git a/src/commands.ts b/src/commands.ts index 62da4dfa29..ddb34e8736 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -9,27 +9,33 @@ import * as vscode from 'vscode'; import { Repository } from './api/api'; import { GitErrorCodes } from './api/api1'; import { CommentReply, findActiveHandler, resolveCommentHandler } from './commentHandlerResolver'; -import { IComment } from './common/comment'; import { commands } from './common/executeCommands'; import Logger from './common/logger'; -import { FILE_LIST_LAYOUT, PR_SETTINGS_NAMESPACE } from './common/settingKeys'; +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, SessionIdForPr } from './github/copilotRemoteAgent'; import { FolderRepositoryManager } from './github/folderRepositoryManager'; import { GitHubRepository } from './github/githubRepository'; -import { Issue, PullRequest } from './github/interface'; +import { Issue } from './github/interface'; import { IssueModel } from './github/issueModel'; import { IssueOverviewPanel } from './github/issueOverview'; -import { NotificationProvider } from './github/notifications'; import { GHPRComment, GHPRCommentThread, TemporaryComment } from './github/prComment'; import { PullRequestModel } from './github/pullRequestModel'; import { PullRequestOverviewPanel } from './github/pullRequestOverview'; +import { chooseItem } from './github/quickPicks'; import { RepositoriesManager } from './github/repositoriesManager'; -import { getIssuesUrl, getPullsUrl, isInCodespaces, vscodeDevPrLink } from './github/utils'; +import { getIssuesUrl, getPullsUrl, isInCodespaces, ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput, vscodeDevPrLink } from './github/utils'; +import { OverviewContext } from './github/views'; import { isNotificationTreeItem, NotificationTreeItem } from './notifications/notificationItem'; -import { PullRequestsTreeDataProvider } from './view/prsTreeDataProvider'; +import { NotificationsManager } from './notifications/notificationsManager'; +import { PrsTreeModel } from './view/prsTreeModel'; import { ReviewCommentController } from './view/reviewCommentController'; import { ReviewManager } from './view/reviewManager'; import { ReviewsManager } from './view/reviewsManager'; @@ -45,9 +51,6 @@ import { import { PRNode } from './view/treeNodes/pullRequestNode'; import { RepositoryChangesNode } from './view/treeNodes/repositoryChangesNode'; -const _onDidUpdatePR = new vscode.EventEmitter(); -export const onDidUpdatePR: vscode.Event = _onDidUpdatePR.event; - function ensurePR(folderRepoManager: FolderRepositoryManager, pr?: PRNode): PullRequestModel; function ensurePR>(folderRepoManager: FolderRepositoryManager, pr?: TIssueModel): TIssueModel; function ensurePR>(folderRepoManager: FolderRepositoryManager, pr?: PRNode | TIssueModel): TIssueModel { @@ -71,7 +74,6 @@ export async function openDescription( folderManager: FolderRepositoryManager, revealNode: boolean, preserveFocus: boolean = true, - notificationProvider?: NotificationProvider ) { const issue = ensurePR(folderManager, issueModel); if (revealNode) { @@ -80,10 +82,6 @@ export async function openDescription( // Create and show a new webview if (issue instanceof PullRequestModel) { await PullRequestOverviewPanel.createOrShow(telemetry, folderManager.context.extensionUri, folderManager, issue, undefined, preserveFocus); - /* __GDPR__ - "pr.openDescription" : {} - */ - telemetry.sendTelemetryEvent('pr.openDescription'); } else { await IssueOverviewPanel.createOrShow(telemetry, folderManager.context.extensionUri, folderManager, issue); /* __GDPR__ @@ -91,32 +89,6 @@ export async function openDescription( */ telemetry.sendTelemetryEvent('issue.openDescription'); } - - if (notificationProvider?.hasNotification(issue)) { - notificationProvider.markPrNotificationsAsRead(issue); - } - - -} - -async function chooseItem( - activePullRequests: T[], - propertyGetter: (itemValue: T) => string, - options?: vscode.QuickPickOptions, -): Promise { - if (activePullRequests.length === 1) { - return activePullRequests[0]; - } - interface Item extends vscode.QuickPickItem { - itemValue: T; - } - const items: Item[] = activePullRequests.map(currentItem => { - return { - label: propertyGetter(currentItem), - itemValue: currentItem, - }; - }); - return (await vscode.window.showQuickPick(items, options))?.itemValue; } export async function openPullRequestOnGitHub(e: PRNode | RepositoryChangesNode | IssueModel | NotificationTreeItem, telemetry: ITelemetry) { @@ -146,12 +118,24 @@ export async function closeAllPrAndReviewEditors() { } } +function isChatSessionWithPR(value: any): value is ChatSessionWithPR { + const asChatSessionWithPR = value as Partial; + 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, reviewsManager: ReviewsManager, telemetry: ITelemetry, - tree: PullRequestsTreeDataProvider, + copilotRemoteAgentManager: CopilotRemoteAgentManager, + notificationManager: NotificationsManager, + prsTreeModel: PrsTreeModel ) { const logId = 'RegisterCommands'; context.subscriptions.push( @@ -166,7 +150,7 @@ export function registerCommands( if (activePullRequests.length >= 1) { const result = await chooseItem( activePullRequests, - itemValue => itemValue.html_url, + itemValue => ({ label: itemValue.html_url }), ); if (result) { openPullRequestOnGitHub(result, telemetry); @@ -203,7 +187,7 @@ export function registerCommands( ? ( await chooseItem( activePullRequestsWithFolderManager, - itemValue => itemValue.activePr.html_url, + itemValue => ({ label: itemValue.activePr.html_url }), ) ) : activePullRequestsWithFolderManager[0]; @@ -225,82 +209,6 @@ export function registerCommands( ), ); - context.subscriptions.push( - vscode.commands.registerCommand('review.suggestDiff', async e => { - const hasShownMessageKey = 'githubPullRequest.suggestDiffMessage'; - const hasShownMessage = context.globalState.get(hasShownMessageKey, false); - if (!hasShownMessage) { - await context.globalState.update(hasShownMessageKey, true); - const documentation = vscode.l10n.t('Open documentation'); - const result = await vscode.window.showInformationMessage(vscode.l10n.t('You can now make suggestions from review comments, just like on GitHub.com. See the documentation for more details.'), - { modal: true }, documentation); - if (result === documentation) { - return vscode.env.openExternal(vscode.Uri.parse('https://github.com/microsoft/vscode-pull-request-github/blob/main/documentation/suggestAChange.md')); - } - } - try { - const folderManager = await chooseItem( - reposManager.folderManagers, - itemValue => pathLib.basename(itemValue.repository.rootUri.fsPath), - ); - if (!folderManager || !folderManager.activePullRequest) { - return; - } - - const { indexChanges, workingTreeChanges } = folderManager.repository.state; - - if (!indexChanges.length) { - if (workingTreeChanges.length) { - const yes = vscode.l10n.t('Yes'); - const stageAll = await vscode.window.showWarningMessage( - vscode.l10n.t('There are no staged changes to suggest.\n\nWould you like to automatically stage all your of changes and suggest them?'), - { modal: true }, - yes, - ); - if (stageAll === yes) { - await vscode.commands.executeCommand('git.stageAll'); - } else { - return; - } - } else { - vscode.window.showInformationMessage(vscode.l10n.t('There are no changes to suggest.')); - return; - } - } - - const diff = await folderManager.repository.diff(true); - - let suggestEditMessage = vscode.l10n.t('Suggested edit:\n'); - if (e && e.inputBox && e.inputBox.value) { - suggestEditMessage = `${e.inputBox.value}\n`; - e.inputBox.value = ''; - } - - const suggestEditText = `${suggestEditMessage}\`\`\`diff\n${diff}\n\`\`\``; - await folderManager.activePullRequest.createIssueComment(suggestEditText); - - // Reset HEAD and then apply reverse diff - await vscode.commands.executeCommand('git.unstageAll'); - - const tempFilePath = pathLib.join( - folderManager.repository.rootUri.fsPath, - '.git', - `${folderManager.activePullRequest.number}.diff`, - ); - const encoder = new TextEncoder(); - const tempUri = vscode.Uri.file(tempFilePath); - - await vscode.workspace.fs.writeFile(tempUri, encoder.encode(diff)); - await folderManager.repository.apply(tempFilePath, true); - await vscode.workspace.fs.delete(tempUri); - } catch (err) { - const moreError = `${err}${err.stderr ? `\n${err.stderr}` : ''}`; - Logger.error(`Applying patch failed: ${moreError}`, logId); - vscode.window.showErrorMessage(vscode.l10n.t('Applying patch failed: {0}', formatError(err))); - } - }), - ); - context.subscriptions.push( vscode.commands.registerCommand('pr.openFileOnGitHub', async (e: GitFileChangeNode | RemoteFileChangeNode) => { if (e instanceof RemoteFileChangeNode) { @@ -443,8 +351,6 @@ export function registerCommands( "pr.deleteLocalPullRequest.success" : {} */ telemetry.sendTelemetryEvent('pr.deleteLocalPullRequest.success'); - // fire and forget - vscode.commands.executeCommand('pr.refreshList'); } }), ); @@ -460,7 +366,7 @@ export function registerCommands( } return chooseItem( reviewsManager.reviewManagers, - itemValue => pathLib.basename(itemValue.repository.rootUri.fsPath), + itemValue => ({ label: pathLib.basename(itemValue.repository.rootUri.fsPath) }), { placeHolder: vscode.l10n.t('Choose a repository to create a pull request in'), ignoreFocusOut: true }, ); } @@ -530,43 +436,224 @@ export function registerCommands( pullRequestModel = pr; } + // Get the folder manager to access the repository + const folderManager = reposManager.getManagerForIssueModel(pullRequestModel); + if (!folderManager) { + return vscode.window.showErrorMessage(vscode.l10n.t('Unable to find repository for this pull request.')); + } + const fromDescriptionPage = pr instanceof PullRequestModel; - /* __GDPR__ - "pr.checkout" : { - "fromDescriptionPage" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + return reviewsManager.switchToPr(folderManager, pullRequestModel, repository, fromDescriptionPage); + + })); + + const resolvePr = async (context: OverviewContext | undefined): Promise<{ folderManager: FolderRepositoryManager, pr: PullRequestModel } | undefined> => { + if (!context) { + return undefined; + } + + const folderManager = reposManager.getManagerForRepository(context.owner, context.repo) ?? reposManager.folderManagers[0]; + if (!folderManager) { + return undefined; + } + + const pr = await folderManager.resolvePullRequest(context.owner, context.repo, context.number, true); + if (!pr) { + return undefined; + } + + return { folderManager, pr }; + }; + + const applyPullRequestChanges = async (task: vscode.Progress<{ message?: string; increment?: number; }>, folderManager: FolderRepositoryManager, pullRequest: PullRequestModel): Promise => { + let patch: string | undefined; + try { + patch = await pullRequest.getPatch(); + + if (!patch.trim()) { + vscode.window.showErrorMessage(vscode.l10n.t('No patch data available for pull request #{0}', pullRequest.number.toString())); + return; } - */ - telemetry.sendTelemetryEvent('pr.checkout', { fromDescription: fromDescriptionPage.toString() }); - return vscode.window.withProgress( + const tempFilePath = pathLib.join( + folderManager.repository.rootUri.fsPath, + '.git', + `pr-${pullRequest.number}.patch`, + ); + const encoder = new TextEncoder(); + const tempUri = vscode.Uri.file(tempFilePath); + + 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); + Logger.error(`Failed to apply PR changes: ${errorMessage}`, 'Commands'); + + const copyGitApply = vscode.l10n.t('Copy git apply'); + const result = await vscode.window.showErrorMessage( + vscode.l10n.t('Failed to apply changes from pull request: {0}', errorMessage), + copyGitApply + ); + + if (result === copyGitApply) { + if (patch) { + const gitApplyCommand = `git apply --3way <<'EOF'\n${patch}\nEOF`; + await vscode.env.clipboard.writeText(gitApplyCommand); + vscode.window.showInformationMessage(vscode.l10n.t('Git apply command copied to clipboard')); + } else { + vscode.window.showErrorMessage(vscode.l10n.t('Unable to copy git apply command - patch content is not available')); + } + } + } + }; + + function contextHasPath(ctx: OverviewContext | { path: string } | undefined): ctx is { path: string } { + const contextAsPath: Partial<{ path: string }> = (ctx as { path: string }); + 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.')); + } + + if (contextHasPath(ctx)) { + const { path } = ctx; + 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())); + } + + 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 reviewsManager.switchToPr(resolved.folderManager, resolved.pr, resolved.folderManager.repository, true); + + })); + + context.subscriptions.push(vscode.commands.registerCommand('pr.applyChangesFromDescription', async (ctx: OverviewContext | { path: string } | undefined) => { + if (!ctx) { + return vscode.window.showErrorMessage(vscode.l10n.t('No pull request context provided for applying changes.')); + } + + if (contextHasPath(ctx)) { + const { path } = ctx; + const prNumber = prNumberFromUriPath(path); + if (!prNumber) { + return vscode.window.showErrorMessage(vscode.l10n.t('Unable to parse pull request number.')); + } + + await vscode.window.withProgress( { - location: vscode.ProgressLocation.SourceControl, - title: vscode.l10n.t('Switching to Pull Request #{0}', pullRequestModel.number), + location: vscode.ProgressLocation.Notification, + title: vscode.l10n.t('Applying changes from pull request #{0}', prNumber.toString()), + cancellable: false }, - async () => { - await ReviewManager.getReviewManagerForRepository( - reviewsManager.reviewManagers, - pullRequestModel.githubRepository, - repository - )?.switch(pullRequestModel); - }, - ); - }), - ); + async (task) => { + task.report({ increment: 30 }); + + 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; + } + + 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( - vscode.commands.registerCommand('pr.openChanges', async (pr: PRNode | RepositoryChangesNode | PullRequestModel) => { + 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); return vscode.window.showErrorMessage(vscode.l10n.t('No pull request was selected to checkout, please try again.')); } - let pullRequestModel: PullRequestModel; + let pullRequestModel: PullRequestModel | undefined; if (pr instanceof PRNode || pr instanceof RepositoryChangesNode) { pullRequestModel = pr.pullRequestModel; - } else { + } else if (pr instanceof PullRequestModel) { 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 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; + } + + if (!pullRequestModel) { + return vscode.window.showErrorMessage(vscode.l10n.t('No pull request found to open changes.')); } const folderReposManager = reposManager.getManagerForIssueModel(pullRequestModel); @@ -627,6 +714,25 @@ export function registerCommands( }), ); + context.subscriptions.push(vscode.commands.registerCommand('pr.checkoutOnVscodeDevFromDescription', async (context: OverviewContext | undefined) => { + if (!context) { + return vscode.window.showErrorMessage(vscode.l10n.t('No pull request context provided for checkout.')); + } + const resolved = await resolvePr(context); + if (!resolved) { + return vscode.window.showErrorMessage(vscode.l10n.t('Unable to resolve pull request for checkout.')); + } + 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; @@ -637,7 +743,7 @@ export function registerCommands( pullRequestModel = await chooseItem(reposManager.folderManagers .map(folderManager => folderManager.activePullRequest!) .filter(activePR => !!activePR), - itemValue => `${itemValue.number}: ${itemValue.title}`, + itemValue => ({ label: `${itemValue.number}: ${itemValue.title}` }), { placeHolder: vscode.l10n.t('Choose the pull request to exit') }); } else { pullRequestModel = pr; @@ -705,7 +811,7 @@ export function registerCommands( let newPR; if (value === yes) { try { - newPR = await folderManager.mergePullRequest(pullRequest); + newPR = await pullRequest.merge(folderManager.repository); return newPR; } catch (e) { vscode.window.showErrorMessage(`Unable to merge pull request. ${formatError(e)}`); @@ -717,99 +823,23 @@ export function registerCommands( ); context.subscriptions.push( - vscode.commands.registerCommand('pr.readyForReview', async (pr?: PRNode) => { - const folderManager = reposManager.getManagerForIssueModel(pr?.pullRequestModel); - if (!folderManager) { - return; + vscode.commands.registerCommand('pr.dismissNotification', node => { + if (node instanceof PRNode) { + notificationManager.markPrNotificationsAsRead(node.pullRequestModel); + prsTreeModel.clearCopilotNotification(node.pullRequestModel.remote.owner, node.pullRequestModel.remote.repositoryName, node.pullRequestModel.number); } - 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; - vscode.commands.executeCommand('pr.refreshList'); - 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.close', async (pr?: PRNode | PullRequestModel, message?: string) => { - let pullRequestModel: PullRequestModel | undefined; - if (pr) { - pullRequestModel = pr instanceof PullRequestModel ? pr : pr.pullRequestModel; - } else { - const activePullRequests: PullRequestModel[] = reposManager.folderManagers - .map(folderManager => folderManager.activePullRequest!) - .filter(activePR => !!activePR); - pullRequestModel = await chooseItem( - activePullRequests, - itemValue => `${itemValue.number}: ${itemValue.title}`, - { placeHolder: vscode.l10n.t('Pull request to close') }, - ); - } - if (!pullRequestModel) { - return; - } - const pullRequest: PullRequestModel = pullRequestModel; - const yes = vscode.l10n.t('Yes'); - return vscode.window - .showWarningMessage( - vscode.l10n.t('Are you sure you want to close this pull request on GitHub? This will close the pull request without merging.'), - { modal: true }, - yes, - vscode.l10n.t('No'), - ) - .then(async value => { - if (value === yes) { - try { - let newComment: IComment | undefined = undefined; - if (message) { - newComment = await pullRequest.createIssueComment(message); - } - - const newPR = await pullRequest.close(); - vscode.commands.executeCommand('pr.refreshList'); - _onDidUpdatePR.fire(newPR); - return newComment; - } catch (e) { - vscode.window.showErrorMessage(`Unable to close pull request. ${formatError(e)}`); - _onDidUpdatePR.fire(); - } - } - - _onDidUpdatePR.fire(); - }); - }), - ); - - context.subscriptions.push( - vscode.commands.registerCommand('pr.dismissNotification', node => { - if (node instanceof PRNode) { - tree.notificationProvider.markPrNotificationsAsRead(node.pullRequestModel).then( - () => tree.refresh(node) - ); - + vscode.commands.registerCommand('pr.markAllCopilotNotificationsAsRead', node => { + if (node instanceof CategoryTreeNode && node.isCopilot && node.repo) { + prsTreeModel.clearAllCopilotNotifications(node.repo.owner, node.repo.repositoryName); } }), ); - async function openDescriptionCommand(argument: RepositoryChangesNode | PRNode | IssueModel | undefined) { + async function openDescriptionCommand(argument: RepositoryChangesNode | PRNode | IssueModel | ChatSessionWithPR | undefined) { let issueModel: IssueModel | undefined; if (!argument) { const activePullRequests: PullRequestModel[] = reposManager.folderManagers @@ -818,7 +848,7 @@ export function registerCommands( if (activePullRequests.length >= 1) { issueModel = await chooseItem( activePullRequests, - itemValue => itemValue.title, + itemValue => ({ label: itemValue.title }), ); } } else { @@ -826,6 +856,8 @@ export function registerCommands( issueModel = argument.pullRequestModel; } else if (argument instanceof PRNode) { issueModel = argument.pullRequestModel; + } else if (isChatSessionWithPR(argument)) { + issueModel = argument.pullRequest; } else { issueModel = argument; } @@ -852,9 +884,83 @@ export function registerCommands( const revealDescription = !(argument instanceof PRNode); - await openDescription(telemetry, issueModel, descriptionNode, folderManager, revealDescription, !(argument instanceof RepositoryChangesNode), tree.notificationProvider); + await openDescription(telemetry, issueModel, descriptionNode, folderManager, revealDescription, !(argument instanceof RepositoryChangesNode)); + } + + 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; + } + + const folderManager = reposManager.getManagerForRepository(pr.githubRepository.remote.owner, pr.githubRepository.remote.repositoryName); + if (!folderManager) { + Logger.warn(`No folder manager found for pull request ${pr.number}`, logId); + return vscode.window.showErrorMessage(vscode.l10n.t('Unable to find repository for pull request #{0}', pr.number.toString())); + } + + return reviewsManager.switchToPr(folderManager, pr, folderManager.repository, false); + } + + 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; + } + await pr.close(); + copilotRemoteAgentManager.refreshChatSessions(); + } + + 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; + } + + copilotRemoteAgentManager.cancelMostRecentChatSession(pr); + // TODO: show a progress icon until the cancelation is finished } + context.subscriptions.push( + vscode.commands.registerCommand( + 'pr.checkoutChatSessionPullRequest', + checkoutChatSessionPullRequest + ) + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + 'pr.closeChatSessionPullRequest', + closeChatSessionPullRequest + ) + ); + + context.subscriptions.push( + vscode.commands.registerCommand( + 'pr.cancelCodingAgent', + cancelCodingAgent + ) + ); + context.subscriptions.push( vscode.commands.registerCommand( 'pr.openDescription', @@ -1151,7 +1257,10 @@ ${contents} "pr.editQuery" : {} */ telemetry.sendTelemetryEvent('pr.editQuery'); - return query.editQuery(); + if (query.label === undefined) { + return; + } + return editQuery(PR_SETTINGS_NAMESPACE, query.label); }), ); @@ -1218,12 +1327,20 @@ ${contents} }), ); - context.subscriptions.push(vscode.commands.registerCommand('review.createSuggestionsFromChanges', async (value: ({ resourceStates: { resourceUri }[] }) | ({ resourceUri: vscode.Uri }), ...additionalSelected: ({ resourceUri: vscode.Uri })[]) => { + interface SCMResourceStates { + resourceStates: { resourceUri: vscode.Uri }[]; + } + interface SCMResourceUri { + resourceUri: vscode.Uri; + } + context.subscriptions.push(vscode.commands.registerCommand('review.createSuggestionsFromChanges', async (value: SCMResourceStates | SCMResourceUri, ...additionalSelected: SCMResourceUri[]) => { let resources: vscode.Uri[]; - if ('resourceStates' in value) { - resources = value.resourceStates.map(resource => resource.resourceUri); + const asResourceStates = value as Partial; + if (asResourceStates.resourceStates) { + resources = asResourceStates.resourceStates.map(resource => resource.resourceUri); } else { - resources = [value.resourceUri]; + const asResourceUri = value as SCMResourceUri; + resources = [asResourceUri.resourceUri]; if (additionalSelected) { resources.push(...additionalSelected.map(resource => resource.resourceUri)); } @@ -1276,9 +1393,10 @@ ${contents} vscode.commands.registerCommand('pr.refreshChanges', _ => { reviewsManager.reviewManagers.forEach(reviewManager => { vscode.window.withProgress({ location: { viewId: 'prStatus:github' } }, async () => { - await reviewManager.updateComments(); - PullRequestOverviewPanel.refresh(); - reviewManager.changesInPrDataProvider.refresh(); + await Promise.all([ + reviewManager.repository.pull(false), + reviewManager.updateComments() + ]); }); }); }), @@ -1296,6 +1414,14 @@ ${contents} }), ); + context.subscriptions.push( + vscode.commands.registerCommand('pr.toggleHideViewedFiles', _ => { + const config = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE); + const currentValue = config.get(HIDE_VIEWED_FILES, false); + config.update(HIDE_VIEWED_FILES, !currentValue, vscode.ConfigurationTarget.Global); + }), + ); + context.subscriptions.push( vscode.commands.registerCommand('pr.refreshPullRequest', (prNode: PRNode) => { const folderManager = reposManager.getManagerForIssueModel(prNode.pullRequestModel); @@ -1304,7 +1430,6 @@ ${contents} } PullRequestOverviewPanel.refresh(); - tree.refresh(prNode); }), ); @@ -1314,9 +1439,7 @@ ${contents} if (prQuery) { for (const githubRepos of (manager?.gitHubRepositories ?? [])) { const prNumber = Number(prQuery.prNumber); - if (githubRepos.pullRequestModels.has(prNumber)) { - return githubRepos.pullRequestModels.get(prNumber); - } + return githubRepos.getExistingPullRequestModel(prNumber); } } } else { @@ -1409,20 +1532,61 @@ ${contents} })); context.subscriptions.push( - vscode.commands.registerCommand('pr.copyVscodeDevPrLink', async () => { - const activePullRequests: PullRequestModel[] = reposManager.folderManagers - .map(folderManager => folderManager.activePullRequest!) - .filter(activePR => !!activePR); - const pr = await chooseItem( - activePullRequests, - itemValue => `${itemValue.number}: ${itemValue.title}`, - { placeHolder: vscode.l10n.t('Pull request to create a link for') }, - ); + vscode.commands.registerCommand('pr.copyVscodeDevPrLink', async (params: OverviewContext | undefined) => { + let pr: PullRequestModel | undefined; + if (params) { + pr = await reposManager.getManagerForRepository(params.owner, params.repo)?.resolvePullRequest(params.owner, params.repo, params.number, true); + } else { + const activePullRequests: PullRequestModel[] = reposManager.folderManagers + .map(folderManager => folderManager.activePullRequest!) + .filter(activePR => !!activePR); + pr = await chooseItem( + activePullRequests, + itemValue => ({ label: `${itemValue.number}: ${itemValue.title}` }), + { placeHolder: vscode.l10n.t('Pull request to create a link for') }, + ); + } if (pr) { return vscode.env.clipboard.writeText(vscodeDevPrLink(pr)); } })); + context.subscriptions.push( + vscode.commands.registerCommand('pr.copyPrLink', async (params: OverviewContext | undefined) => { + let pr: PullRequestModel | undefined; + if (params) { + pr = await reposManager.getManagerForRepository(params.owner, params.repo)?.resolvePullRequest(params.owner, params.repo, params.number, true); + } + if (pr) { + return vscode.env.clipboard.writeText(pr.html_url); + } + })); + + function validateAndParseInput(input: string, expectedOwner: string, expectedRepo: string): { isValid: true; prNumber: number; errorMessage?: string } | { isValid: false; prNumber?: number; errorMessage: string } { + const prNumberMatcher = /^#?(\d*)$/; + const numberMatches = input.match(prNumberMatcher); + if (numberMatches && (numberMatches.length === 2) && !Number.isNaN(Number(numberMatches[1]))) { + const num = Number(numberMatches[1]); + if (num > 0) { + return { isValid: true, prNumber: num }; + } + } + + const urlMatches = input.match(ISSUE_OR_URL_EXPRESSION); + const parsed = parseIssueExpressionOutput(urlMatches); + if (parsed && parsed.issueNumber && parsed.issueNumber > 0) { + // Check if the repository owner and name match + if (parsed.owner && parsed.name) { + if (parsed.owner !== expectedOwner || parsed.name !== expectedRepo) { + return { isValid: false, errorMessage: vscode.l10n.t('Repository in URL does not match the selected repository') }; + } + } + return { isValid: true, prNumber: parsed.issueNumber }; + } + + return { isValid: false, errorMessage: vscode.l10n.t('Value must be a pull request number or GitHub URL') }; + } + context.subscriptions.push( vscode.commands.registerCommand('pr.checkoutByNumber', async () => { @@ -1434,27 +1598,30 @@ ${contents} } const githubRepo = await chooseItem<{ manager: FolderRepositoryManager, repo: GitHubRepository }>( githubRepositories, - itemValue => `${itemValue.repo.remote.owner}/${itemValue.repo.remote.repositoryName}`, + itemValue => ({ label: `${itemValue.repo.remote.owner}/${itemValue.repo.remote.repositoryName}` }), { placeHolder: vscode.l10n.t('Which GitHub repository do you want to checkout the pull request from?') } ); if (!githubRepo) { return; } - const prNumberMatcher = /^#?(\d*)$/; const prNumber = await vscode.window.showInputBox({ - ignoreFocusOut: true, prompt: vscode.l10n.t('Enter the pull request number'), + ignoreFocusOut: true, prompt: vscode.l10n.t('Enter the pull request number or URL'), validateInput: (input: string) => { - const matches = input.match(prNumberMatcher); - if (!matches || (matches.length !== 2) || Number.isNaN(Number(matches[1]))) { - return vscode.l10n.t('Value must be a number'); - } - return undefined; + const result = validateAndParseInput(input, githubRepo.repo.remote.owner, githubRepo.repo.remote.repositoryName); + return result.isValid ? undefined : result.errorMessage; } }); if ((prNumber === undefined) || prNumber === '#') { return; } - const prModel = await githubRepo.manager.fetchById(githubRepo.repo, Number(prNumber.match(prNumberMatcher)![1])); + + // Extract PR number from input (either direct number or URL) + const parseResult = validateAndParseInput(prNumber, githubRepo.repo.remote.owner, githubRepo.repo.remote.repositoryName); + if (!parseResult.isValid) { + return vscode.window.showErrorMessage(parseResult.errorMessage || vscode.l10n.t('Invalid pull request number or URL')); + } + + const prModel = await githubRepo.manager.fetchById(githubRepo.repo, parseResult.prNumber); if (prModel) { return ReviewManager.getReviewManagerForFolderManager(reviewsManager.reviewManagers, githubRepo.manager)?.switch(prModel); } @@ -1467,7 +1634,7 @@ ${contents} }); return chooseItem( githubRepositories, - itemValue => `${itemValue.remote.owner}/${itemValue.remote.repositoryName}`, + itemValue => ({ label: `${itemValue.remote.owner}/${itemValue.remote.repositoryName}` }), { placeHolder: vscode.l10n.t('Which GitHub repository do you want to open?') } ); } @@ -1500,6 +1667,9 @@ ${contents} handler.applySuggestion(comment); } })); + context.subscriptions.push( + vscode.commands.registerCommand('githubpr.remoteAgent', async (args: ICopilotRemoteAgentCommandArgs) => await copilotRemoteAgentManager.commandImpl(args)) + ); context.subscriptions.push( vscode.commands.registerCommand('pr.applySuggestionWithCopilot', async (comment: GHPRComment) => { /* __GDPR__ @@ -1668,8 +1838,8 @@ ${contents} for (const folderManager of reposManager.folderManagers) { for (const githubRepository of folderManager.gitHubRepositories) { for (const pullRequest of githubRepository.pullRequestModels) { - if (pullRequest[1].isResolved() && pullRequest[1].reviewThreadsCacheReady) { - pullRequest[1].initializeReviewThreadCache(); + if (pullRequest.isResolved() && pullRequest.reviewThreadsCacheReady) { + pullRequest.initializeReviewThreadCache(); } } } @@ -1688,7 +1858,7 @@ ${contents} const pr = await chooseItem( activePullRequests, - itemValue => `${itemValue.number}: ${itemValue.title}`, + itemValue => ({ label: `${itemValue.number}: ${itemValue.title}` }), { placeHolder: vscode.l10n.t('Pull request to create a link for') }, ); if (pr) { @@ -1696,4 +1866,22 @@ ${contents} } }) ); + + context.subscriptions.push( + vscode.commands.registerCommand('pr.refreshChatSessions', async () => { + copilotRemoteAgentManager.refreshChatSessions(); + }) + ); + + context.subscriptions.push( + vscode.commands.registerCommand('pr.preferredCodingAgentGitHubRemote', async () => { + await copilotRemoteAgentManager.promptAndUpdatePreferredGitHubRemote(); + }) + ); + + context.subscriptions.push( + vscode.commands.registerCommand('pr.resetCodingAgentPreferences', async () => { + await copilotRemoteAgentManager.resetCodingAgentPreferences(); + }) + ); } diff --git a/src/common/comment.ts b/src/common/comment.ts index 079fd8791d..889549c996 100644 --- a/src/common/comment.ts +++ b/src/common/comment.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { IAccount } from '../github/interface'; import { COPILOT_LOGINS } from './copilot'; import { DiffHunk } from './diffHunk'; +import { IAccount, Reaction } from '../github/interface'; export enum DiffSide { LEFT = 'LEFT', @@ -19,14 +19,6 @@ export enum ViewedState { UNVIEWED = 'UNVIEWED' } -export interface Reaction { - label: string; - count: number; - icon?: vscode.Uri; - viewerHasReacted: boolean; - reactors: readonly string[]; -} - export enum SubjectType { LINE = 'LINE', FILE = 'FILE' @@ -77,8 +69,9 @@ export interface IComment { const COPILOT_AUTHOR = { name: 'Copilot', // TODO: The copilot reviewer is a Bot, but per the graphQL schema, Bots don't have a name, just a login. We have it hardcoded here for now. - postComment: vscode.l10n.t('Copilot is powered by AI, so mistakes are possible. Review output carefully before use.') + postComment: vscode.l10n.t('Copilot is powered by AI, so mistakes are possible. Review output carefully before use.'), + url: 'https://github.com/apps/copilot-swe-agent' }; -export const COPILOT_ACCOUNTS: { [key: string]: { postComment: string, name: string } } = +export const COPILOT_ACCOUNTS: { [key: string]: { postComment: string, name: string, url: string } } = Object.fromEntries(COPILOT_LOGINS.map(login => [login, COPILOT_AUTHOR])); \ No newline at end of file diff --git a/src/common/config.ts b/src/common/config.ts new file mode 100644 index 0000000000..16d74de481 --- /dev/null +++ b/src/common/config.ts @@ -0,0 +1,33 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import vscode from 'vscode'; +import { CODING_AGENT, CODING_AGENT_AUTO_COMMIT_AND_PUSH, CODING_AGENT_ENABLED, CODING_AGENT_PROMPT_FOR_CONFIRMATION } from './settingKeys'; + +/** + * Handles configuration settings for the Copilot Remote Agent + */ +export namespace CopilotRemoteAgentConfig { + function config() { + return vscode.workspace.getConfiguration(CODING_AGENT); + } + + export function getEnabled(): boolean { + return config().get(CODING_AGENT_ENABLED, false); + } + + export function getPromptForConfirmation(): boolean { + return config().get(CODING_AGENT_PROMPT_FOR_CONFIRMATION, true); + + } + + export function getAutoCommitAndPushEnabled(): boolean { + return config().get(CODING_AGENT_AUTO_COMMIT_AND_PUSH, false); + } + + export async function disablePromptForConfirmation(): Promise { + await config().update(CODING_AGENT_PROMPT_FOR_CONFIRMATION, false, vscode.ConfigurationTarget.Global); + } +} diff --git a/src/common/copilot.ts b/src/common/copilot.ts index d63ecf1047..8bfed7d293 100644 --- a/src/common/copilot.ts +++ b/src/common/copilot.ts @@ -3,8 +3,49 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { EventType, TimelineEvent } from './timelineEvent'; + +export const COPILOT_SWE_AGENT = 'copilot-swe-agent'; +export const COPILOT_CLOUD_AGENT = 'copilot-cloud-agent'; +export const COPILOT_REVIEWER = 'copilot-pull-request-reviewer'; +export const COPILOT_REVIEWER_ID = 'BOT_kgDOCnlnWA'; + export const COPILOT_LOGINS = [ - 'copilot-pull-request-reviewer', - 'copilot-swe-agent', + COPILOT_REVIEWER, + COPILOT_SWE_AGENT, 'Copilot' -]; \ No newline at end of file +]; + +export enum CopilotPRStatus { + None = 0, + Started = 1, + Completed = 2, + Failed = 3, +} + +export function copilotEventToStatus(event: TimelineEvent | undefined): CopilotPRStatus { + if (!event) { + return CopilotPRStatus.None; + } + + switch (event.event) { + case EventType.CopilotStarted: + return CopilotPRStatus.Started; + case EventType.CopilotFinished: + return CopilotPRStatus.Completed; + case EventType.CopilotFinishedError: + return CopilotPRStatus.Failed; + default: + return CopilotPRStatus.None; + } +} + +export function mostRecentCopilotEvent(events: TimelineEvent[]): TimelineEvent | undefined { + for (let i = events.length - 1; i >= 0; i--) { + const status = copilotEventToStatus(events[i]); + if (status !== CopilotPRStatus.None) { + return events[i]; + } + } + return undefined; +} \ No newline at end of file diff --git a/src/common/diffHunk.ts b/src/common/diffHunk.ts index e28fd0d1a4..d7b9429fa1 100644 --- a/src/common/diffHunk.ts +++ b/src/common/diffHunk.ts @@ -7,8 +7,8 @@ * Inspired by and includes code from GitHub/VisualStudio project, obtained from https://github.com/github/VisualStudio/blob/master/src/GitHub.Exports/Models/DiffLine.cs */ -import { IRawFileChange } from '../github/interface'; import { GitChangeType, InMemFileChange, SlimFileChange } from './file'; +import { IRawFileChange } from '../github/interface'; export enum DiffChangeType { Context, @@ -18,12 +18,8 @@ export enum DiffChangeType { } export class DiffLine { - public get raw(): string { - return this._raw; - } - public get text(): string { - return this._raw.substr(1); + return this.raw.substr(1); } constructor( @@ -31,7 +27,7 @@ export class DiffLine { public oldLineNumber: number /* 1 based */, public newLineNumber: number /* 1 based */, public positionInHunk: number, - private _raw: string, + public readonly raw: string, public endwithLineBreak: boolean = true, ) { } } diff --git a/src/common/emoji.ts b/src/common/emoji.ts index 1a0bea6853..f94b7a612f 100644 --- a/src/common/emoji.ts +++ b/src/common/emoji.ts @@ -13,17 +13,18 @@ const emojiRegex = /:([-+_a-z0-9]+):/g; let emojiMap: Record | undefined; let emojiMapPromise: Promise | undefined; -export async function ensureEmojis(context: ExtensionContext) { +export async function ensureEmojis(context: ExtensionContext): Promise> { if (emojiMap === undefined) { if (emojiMapPromise === undefined) { emojiMapPromise = loadEmojiMap(context); } await emojiMapPromise; } + return emojiMap!; } async function loadEmojiMap(context: ExtensionContext) { - const uri = (Uri as any).joinPath(context.extensionUri, 'resources', 'emojis.json'); + const uri = Uri.joinPath(context.extensionUri, 'resources', 'emojis.json'); emojiMap = JSON.parse(new TextDecoder('utf8').decode(await workspace.fs.readFile(uri))); } diff --git a/src/common/executeCommands.ts b/src/common/executeCommands.ts index 99bbbafae2..dee2f69f39 100644 --- a/src/common/executeCommands.ts +++ b/src/common/executeCommands.ts @@ -19,10 +19,13 @@ export namespace contexts { export const PULL_REQUEST_DESCRIPTION_VISIBLE = 'github:pullRequestDescriptionVisible'; // Boolean indicating if the pull request description is visible export const ACTIVE_COMMENT_HAS_SUGGESTION = 'github:activeCommentHasSuggestion'; // Boolean indicating if the active comment has a suggestion export const CREATING = 'pr:creating'; + export const NOTIFICATION_COUNT = 'github:notificationCount'; // Number of notifications in the notifications view } export namespace commands { export const OPEN_CHAT = 'workbench.action.chat.open'; + export const NEW_CHAT = 'workbench.action.chat.newChat'; + export const CHAT_SETUP_ACTION_ID = 'workbench.action.chat.triggerSetup'; export const QUICK_CHAT_OPEN = 'workbench.action.quickchat.toggle'; @@ -37,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/common/gitUtils.ts b/src/common/gitUtils.ts new file mode 100644 index 0000000000..dc4539e1ec --- /dev/null +++ b/src/common/gitUtils.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Repository } from '../api/api'; +import { GitApiImpl } from '../api/api1'; + +/** + * Determines if a repository is a submodule by checking if its path + * appears in any other repository's submodules list. + */ +export function isSubmodule(repo: Repository, git: GitApiImpl): boolean { + const repoPath = repo.rootUri.fsPath; + + // Check all other repositories to see if this repo is listed as a submodule + for (const otherRepo of git.repositories) { + if (otherRepo.rootUri.toString() === repo.rootUri.toString()) { + continue; // Skip self + } + + // Check if this repo's path appears in the other repo's submodules + for (const submodule of otherRepo.state.submodules) { + // The submodule path is relative to the parent repo, so we need to resolve it + const submodulePath = vscode.Uri.joinPath(otherRepo.rootUri, submodule.path).fsPath; + if (submodulePath === repoPath) { + return true; + } + } + } + + return false; +} \ No newline at end of file diff --git a/src/common/githubRef.ts b/src/common/githubRef.ts index 867a6f0838..a3803173ab 100644 --- a/src/common/githubRef.ts +++ b/src/common/githubRef.ts @@ -3,9 +3,9 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Remote, Repository } from '../api/api'; import { Protocol } from './protocol'; import { parseRemote } from './remote'; +import { Remote, Repository } from '../api/api'; export class GitHubRef { public repositoryCloneUrl: Protocol; diff --git a/src/common/logger.ts b/src/common/logger.ts index 5fc3fca312..024cd584d2 100644 --- a/src/common/logger.ts +++ b/src/common/logger.ts @@ -7,6 +7,10 @@ import { Disposable } from './lifecycle'; export const PR_TREE = 'PullRequestTree'; +interface Stringish { + toString: () => string; +} + class Log extends Disposable { private readonly _outputChannel: vscode.LogOutputChannel; private readonly _activePerfMarkers: Map = new Map(); @@ -28,36 +32,40 @@ class Log extends Disposable { this._activePerfMarkers.delete(marker); } - private logString(message: any, component?: string): string { + private logString(message: string | Error | Stringish | Object, component?: string): string { + let logMessage: string; if (typeof message !== 'string') { + const asString = message as Partial; if (message instanceof Error) { - message = message.message; - } else if ('toString' in message) { - message = message.toString(); + logMessage = message.message; + } else if (asString.toString) { + logMessage = asString.toString(); } else { - message = JSON.stringify(message); + logMessage = JSON.stringify(message); } + } else { + logMessage = message; } - return component ? `[${component}] ${message}` : message; + return component ? `[${component}] ${logMessage}` : logMessage; } - public trace(message: any, component: string) { + public trace(message: string | Error | Stringish | Object, component: string) { this._outputChannel.trace(this.logString(message, component)); } - public debug(message: any, component: string) { + public debug(message: string | Error | Stringish | Object, component: string) { this._outputChannel.debug(this.logString(message, component)); } - public appendLine(message: any, component: string) { + public appendLine(message: string | Error | Stringish | Object, component: string) { this._outputChannel.info(this.logString(message, component)); } - public warn(message: any, component?: string) { + public warn(message: string | Error | Stringish | Object, component?: string) { this._outputChannel.warn(this.logString(message, component)); } - public error(message: any, component: string) { + public error(message: string | Error | Stringish | Object, component: string) { this._outputChannel.error(this.logString(message, component)); } } diff --git a/src/common/protocol.ts b/src/common/protocol.ts index 9af6576d51..cac2471f14 100644 --- a/src/common/protocol.ts +++ b/src/common/protocol.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { resolve } from '../env/node/ssh'; import Logger from './logger'; +import { resolve } from '../env/node/ssh'; export enum ProtocolType { @@ -32,6 +32,7 @@ export class Protocol { public readonly url: vscode.Uri; constructor(uriString: string) { if (this.parseSshProtocol(uriString)) { + this.url = vscode.Uri.from({ scheme: 'ssh', authority: this.host, path: `/${this.nameWithOwner}` }); return; } diff --git a/src/common/remote.ts b/src/common/remote.ts index 96671830ab..ae3e34ae23 100644 --- a/src/common/remote.ts +++ b/src/common/remote.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { Repository } from '../api/api'; -import { getEnterpriseUri, isEnterprise } from '../github/utils'; import { AuthProvider, GitHubServerType } from './authentication'; import { Protocol } from './protocol'; +import { Repository } from '../api/api'; +import { getEnterpriseUri, isEnterprise } from '../github/utils'; export class Remote { public get host(): string { diff --git a/src/common/resources.ts b/src/common/resources.ts deleted file mode 100644 index cff11666af..0000000000 --- a/src/common/resources.ts +++ /dev/null @@ -1,26 +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 path from 'path'; -import * as vscode from 'vscode'; - -export class Resource { - static icons: any; - - static initialize(context: vscode.ExtensionContext) { - Resource.icons = { - reactions: { - THUMBS_UP: context.asAbsolutePath(path.join('resources', 'icons', 'reactions', 'thumbs_up.png')), - THUMBS_DOWN: context.asAbsolutePath(path.join('resources', 'icons', 'reactions', 'thumbs_down.png')), - CONFUSED: context.asAbsolutePath(path.join('resources', 'icons', 'reactions', 'confused.png')), - EYES: context.asAbsolutePath(path.join('resources', 'icons', 'reactions', 'eyes.png')), - HEART: context.asAbsolutePath(path.join('resources', 'icons', 'reactions', 'heart.png')), - HOORAY: context.asAbsolutePath(path.join('resources', 'icons', 'reactions', 'hooray.png')), - LAUGH: context.asAbsolutePath(path.join('resources', 'icons', 'reactions', 'laugh.png')), - ROCKET: context.asAbsolutePath(path.join('resources', 'icons', 'reactions', 'rocket.png')), - }, - }; - } -} diff --git a/src/common/settingKeys.ts b/src/common/settingKeys.ts index 712312812d..b3d358bf31 100644 --- a/src/common/settingKeys.ts +++ b/src/common/settingKeys.ts @@ -6,17 +6,22 @@ export const PR_SETTINGS_NAMESPACE = 'githubPullRequests'; export const TERMINAL_LINK_HANDLER = 'terminalLinksHandler'; export const BRANCH_PUBLISH = 'createOnPublishBranch'; +export const BRANCH_LIST_TIMEOUT = 'branchListTimeout'; export const USE_REVIEW_MODE = 'useReviewMode'; export const FILE_LIST_LAYOUT = 'fileListLayout'; +export const HIDE_VIEWED_FILES = 'hideViewedFiles'; export const ASSIGN_TO = 'assignCreated'; export const PUSH_BRANCH = 'pushBranch'; export const IGNORE_PR_BRANCHES = 'ignoredPullRequestBranches'; +export const IGNORE_SUBMODULES = 'ignoreSubmodules'; export const NEVER_IGNORE_DEFAULT_BRANCH = 'neverIgnoreDefaultBranch'; export const OVERRIDE_DEFAULT_BRANCH = 'overrideDefaultBranch'; export const PULL_BRANCH = 'pullBranch'; export const PULL_REQUEST_DESCRIPTION = 'pullRequestDescription'; export const NOTIFICATION_SETTING = 'notifications'; +export type NotificationVariants = 'off' | 'pullRequests'; export const POST_CREATE = 'postCreate'; +export const POST_DONE = 'postDone'; export const QUERIES = 'queries'; export const PULL_REQUEST_LABELS = 'labelCreated'; export const FOCUSED_MODE = 'focusedMode'; @@ -28,6 +33,7 @@ export const DEFAULT_MERGE_METHOD = 'defaultMergeMethod'; export const DEFAULT_DELETION_METHOD = 'defaultDeletionMethod'; export const SELECT_LOCAL_BRANCH = 'selectLocalBranch'; export const SELECT_REMOTE = 'selectRemote'; +export const DELETE_BRANCH_AFTER_MERGE = 'deleteBranchAfterMerge'; export const REMOTES = 'remotes'; export const PULL_PR_BRANCH_BEFORE_CHECKOUT = 'pullPullRequestBranchBeforeCheckout'; export type PullPRBranchVariants = 'never' | 'pull' | 'pullAndMergeBase' | 'pullAndUpdateBase' | true | false; @@ -52,11 +58,12 @@ export const DEFAULT = 'default'; export const IGNORE_MILESTONES = 'ignoreMilestones'; export const ALLOW_FETCH = 'allowFetch'; export const ALWAYS_PROMPT_FOR_NEW_ISSUE_REPO = 'alwaysPromptForNewIssueRepo'; +export const ISSUE_AVATAR_DISPLAY = 'issueAvatarDisplay'; export const EXPERIMENTAL_CHAT = 'experimental.chat'; export const EXPERIMENTAL_USE_QUICK_CHAT = 'experimental.useQuickChat'; -export const EXPERIMENTAL_NOTIFICATIONS = 'experimental.notificationsView'; export const EXPERIMENTAL_NOTIFICATIONS_PAGE_SIZE = 'experimental.notificationsViewPageSize'; export const EXPERIMENTAL_NOTIFICATIONS_SCORE = 'experimental.notificationsScore'; +export const WEBVIEW_REFRESH_INTERVAL = 'webviewRefreshInterval'; // git export const GIT = 'git'; @@ -64,6 +71,8 @@ export const PULL_BEFORE_CHECKOUT = 'pullBeforeCheckout'; export const OPEN_DIFF_ON_CLICK = 'openDiffOnClick'; export const SHOW_INLINE_OPEN_FILE_ACTION = 'showInlineOpenFileAction'; export const AUTO_STASH = 'autoStash'; +export const BRANCH_WHITESPACE_CHAR = 'branchWhitespaceChar'; +export const BRANCH_RANDOM_NAME_DICTIONARY = 'branchRandomName.dictionary'; // GitHub Enterprise export const GITHUB_ENTERPRISE = 'github-enterprise'; @@ -80,3 +89,15 @@ export const OPEN_VIEW = 'openView'; // Explorer export const EXPLORER = 'explorer'; export const AUTO_REVEAL = 'autoReveal'; + +// Workbench +export const WORKBENCH = 'workbench'; +export const COLOR_THEME = 'colorTheme'; + +// Coding Agent + +export const CODING_AGENT = `${PR_SETTINGS_NAMESPACE}.codingAgent`; +export const CODING_AGENT_ENABLED = 'enabled'; +export const CODING_AGENT_AUTO_COMMIT_AND_PUSH = 'autoCommitAndPush'; +export const CODING_AGENT_PROMPT_FOR_CONFIRMATION = 'promptForConfirmation'; +export const SHOW_CODE_LENS = 'codeLens'; \ No newline at end of file diff --git a/src/common/settingsUtils.ts b/src/common/settingsUtils.ts index 41e18d4379..2c3b3d7001 100644 --- a/src/common/settingsUtils.ts +++ b/src/common/settingsUtils.ts @@ -5,7 +5,8 @@ 'use strict'; import * as vscode from 'vscode'; -import { PR_SETTINGS_NAMESPACE, USE_REVIEW_MODE } from './settingKeys'; +import { commands } from './executeCommands'; +import { PR_SETTINGS_NAMESPACE, QUERIES, USE_REVIEW_MODE } from './settingKeys'; export function getReviewMode(): { merged: boolean, closed: boolean } { const desktopDefaults = { merged: false, closed: false }; @@ -18,4 +19,155 @@ export function getReviewMode(): { merged: boolean, closed: boolean } { return { merged: true, closed: true }; } return desktopDefaults; +} + +export function initBasedOnSettingChange(namespace: string, key: string, isEnabled: () => boolean, initializer: () => void, disposables: vscode.Disposable[]): void { + const eventDisposable = vscode.workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration(`${namespace}.${key}`)) { + if (isEnabled()) { + initializer(); + eventDisposable.dispose(); + } + } + }); + disposables.push(eventDisposable); +} + +interface QueryInspect { + key: string; + defaultValue?: { label: string; query: string }[]; + globalValue?: { label: string; query: string }[]; + workspaceValue?: { label: string; query: string }[]; + workspaceFolderValue?: { label: string; query: string }[]; + defaultLanguageValue?: { label: string; query: string }[]; + globalLanguageValue?: { label: string; query: string }[]; + workspaceLanguageValue?: { label: string; query: string }[]; + workspaceFolderLanguageValue?: { label: string; query: string }[]; + languageIds?: string[] +} + +export function editQuery(namespace: string, queryName: string) { + const config = vscode.workspace.getConfiguration(namespace); + const inspect = config.inspect<{ label: string; query: string }[]>(QUERIES); + const queryValue = config.get<{ label: string; query: string }[]>(QUERIES)?.find((query) => query.label === queryName)?.query; + + const inputBox = vscode.window.createQuickPick(); + inputBox.title = vscode.l10n.t('Edit Query "{0}"', queryName ?? ''); + inputBox.value = queryValue ?? ''; + inputBox.items = [ + { iconPath: new vscode.ThemeIcon('pencil'), label: vscode.l10n.t('Save edits'), alwaysShow: true }, + { iconPath: new vscode.ThemeIcon('add'), label: vscode.l10n.t('Add new query'), alwaysShow: true }, + { iconPath: new vscode.ThemeIcon('settings'), label: vscode.l10n.t('Edit in settings.json'), alwaysShow: true }, + { iconPath: new vscode.ThemeIcon('sparkle'), label: vscode.l10n.t('Edit with AI'), alwaysShow: true } + ]; + inputBox.activeItems = []; + inputBox.selectedItems = []; + inputBox.onDidAccept(async () => { + inputBox.busy = true; + if (inputBox.selectedItems[0] === inputBox.items[0]) { + const newQuery = inputBox.value; + if (newQuery !== queryValue) { + let newValue: { label: string; query: string }[]; + let target: vscode.ConfigurationTarget; + if (inspect?.workspaceFolderValue) { + target = vscode.ConfigurationTarget.WorkspaceFolder; + newValue = inspect.workspaceFolderValue; + } else if (inspect?.workspaceValue) { + target = vscode.ConfigurationTarget.Workspace; + newValue = inspect.workspaceValue; + } else { + target = vscode.ConfigurationTarget.Global; + newValue = config.get<{ label: string; query: string }[]>(QUERIES) ?? []; + } + newValue.find((query) => query.label === queryName)!.query = newQuery; + await config.update(QUERIES, newValue, target); + } + inputBox.dispose(); + } else if (inputBox.selectedItems[0] === inputBox.items[1]) { + addNewQuery(config, inspect, inputBox.value); + inputBox.dispose(); + } else if (inputBox.selectedItems[0] === inputBox.items[2]) { + openSettingsAtQuery(config, inspect, queryName); + inputBox.dispose(); + } else if (inputBox.selectedItems[0] === inputBox.items[3]) { + inputBox.ignoreFocusOut = true; + await openCopilotForQuery(inputBox.value); + inputBox.busy = false; + } + }); + inputBox.onDidHide(() => inputBox.dispose()); + inputBox.show(); +} + +function addNewQuery(config: vscode.WorkspaceConfiguration, inspect: QueryInspect | undefined, startingValue: string) { + const inputBox = vscode.window.createInputBox(); + inputBox.title = vscode.l10n.t('Enter the title of the new query'); + inputBox.placeholder = vscode.l10n.t('Title'); + inputBox.step = 1; + inputBox.totalSteps = 2; + inputBox.show(); + let title: string | undefined; + inputBox.onDidAccept(async () => { + inputBox.validationMessage = ''; + if (inputBox.step === 1) { + if (!inputBox.value) { + inputBox.validationMessage = vscode.l10n.t('Title is required'); + return; + } + + title = inputBox.value; + inputBox.value = startingValue; + inputBox.title = vscode.l10n.t('Enter the GitHub search query'); + inputBox.step++; + } else { + if (!inputBox.value) { + inputBox.validationMessage = vscode.l10n.t('Query is required'); + return; + } + inputBox.busy = true; + if (inputBox.value && title) { + if (inspect?.workspaceValue) { + inspect.workspaceValue.push({ label: title, query: inputBox.value }); + await config.update(QUERIES, inspect.workspaceValue, vscode.ConfigurationTarget.Workspace); + } else { + const value = config.get<{ label: string; query: string }[]>(QUERIES); + value?.push({ label: title, query: inputBox.value }); + await config.update(QUERIES, value, vscode.ConfigurationTarget.Global); + } + } + inputBox.dispose(); + } + }); + inputBox.onDidHide(() => inputBox.dispose()); +} + +async function openSettingsAtQuery(config: vscode.WorkspaceConfiguration, inspect: QueryInspect | undefined, queryName: string) { + let command: string; + if (inspect?.workspaceValue) { + command = 'workbench.action.openWorkspaceSettingsFile'; + } else { + const value = config.get<{ label: string; query: string }[]>(QUERIES); + if (inspect?.defaultValue && JSON.stringify(inspect?.defaultValue) === JSON.stringify(value)) { + await config.update(QUERIES, inspect.defaultValue, vscode.ConfigurationTarget.Global); + } + command = 'workbench.action.openSettingsJson'; + } + await vscode.commands.executeCommand(command); + const editor = vscode.window.activeTextEditor; + if (editor) { + const text = editor.document.getText(); + const search = text.search(queryName); + if (search >= 0) { + const position = editor.document.positionAt(search); + editor.revealRange(new vscode.Range(position, position)); + editor.selection = new vscode.Selection(position, position); + } + } +} + +async function openCopilotForQuery(currentQuery: string) { + const chatMessage = vscode.l10n.t('I want to edit this GitHub search query: \n```\n{0}\n```\nOutput only one, minimally modified query in a codeblock.\nModify it so that it ', currentQuery); + + // Open chat with the query pre-populated + await vscode.commands.executeCommand(commands.NEW_CHAT, { inputValue: chatMessage, isPartialQuery: true, agentMode: false }); } \ No newline at end of file diff --git a/src/common/timelineEvent.ts b/src/common/timelineEvent.ts index 8329ee850e..8ef0c07ee0 100644 --- a/src/common/timelineEvent.ts +++ b/src/common/timelineEvent.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IAccount, IActor } from '../github/interface'; import { IComment } from './comment'; +import { IAccount, IActor, Reaction } from '../github/interface'; export enum EventType { Committed, @@ -16,9 +16,15 @@ export enum EventType { Labeled, Milestoned, Assigned, + Unassigned, HeadRefDeleted, Merged, CrossReferenced, + Closed, + Reopened, + CopilotStarted, + CopilotFinished, + CopilotFinishedError, Other, } @@ -39,6 +45,7 @@ export interface CommentEvent { canEdit?: boolean; canDelete?: boolean; createdAt: string; + reactions?: Reaction[]; } export interface ReviewResolveInfo { @@ -62,6 +69,7 @@ export interface ReviewEvent { user: IAccount; authorAssociation: string; state?: ReviewStateValue; + reactions?: Reaction[]; } export interface CommitEvent { @@ -72,7 +80,8 @@ export interface CommitEvent { htmlUrl: string; message: string; bodyHTML?: string; - authoredDate: Date; + committedDate: Date; + status?: 'EXPECTED' | 'ERROR' | 'FAILURE' | 'PENDING' | 'SUCCESS'; } export interface NewCommitsSinceReviewEvent { @@ -100,6 +109,14 @@ export interface AssignEvent { createdAt: string; } +export interface UnassignEvent { + id: number; + event: EventType.Unassigned; + unassignees: IAccount[]; + actor: IActor; + createdAt: string; +} + export interface HeadRefDeleteEvent { id: string; event: EventType.HeadRefDeleted; @@ -116,9 +133,63 @@ export interface CrossReferencedEvent { source: { number: number; url: string; + extensionUrl: string; title: string; + isIssue: boolean; + owner: string; + repo: string; }; willCloseTarget: boolean; } -export type TimelineEvent = CommitEvent | ReviewEvent | CommentEvent | NewCommitsSinceReviewEvent | MergedEvent | AssignEvent | HeadRefDeleteEvent | CrossReferencedEvent; +export interface ClosedEvent { + id: string + event: EventType.Closed; + actor: IActor; + createdAt: string; +} + +export interface ReopenedEvent { + id: string; + event: EventType.Reopened; + actor: IActor; + createdAt: string; +} + +export interface SessionPullInfo { + id: number; + host: string; + owner: string; + repo: string; + pullNumber: number; +} + +export interface SessionLinkInfo extends SessionPullInfo { + sessionIndex: number; + openToTheSide?: boolean; +} + +export interface CopilotStartedEvent { + id: string; + event: EventType.CopilotStarted; + createdAt: string; + onBehalfOf: IAccount; + sessionLink: SessionLinkInfo; +} + +export interface CopilotFinishedEvent { + id: string; + event: EventType.CopilotFinished; + createdAt: string; + onBehalfOf: IAccount; +} + +export interface CopilotFinishedErrorEvent { + id: string; + event: EventType.CopilotFinishedError; + createdAt: string; + onBehalfOf: IAccount; + sessionLink: SessionLinkInfo; +} + +export type TimelineEvent = CommitEvent | ReviewEvent | CommentEvent | NewCommitsSinceReviewEvent | MergedEvent | AssignEvent | UnassignEvent | HeadRefDeleteEvent | CrossReferencedEvent | ClosedEvent | ReopenedEvent | CopilotStartedEvent | CopilotFinishedEvent | CopilotFinishedErrorEvent; diff --git a/src/common/uri.ts b/src/common/uri.ts index fd0f1d2728..36a60a8ee7 100644 --- a/src/common/uri.ts +++ b/src/common/uri.ts @@ -9,12 +9,15 @@ import { Buffer } from 'buffer'; import * as pathUtils from 'path'; import fetch from 'cross-fetch'; import * as vscode from 'vscode'; +import { RemoteInfo } from '../../common/types'; import { Repository } from '../api/api'; -import { IAccount, ITeam, reviewerId } from '../github/interface'; -import { PullRequestModel } from '../github/pullRequestModel'; +import { EXTENSION_ID } from '../constants'; import { GitChangeType } from './file'; import Logger from './logger'; import { TemporaryState } from './temporaryState'; +import { compareIgnoreCase } from './utils'; +import { IAccount, isITeam, ITeam, reviewerId } from '../github/interface'; +import { PullRequestModel } from '../github/pullRequestModel'; export interface ReviewUriParams { path: string; @@ -50,7 +53,8 @@ export function fromPRUri(uri: vscode.Uri): PRUriParams | undefined { } export interface PRNodeUriParams { - prIdentifier: string + prIdentifier: string; + showCopilot?: boolean; } export function fromPRNodeUri(uri: vscode.Uri): PRNodeUriParams | undefined { @@ -90,6 +94,29 @@ export interface GitUriOptions { base: boolean; } +export interface GitHubCommitUriParams { + commit: string; + owner: string; + repo: string; +} + +export function fromGitHubCommitUri(uri: vscode.Uri): GitHubCommitUriParams | undefined { + if (uri.scheme !== Schemes.GitHubCommit || uri.query === '') { + return undefined; + } + try { + return JSON.parse(uri.query) as GitHubCommitUriParams; + } catch (e) { } +} + +export function toGitHubCommitUri(fileName: string, params: GitHubCommitUriParams): vscode.Uri { + return vscode.Uri.from({ + scheme: Schemes.GitHubCommit, + path: `/${fileName}`, + query: JSON.stringify(params) + }); +} + const ImageMimetypes = ['image/png', 'image/gif', 'image/jpeg', 'image/webp', 'image/tiff', 'image/bmp']; // Known media types that VS Code can handle: https://github.com/microsoft/vscode/blob/a64e8e5673a44e5b9c2d493666bde684bd5a135c/src/vs/base/common/mime.ts#L33-L84 export const KnownMediaExtensions = [ @@ -179,7 +206,17 @@ export namespace DataUri { const iconsFolder = 'userIcons'; function iconFilename(user: IAccount | ITeam): string { - return `${reviewerId(user)}.jpg`; + // Include avatarUrl hash to invalidate cache when URL changes + const baseId = reviewerId(user); + if (user.avatarUrl) { + // Create a simple hash of the URL to detect changes + const urlHash = user.avatarUrl.split('').reduce((a, b) => { + a = ((a << 5) - a) + b.charCodeAt(0); + return a & a; + }, 0); + return `${baseId}_${Math.abs(urlHash)}.jpg`; + } + return `${baseId}.jpg`; } function cacheLocation(context: vscode.ExtensionContext): vscode.Uri { @@ -216,6 +253,43 @@ export namespace DataUri { ); } + export function copilotErrorAsImageDataURI(foreground: string, color: string): vscode.Uri { + const svgContent = ` + + +`; + const contents = Buffer.from(svgContent); + return asImageDataURI(contents); + } + + export function copilotInProgressAsImageDataURI(foreground: string, color: string): vscode.Uri { + const svgContent = ` + + +`; + const contents = Buffer.from(svgContent); + return asImageDataURI(contents); + } + + export function copilotSuccessAsImageDataURI(foreground: string, color: string): vscode.Uri { + const svgContent = ` + + +`; + const contents = Buffer.from(svgContent); + return asImageDataURI(contents); + } + + function genericUserIconAsImageDataURI(width: number, height: number): vscode.Uri { + // The account icon + const foreground = vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark ? '#FFFFFF' : '#000000'; + const svgContent = ` + +`; + const contents = Buffer.from(svgContent); + return asImageDataURI(contents); + } + export async function avatarCirclesAsImageDataUris(context: vscode.ExtensionContext, users: (IAccount | ITeam)[], height: number, width: number, localOnly?: boolean): Promise<(vscode.Uri | undefined)[]> { let cacheLogOrder: string[]; const cacheLog = cacheLogUri(context); @@ -228,7 +302,6 @@ export namespace DataUri { const startingCacheSize = cacheLogOrder.length; const results = await Promise.all(users.map(async (user) => { - const imageSourceUrl = user.avatarUrl; if (imageSourceUrl === undefined) { return undefined; @@ -248,15 +321,27 @@ export namespace DataUri { cacheMiss = true; const doFetch = async () => { const response = await fetch(imageSourceUrl.toString()); - const buffer = await response.arrayBuffer(); - await writeAvatarToCache(context, user, new Uint8Array(buffer)); - innerImageContents = Buffer.from(buffer); + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + if (response.headers.get('content-type')?.startsWith('image/')) { + const buffer = await response.arrayBuffer(); + await writeAvatarToCache(context, user, new Uint8Array(buffer)); + innerImageContents = Buffer.from(buffer); + } }; try { await doFetch(); } catch (e) { // We retry once. - await doFetch(); + try { + await doFetch(); + } catch (retryError) { + // Log the error and return a generic user icon instead of crashing + const userIdentifier = isITeam(user) ? `${user.org}/${user.slug}` : user.login || 'unknown'; + Logger.error(`Failed to fetch avatar after retry for user ${userIdentifier}: ${retryError}`, 'avatarCirclesAsImageDataUris'); + return genericUserIconAsImageDataURI(width, height); + } } } if (!innerImageContents) { @@ -294,6 +379,13 @@ export namespace DataUri { } } +/** + * @param fileName The repo relative path to the file + */ +export function reviewPath(fileName: string, commitSha: string) { + return vscode.Uri.parse(pathUtils.posix.join(`commit~${commitSha.substr(0, 8)}`, fileName)); +} + export function toReviewUri( uri: vscode.Uri, filePath: string | undefined, @@ -397,13 +489,30 @@ export function createPRNodeIdentifier(pullRequest: PullRequestModel | { remote: return identifier; } +export function parsePRNodeIdentifier(identifier: string): { remote: string, prNumber: number } | undefined { + const lastColon = identifier.lastIndexOf(':'); + if (lastColon === -1) { + return undefined; + } + const remote = identifier.substring(0, lastColon); + const prNumberStr = identifier.substring(lastColon + 1); + const prNumber = Number(prNumberStr); + if (!remote || isNaN(prNumber) || prNumber <= 0) { + return undefined; + } + return { remote, prNumber }; +} + export function createPRNodeUri( - pullRequest: PullRequestModel | { remote: string, prNumber: number } | string + pullRequest: PullRequestModel | { remote: string, prNumber: number } | string, showCopilot?: boolean ): vscode.Uri { const identifier = createPRNodeIdentifier(pullRequest); const params: PRNodeUriParams = { prIdentifier: identifier, }; + if (showCopilot !== undefined) { + params.showCopilot = showCopilot; + } const uri = vscode.Uri.parse(`PRNode:${identifier}`); @@ -413,6 +522,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; } @@ -500,6 +639,132 @@ export function fromRepoUri(uri: vscode.Uri): RepoUriParams | undefined { } catch (e) { } } +const ownerRegex = /^(?!-)(?!.*--)[a-zA-Z0-9-]+(? { + const query = JSON.stringify(params); + return vscode.env.asExternalUri(vscode.Uri.from({ scheme: vscode.env.uriScheme, authority: EXTENSION_ID, path: UriHandlerPaths.OpenIssueWebview, query })); +} + +export function fromOpenIssueWebviewUri(uri: vscode.Uri): OpenIssueWebviewUriParams | undefined { + if (compareIgnoreCase(uri.authority, EXTENSION_ID) !== 0) { + return; + } + if (uri.path !== UriHandlerPaths.OpenIssueWebview) { + return; + } + try { + const query = JSON.parse(uri.query.split('&')[0]); + if (!validateOpenWebviewParams(query.owner, query.repo, query.issueNumber)) { + return; + } + return query; + } catch (e) { } +} + +export interface OpenPullRequestWebviewUriParams { + owner: string; + repo: string; + pullRequestNumber: number; +} + +export async function toOpenPullRequestWebviewUri(params: OpenPullRequestWebviewUriParams): Promise { + const query = JSON.stringify(params); + return vscode.env.asExternalUri(vscode.Uri.from({ scheme: vscode.env.uriScheme, authority: EXTENSION_ID, path: UriHandlerPaths.OpenPullRequestWebview, query })); +} + +export function fromOpenOrCheckoutPullRequestWebviewUri(uri: vscode.Uri): OpenPullRequestWebviewUriParams | undefined { + if (compareIgnoreCase(uri.authority, EXTENSION_ID) !== 0) { + return; + } + if (uri.path !== UriHandlerPaths.OpenPullRequestWebview && uri.path !== UriHandlerPaths.CheckoutPullRequest) { + 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 + // 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, + 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; + } + return query; + } catch (e) { } +} + +export function toQueryUri(params: { remote: RemoteInfo | undefined, isCopilot?: boolean }) { + const uri = vscode.Uri.from({ scheme: Schemes.PRQuery, path: params.isCopilot ? 'copilot' : undefined, query: params.remote ? JSON.stringify({ remote: params.remote }) : undefined }); + return uri; +} + +export function fromQueryUri(uri: vscode.Uri): { remote: RemoteInfo | undefined, isCopilot?: boolean } | undefined { + if (uri.scheme !== Schemes.PRQuery) { + return; + } + try { + const query = uri.query ? JSON.parse(uri.query) : undefined; + return { + remote: query.remote, + isCopilot: uri.path === 'copilot' + }; + } catch (e) { } +} export enum Schemes { File = 'file', @@ -516,6 +781,9 @@ export enum Schemes { NewIssue = 'newissue', // New issue file 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 + CommitsNode = 'commitsnode' // Commits tree node, for decorations } export function resolvePath(from: vscode.Uri, to: string) { @@ -525,11 +793,3 @@ export function resolvePath(from: vscode.Uri, to: string) { return pathUtils.posix.resolve(from.path, to); } } - -class UriEventHandler extends vscode.EventEmitter implements vscode.UriHandler { - public handleUri(uri: vscode.Uri) { - this.fire(uri); - } -} - -export const handler = new UriEventHandler(); diff --git a/src/common/utils.ts b/src/common/utils.ts index d22a66b404..0f36f1202b 100644 --- a/src/common/utils.ts +++ b/src/common/utils.ts @@ -102,7 +102,13 @@ function isWindowsPath(path: string): boolean { return /^[a-zA-Z]:\\/.test(path); } -export function isDescendant(parent: string, descendant: string, separator: string = sep): boolean { +export function isDescendant(parent: string, descendant: string, caseInsensitive: boolean = false, separator: string = sep): boolean { + // Windows is case insensitive + if (isWindowsPath(parent) || caseInsensitive) { + parent = parent.toLowerCase(); + descendant = descendant.toLowerCase(); + } + if (parent === descendant) { return true; } @@ -111,12 +117,6 @@ export function isDescendant(parent: string, descendant: string, separator: stri parent += separator; } - // Windows is case insensitive - if (isWindowsPath(parent)) { - parent = parent.toLowerCase(); - descendant = descendant.toLowerCase(); - } - return descendant.startsWith(parent); } @@ -135,11 +135,11 @@ export class UnreachableCaseError extends Error { } interface HookError extends Error { - errors: any; + errors: (string | { message: string })[]; } function isHookError(e: Error): e is HookError { - return !!(e as any).errors; + return !!(e as Partial).errors; } function hasFieldErrors(e: any): e is Error & { errors: { value: string; field: string; status: string }[] } { @@ -184,7 +184,7 @@ export function formatError(e: HookError | any): string { return e.message; } else if (isHookError(e) && e.errors) { return e.errors - .map((error: any) => { + .map((error) => { if (typeof error === 'string') { return error; } else { @@ -200,10 +200,6 @@ export function formatError(e: HookError | any): string { return errorMessage; } -export interface PromiseAdapter { - (value: T, resolve: (value?: U | PromiseLike) => void, reject: (reason: any) => void): any; -} - // Copied from https://github.com/microsoft/vscode/blob/cfd9d25826b5b5bc3b06677521660b4f1ba6639a/extensions/vscode-api-tests/src/utils.ts#L135-L136 export async function asPromise(event: Event): Promise { return new Promise((resolve) => { @@ -370,6 +366,10 @@ export interface Predicate { (input: T): boolean; } +export interface AsyncPredicate { + (input: T): Promise; +} + export const enum CharCode { Period = 46, /** @@ -986,6 +986,16 @@ export async function stringReplaceAsync(str: string, regex: RegExp, asyncFn: (s return str.replace(regex, () => data[offset++]); } +export async function arrayFindIndexAsync(arr: T[], predicate: (value: T, index: number, array: T[]) => Promise): Promise { + for (let i = 0; i < arr.length; i++) { + // Evaluate predicate sequentially to allow early exit on first match + if (await predicate(arr[i], i, arr)) { + return i; + } + } + return -1; +} + export async function batchPromiseAll(items: readonly T[], batchSize: number, processFn: (item: T) => Promise): Promise { const batches = Math.ceil(items.length / batchSize); @@ -995,3 +1005,13 @@ export async function batchPromiseAll(items: readonly T[], batchSize: number, } } +export function escapeRegExp(string: string) { + return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} + +export function truncate(value: string, maxLength: number, suffix = '...'): string { + if (value.length <= maxLength) { + return value; + } + return `${value.substr(0, maxLength)}${suffix}`; +} \ No newline at end of file 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 d392fc8576..f887fd349b 100644 --- a/src/common/webview.ts +++ b/src/common/webview.ts @@ -17,19 +17,11 @@ export interface IRequestMessage { export interface IReplyMessage { seq?: string; - err?: any; + err?: string; + // eslint-disable-next-line rulesdir/no-any-except-union-method-signature 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; @@ -85,7 +77,7 @@ export class WebviewBase extends Disposable { this._webview?.postMessage(reply); } - protected async _throwError(originalMessage: IRequestMessage | undefined, error: any) { + protected async _throwError(originalMessage: IRequestMessage | undefined, error: string) { const reply: IReplyMessage = { seq: originalMessage?.req, err: error, diff --git a/src/extension.ts b/src/extension.ts index 6ab8a5e2b9..f91a82d43f 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -3,42 +3,57 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -'use strict'; import TelemetryReporter from '@vscode/extension-telemetry'; import * as vscode from 'vscode'; + import { LiveShare } from 'vsls/vscode.js'; import { PostCommitCommandsProvider, Repository } from './api/api'; import { GitApiImpl } from './api/api1'; import { registerCommands } from './commands'; +import { COPILOT_SWE_AGENT } from './common/copilot'; import { commands } from './common/executeCommands'; +import { isSubmodule } from './common/gitUtils'; import Logger from './common/logger'; import * as PersistentState from './common/persistentState'; import { parseRepositoryRemotes } from './common/remote'; -import { Resource } from './common/resources'; -import { BRANCH_PUBLISH, EXPERIMENTAL_CHAT, EXPERIMENTAL_NOTIFICATIONS, FILE_LIST_LAYOUT, GIT, OPEN_DIFF_ON_CLICK, PR_SETTINGS_NAMESPACE, SHOW_INLINE_OPEN_FILE_ACTION } from './common/settingKeys'; +import { BRANCH_PUBLISH, EXPERIMENTAL_CHAT, FILE_LIST_LAYOUT, GIT, IGNORE_SUBMODULES, OPEN_DIFF_ON_CLICK, PR_SETTINGS_NAMESPACE, SHOW_INLINE_OPEN_FILE_ACTION } from './common/settingKeys'; +import { initBasedOnSettingChange } from './common/settingsUtils'; import { TemporaryState } from './common/temporaryState'; -import { Schemes, handler as uriHandler } from './common/uri'; +import { Schemes } from './common/uri'; +import { isDescendant } from './common/utils'; import { EXTENSION_ID, FOCUS_REVIEW_MODE } from './constants'; import { createExperimentationService, ExperimentationTelemetry } from './experimentationService'; +import { CopilotRemoteAgentManager } from './github/copilotRemoteAgent'; import { CredentialStore } from './github/credentials'; import { FolderRepositoryManager } from './github/folderRepositoryManager'; +import { OverviewRestorer } from './github/overviewRestorer'; import { RepositoriesManager } from './github/repositoriesManager'; import { registerBuiltinGitProvider, registerLiveShareGitProvider } from './gitProviders/api'; import { GitHubContactServiceProvider } from './gitProviders/GitHubContactServiceProvider'; import { GitLensIntegration } from './integrations/gitlens/gitlensImpl'; import { IssueFeatureRegistrar } from './issues/issueFeatureRegistrar'; +import { StateManager } from './issues/stateManager'; +import { IssueContextProvider } from './lm/issueContextProvider'; import { ChatParticipant, ChatParticipantState } from './lm/participants'; +import { PullRequestContextProvider } from './lm/pullRequestContextProvider'; import { registerTools } from './lm/tools/tools'; import { migrate } from './migrations'; import { NotificationsFeatureRegister } from './notifications/notificationsFeatureRegistar'; +import { NotificationsManager } from './notifications/notificationsManager'; +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'; import { FileTypeDecorationProvider } from './view/fileTypeDecorationProvider'; +import { GitHubCommitFileSystemProvider } from './view/githubFileContentProvider'; import { getInMemPRFileSystemProvider } from './view/inMemPRContentProvider'; import { PullRequestChangesTreeDataProvider } from './view/prChangesTreeDataProvider'; -import { PRNotificationDecorationProvider } from './view/prNotificationDecorationProvider'; import { PullRequestsTreeDataProvider } from './view/prsTreeDataProvider'; +import { PrsTreeModel } from './view/prsTreeModel'; import { ReviewManager, ShowPullRequest } from './view/reviewManager'; import { ReviewsManager } from './view/reviewsManager'; import { TreeDecorationProviders } from './view/treeDecorationProviders'; @@ -59,7 +74,10 @@ async function init( liveshareApiPromise: Promise, showPRController: ShowPullRequest, reposManager: RepositoriesManager, - createPrHelper: CreatePullRequestHelper + createPrHelper: CreatePullRequestHelper, + copilotRemoteAgentManager: CopilotRemoteAgentManager, + themeWatcher: ThemeWatcher, + prsTreeModel: PrsTreeModel, ): Promise { context.subscriptions.push(Logger); Logger.appendLine('Git repository found, initializing review manager and pr tree view.', ACTIVATION); @@ -118,8 +136,6 @@ async function init( }), ); - context.subscriptions.push(vscode.window.registerUriHandler(uriHandler)); - // Sort the repositories to match folders in a multiroot workspace (if possible). const workspaceFolders = vscode.workspace.workspaceFolders; if (workspaceFolders) { @@ -159,11 +175,23 @@ 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); - const reviewsManager = new ReviewsManager(context, reposManager, reviewManagers, tree, changesTree, telemetry, credentialStore, git); + const notificationsManager = new NotificationsManager(notificationsProvider, credentialStore, reposManager, context); + context.subscriptions.push(notificationsManager); + + const reviewsManager = new ReviewsManager(context, reposManager, reviewManagers, prsTreeModel, tree, changesTree, telemetry, credentialStore, git, copilotRemoteAgentManager, notificationsManager); context.subscriptions.push(reviewsManager); + context.subscriptions.push(vscode.languages.registerCompletionItemProvider( + { scheme: Schemes.Comment }, + new EmojiCompletionProvider(context), + ':' + )); + git.onDidChangeState(() => { Logger.appendLine(`Git initialization state changed: state=${git.state}`, ACTIVATION); reviewsManager.reviewManagers.forEach(reviewManager => reviewManager.updateState(true)); @@ -177,7 +205,15 @@ async function init( Logger.appendLine(`Repo ${repo.rootUri} has already been setup.`, ACTIVATION); return; } - const newFolderManager = new FolderRepositoryManager(reposManager.folderManagers.length, context, repo, telemetry, git, credentialStore, createPrHelper); + + // Check if submodules should be ignored + const ignoreSubmodules = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(IGNORE_SUBMODULES, false); + if (ignoreSubmodules && isSubmodule(repo, git)) { + Logger.appendLine(`Repo ${repo.rootUri} is a submodule and will be ignored due to ${IGNORE_SUBMODULES} setting.`, ACTIVATION); + return; + } + + const newFolderManager = new FolderRepositoryManager(reposManager.folderManagers.length, context, repo, telemetry, git, credentialStore, createPrHelper, themeWatcher); reposManager.insertFolderManager(newFolderManager); const newReviewManager = new ReviewManager( reviewManagerIndex++, @@ -194,8 +230,15 @@ async function init( ); reviewsManager.addReviewManager(newReviewManager); } + + // Check if repo is in one of the workspace folders or vice versa + Logger.debug(`Checking if repo ${repo.rootUri.fsPath} is in a workspace folder.`, ACTIVATION); + Logger.debug(`Workspace folders: ${workspaceFolders?.map(folder => folder.uri.fsPath).join(', ')}`, ACTIVATION); + if (workspaceFolders && !workspaceFolders.some(folder => isDescendant(folder.uri.fsPath, repo.rootUri.fsPath, true) || isDescendant(repo.rootUri.fsPath, folder.uri.fsPath, true))) { + Logger.appendLine(`Repo ${repo.rootUri} is not in a workspace folder, ignoring.`, ACTIVATION); + return; + } addRepo(); - tree.notificationProvider.refreshOrLaunchPolling(); const disposable = repo.state.onDidChange(() => { Logger.appendLine(`Repo state for ${repo.rootUri} changed.`, ACTIVATION); addRepo(); @@ -206,35 +249,41 @@ async function init( git.onDidCloseRepository(repo => { reposManager.removeRepo(repo); reviewsManager.removeReviewManager(repo); - tree.notificationProvider.refreshOrLaunchPolling(); }); - tree.initialize(reviewsManager.reviewManagers.map(manager => manager.reviewModel), credentialStore); - - context.subscriptions.push(new PRNotificationDecorationProvider(tree.notificationProvider)); + tree.initialize(reviewsManager.reviewManagers.map(manager => manager.reviewModel), notificationsManager); - registerCommands(context, reposManager, reviewsManager, telemetry, tree); + registerCommands(context, reposManager, reviewsManager, telemetry, copilotRemoteAgentManager, notificationsManager, prsTreeModel); const layout = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(FILE_LIST_LAYOUT); await vscode.commands.executeCommand('setContext', 'fileListLayout:flat', layout === 'flat'); - const issuesFeatures = new IssueFeatureRegistrar(git, reposManager, reviewsManager, context, telemetry); + const issueStateManager = new StateManager(git, reposManager, context); + const issuesFeatures = new IssueFeatureRegistrar(git, reposManager, reviewsManager, context, telemetry, issueStateManager, copilotRemoteAgentManager); context.subscriptions.push(issuesFeatures); await issuesFeatures.initialize(); - const notificationsViewEnabled = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(EXPERIMENTAL_NOTIFICATIONS, false); - if (notificationsViewEnabled) { - const notificationsFeatures = new NotificationsFeatureRegister(credentialStore, reposManager, telemetry); - context.subscriptions.push(notificationsFeatures); - } + const pullRequestContextProvider = new PullRequestContextProvider(prsTreeModel, reposManager, git); + vscode.chat.registerChatContextProvider({ scheme: 'webview-panel', pattern: '**/webview-PullRequestOverview**' }, 'githubpr', pullRequestContextProvider); + vscode.chat.registerChatContextProvider({ scheme: 'webview-panel', pattern: '**/webview-IssueOverview**' }, 'githubissue', new IssueContextProvider(issueStateManager, reposManager)); + pullRequestContextProvider.initialize(); + + const notificationsFeatures = new NotificationsFeatureRegister(credentialStore, reposManager, telemetry, notificationsManager); + context.subscriptions.push(notificationsFeatures); context.subscriptions.push(new GitLensIntegration()); + context.subscriptions.push(new OverviewRestorer(reposManager, telemetry, context.extensionUri, credentialStore)); + 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); - initChat(context, credentialStore, reposManager); + initChat(context, credentialStore, reposManager, copilotRemoteAgentManager, telemetry, prsTreeModel); + 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(); @@ -244,32 +293,24 @@ async function init( telemetry.sendTelemetryEvent('startup'); } -function initChat(context: vscode.ExtensionContext, credentialStore: CredentialStore, reposManager: RepositoriesManager) { +function initChat(context: vscode.ExtensionContext, credentialStore: CredentialStore, reposManager: RepositoriesManager, copilotRemoteManager: CopilotRemoteAgentManager, telemetry: ExperimentationTelemetry, prsTreeModel: PrsTreeModel) { const createParticipant = () => { const chatParticipantState = new ChatParticipantState(); context.subscriptions.push(new ChatParticipant(context, chatParticipantState)); - registerTools(context, credentialStore, reposManager, chatParticipantState); + registerTools(context, credentialStore, reposManager, chatParticipantState, copilotRemoteManager, telemetry, prsTreeModel); }; const chatEnabled = () => vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(EXPERIMENTAL_CHAT, false); if (chatEnabled()) { createParticipant(); } else { - const disposable = vscode.workspace.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(`${PR_SETTINGS_NAMESPACE}.${EXPERIMENTAL_CHAT}`)) { - if (chatEnabled()) { - disposable.dispose(); - createParticipant(); - } - } - }); - context.subscriptions.push(disposable); + initBasedOnSettingChange(PR_SETTINGS_NAMESPACE, EXPERIMENTAL_CHAT, chatEnabled, createParticipant, context.subscriptions); } } export async function activate(context: vscode.ExtensionContext): Promise { Logger.appendLine(`Extension version: ${vscode.extensions.getExtension(EXTENSION_ID)?.packageJSON.version}`, 'Activation'); - // eslint-disable-next-line @typescript-eslint/ban-ts-comment + // @ts-ignore if (EXTENSION_ID === 'GitHub.vscode-pull-request-github-insiders') { const stable = vscode.extensions.getExtension('github.vscode-pull-request-github'); @@ -288,17 +329,12 @@ export async function activate(context: vscode.ExtensionContext): Promise 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 => { @@ -339,7 +375,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)'), @@ -352,10 +388,10 @@ 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); - git.registerPostCommitCommandsProvider(new Provider()); + Logger.appendLine('GitHub remote(s) found, registering post commit commands.', componentId); + context.subscriptions.push(git.registerPostCommitCommandsProvider(new Provider())); return true; } return false; @@ -371,7 +407,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)) { @@ -382,7 +418,7 @@ async function deferredActivateRegisterBuiltInGitProvider(context: vscode.Extens } } -async function deferredActivate(context: vscode.ExtensionContext, apiImpl: GitApiImpl, showPRController: ShowPullRequest) { +async function deferredActivate(context: vscode.ExtensionContext, showPRController: ShowPullRequest) { Logger.debug('Initializing state.', 'Activation'); PersistentState.init(context); await migrate(context); @@ -393,8 +429,17 @@ async function deferredActivate(context: vscode.ExtensionContext, apiImpl: GitAp const experimentationService = await createExperimentationService(context, telemetry); await experimentationService.initializePromise; await experimentationService.isCachedFlightEnabled('githubaa'); - const showBadge = (vscode.env.appHost === 'desktop'); - await credentialStore.create(showBadge ? undefined : { silent: true }); + await credentialStore.create(); + + const reposManager = new RepositoriesManager(credentialStore, telemetry); + context.subscriptions.push(reposManager); + + const prsTreeModel = new PrsTreeModel(telemetry, reposManager, context); + context.subscriptions.push(prsTreeModel); + + // API + const apiImpl = new GitApiImpl(reposManager); + context.subscriptions.push(apiImpl); deferredActivateRegisterBuiltInGitProvider(context, apiImpl, credentialStore); @@ -406,21 +451,55 @@ async function deferredActivate(context: vscode.ExtensionContext, apiImpl: GitAp context.subscriptions.push(apiImpl); Logger.debug('Creating tree view.', 'Activation'); - const reposManager = new RepositoriesManager(credentialStore, telemetry); - context.subscriptions.push(reposManager); - const prTree = new PullRequestsTreeDataProvider(telemetry, context, reposManager); + const copilotRemoteAgentManager = new CopilotRemoteAgentManager(credentialStore, reposManager, telemetry, context, apiImpl, prsTreeModel); + context.subscriptions.push(copilotRemoteAgentManager); + if (vscode.chat?.registerChatSessionItemProvider) { + const chatParticipant = vscode.chat.createChatParticipant(COPILOT_SWE_AGENT, async (request, context, stream, token) => + await copilotRemoteAgentManager.chatParticipantImpl(request, context, stream, token) + ); + context.subscriptions.push(chatParticipant); + + const provider = new class implements vscode.ChatSessionContentProvider, vscode.ChatSessionItemProvider { + label = vscode.l10n.t('GitHub Copilot Coding Agent'); + async provideChatSessionItems(token: vscode.CancellationToken) { + return await copilotRemoteAgentManager.provideChatSessions(token); + } + async provideChatSessionContent(resource: vscode.Uri, token: vscode.CancellationToken) { + return await copilotRemoteAgentManager.provideChatSessionContent(resource, token); + } + onDidChangeChatSessionItems = copilotRemoteAgentManager.onDidChangeChatSessions; + onDidCommitChatSessionItem = copilotRemoteAgentManager.onDidCommitChatSession; + }(); + + context.subscriptions.push(vscode.chat?.registerChatSessionItemProvider( + COPILOT_SWE_AGENT, + provider + )); + + context.subscriptions.push(vscode.chat?.registerChatSessionContentProvider( + COPILOT_SWE_AGENT, + provider, + chatParticipant, + { supportsInterruptions: true } + )); + } + + const prTree = new PullRequestsTreeDataProvider(prsTreeModel, telemetry, context, reposManager, copilotRemoteAgentManager); context.subscriptions.push(prTree); - context.subscriptions.push(credentialStore.onDidGetSession(() => prTree.refresh(undefined, true))); + context.subscriptions.push(credentialStore.onDidGetSession(() => prTree.refreshAll(true))); Logger.appendLine('Looking for git repository', ACTIVATION); const repositories = apiImpl.repositories; Logger.appendLine(`Found ${repositories.length} repositories during activation`, ACTIVATION); const createPrHelper = new CreatePullRequestHelper(); context.subscriptions.push(createPrHelper); + const themeWatcher = new ThemeWatcher(); + context.subscriptions.push(themeWatcher); + let folderManagerIndex = 0; const folderManagers = repositories.map( - repository => new FolderRepositoryManager(folderManagerIndex++, context, repository, telemetry, apiImpl, credentialStore, createPrHelper), + repository => new FolderRepositoryManager(folderManagerIndex++, context, repository, telemetry, apiImpl, credentialStore, createPrHelper, themeWatcher), ); context.subscriptions.push(...folderManagers); for (const folderManager of folderManagers) { @@ -431,8 +510,11 @@ async function deferredActivate(context: vscode.ExtensionContext, apiImpl: GitAp const readOnlyMessage = new vscode.MarkdownString(vscode.l10n.t('Cannot edit this pull request file. [Check out](command:pr.checkoutFromReadonlyFile) this pull request to edit.')); readOnlyMessage.isTrusted = { enabledCommands: ['pr.checkoutFromReadonlyFile'] }; context.subscriptions.push(vscode.workspace.registerFileSystemProvider(Schemes.Pr, inMemPRFileSystemProvider, { isReadonly: readOnlyMessage })); + const githubFilesystemProvider = new GitHubCommitFileSystemProvider(reposManager, apiImpl, credentialStore); + context.subscriptions.push(vscode.workspace.registerFileSystemProvider(Schemes.GitHubCommit, githubFilesystemProvider, { isReadonly: new vscode.MarkdownString(vscode.l10n.t('GitHub commits cannot be edited')) })); - await init(context, apiImpl, credentialStore, repositories, prTree, liveshareApiPromise, showPRController, reposManager, createPrHelper); + await init(context, apiImpl, credentialStore, repositories, prTree, liveshareApiPromise, showPRController, reposManager, createPrHelper, copilotRemoteAgentManager, themeWatcher, prsTreeModel); + return apiImpl; } export async function deactivate() { diff --git a/src/extensionState.ts b/src/extensionState.ts index 976baa4fee..db7c33fc10 100644 --- a/src/extensionState.ts +++ b/src/extensionState.ts @@ -13,6 +13,7 @@ export const NEVER_SHOW_PULL_NOTIFICATION = 'github.pullRequest.pullNotification export const REPO_KEYS = 'github.pullRequest.repos'; export const PREVIOUS_CREATE_METHOD = 'github.pullRequest.previousCreateMethod'; export const LAST_USED_EMAIL = 'github.pullRequest.lastUsedEmail'; +export const BRANCHES_ASSOCIATED_WITH_PRS = 'github.pullRequest.branchesAssociatedWithPRs'; export interface RepoState { mentionableUsers?: IAccount[]; diff --git a/src/gitProviders/GitHubContactServiceProvider.ts b/src/gitProviders/GitHubContactServiceProvider.ts index ad7f11308c..bc3973cfef 100644 --- a/src/gitProviders/GitHubContactServiceProvider.ts +++ b/src/gitProviders/GitHubContactServiceProvider.ts @@ -18,7 +18,7 @@ interface ContactServiceProvider { interface NotifyContactServiceEventArgs { type: string; - body?: any | undefined; + body?: { contacts: Contact[]; exclusive?: boolean } | undefined; } /** @@ -132,7 +132,7 @@ export class GitHubContactServiceProvider implements ContactServiceProvider { } } - private notify(type: string, body: any) { + private notify(type: string, body: { contacts: Contact[]; exclusive?: boolean }) { this.onNotifiedEmitter.fire({ type, body, diff --git a/src/gitProviders/api.ts b/src/gitProviders/api.ts index 353a957dff..cd275d7f93 100644 --- a/src/gitProviders/api.ts +++ b/src/gitProviders/api.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { API } from '../api/api'; -import { CredentialStore } from '../github/credentials'; import { BuiltinGitProvider } from './builtinGit'; import { LiveShareManager } from './vsls'; +import { API } from '../api/api'; +import { CredentialStore } from '../github/credentials'; export function registerLiveShareGitProvider(apiImpl: API): LiveShareManager { const liveShareManager = new LiveShareManager(apiImpl); diff --git a/src/gitProviders/builtinGit.ts b/src/gitProviders/builtinGit.ts index fc42a50cc5..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'; @@ -41,11 +41,18 @@ export class BuiltinGitProvider extends Disposable implements IGit { throw e; } - this._register(this._gitAPI.onDidCloseRepository(e => this._onDidCloseRepository.fire(e as any))); - this._register(this._gitAPI.onDidOpenRepository(e => this._onDidOpenRepository.fire(e as any))); + this._register(this._gitAPI.onDidCloseRepository(e => this._onDidCloseRepository.fire(e))); + this._register(this._gitAPI.onDidOpenRepository(e => this._onDidOpenRepository.fire(e))); 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); + } + + clone(uri: vscode.Uri, options?: CloneOptions): Promise { + return this._gitAPI.clone(uri, options); + } static async createProvider(): Promise { const extension = vscode.extensions.getExtension('vscode.git'); diff --git a/src/gitProviders/vsls.ts b/src/gitProviders/vsls.ts index c58783ea8f..a5eeee9c9d 100644 --- a/src/gitProviders/vsls.ts +++ b/src/gitProviders/vsls.ts @@ -4,11 +4,12 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; + import { LiveShare } from 'vsls/vscode.js'; -import { API } from '../api/api'; -import { Disposable, disposeAll } from '../common/lifecycle'; import { VSLSGuest } from './vslsguest'; import { VSLSHost } from './vslshost'; +import { API } from '../api/api'; +import { Disposable, disposeAll } from '../common/lifecycle'; /** * Should be removed once we fix the webpack bundling issue. diff --git a/src/gitProviders/vslsguest.ts b/src/gitProviders/vslsguest.ts index 40826f44e5..fd823781af 100644 --- a/src/gitProviders/vslsguest.ts +++ b/src/gitProviders/vslsguest.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ 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 { @@ -114,7 +115,7 @@ export class VSLSGuest extends Disposable implements IGit { } public getRepository(folder: vscode.WorkspaceFolder): Repository { - return this._openRepositories.filter(repository => (repository as any).workspaceFolder === folder)[0]; + return this._openRepositories.filter(repository => (repository as (Repository & { workspaceFolder: vscode.WorkspaceFolder })).workspaceFolder === folder)[0]; } } @@ -122,6 +123,7 @@ class LiveShareRepositoryProxyHandler { constructor() { } get(obj: any, prop: any) { + // eslint-disable-next-line no-restricted-syntax if (prop in obj) { return obj[prop]; } @@ -147,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/gitProviders/vslshost.ts b/src/gitProviders/vslshost.ts index 23b1d3706a..0cbb235761 100644 --- a/src/gitProviders/vslshost.ts +++ b/src/gitProviders/vslshost.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; + import { LiveShare, SharedService } from 'vsls/vscode.js'; import { API } from '../api/api'; import { Disposable } from '../common/lifecycle'; diff --git a/src/github/activityBarViewProvider.ts b/src/github/activityBarViewProvider.ts index bca6f76d64..345582cd17 100644 --- a/src/github/activityBarViewProvider.ts +++ b/src/github/activityBarViewProvider.ts @@ -4,24 +4,28 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { onDidUpdatePR, openPullRequestOnGitHub } from '../commands'; -import { IComment } from '../common/comment'; -import { disposeAll } from '../common/lifecycle'; -import { ReviewEvent as CommonReviewEvent } from '../common/timelineEvent'; -import { formatError } from '../common/utils'; -import { getNonce, IRequestMessage, WebviewViewBase } from '../common/webview'; -import { ReviewManager } from '../view/reviewManager'; +import { openPullRequestOnGitHub } from '../commands'; import { FolderRepositoryManager } from './folderRepositoryManager'; -import { GithubItemStateEnum, IAccount, isTeam, ITeam, PullRequestMergeability, reviewerId, ReviewEvent, ReviewState } from './interface'; -import { PullRequestModel } from './pullRequestModel'; +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, ReviewType } from './views'; +import { IComment } from '../common/comment'; +import { emojify, ensureEmojis } from '../common/emoji'; +import { disposeAll } from '../common/lifecycle'; +import { DELETE_BRANCH_AFTER_MERGE, PR_SETTINGS_NAMESPACE } from '../common/settingKeys'; +import { ReviewEvent } from '../common/timelineEvent'; +import { formatError } from '../common/utils'; +import { generateUuid } from '../common/uuid'; +import { IRequestMessage, WebviewViewBase } from '../common/webview'; +import { ReviewManager } from '../view/reviewManager'; export class PullRequestViewProvider extends WebviewViewBase implements vscode.WebviewViewProvider { public override readonly viewType = 'github:activePullRequest'; private _existingReviewers: ReviewState[] = []; + private _isUpdating: boolean = false; constructor( extensionUri: vscode.Uri, @@ -31,33 +35,20 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W ) { super(extensionUri); - this._register(onDidUpdatePR( - pr => { - if (pr) { - this._item.update(pr); - } - - this._postMessage({ - command: 'update-state', - state: this._item.state, - }); - })); - - this._register(this._folderRepositoryManager.onDidMergePullRequest(_ => { - this._postMessage({ - command: 'update-state', - state: GithubItemStateEnum.Merged, - }); + 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))); this._register(vscode.commands.registerCommand('review.approveOnDotCom', () => { - return openPullRequestOnGitHub(this._item, (this._item as any)._telemetry); + return openPullRequestOnGitHub(this._item, this._folderRepositoryManager.telemetry); })); this._register(vscode.commands.registerCommand('review.requestChangesOnDotCom', () => { - return openPullRequestOnGitHub(this._item, (this._item as any)._telemetry); + return openPullRequestOnGitHub(this._item, this._folderRepositoryManager.telemetry); })); } @@ -73,29 +64,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) { @@ -127,7 +100,7 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W case 'pr.submit': return this.submitReviewMessage(message); case 'pr.openOnGitHub': - return openPullRequestOnGitHub(this._item, (this._item as any)._telemetry); + return openPullRequestOnGitHub(this._item, this._folderRepositoryManager.telemetry); case 'pr.checkout-default-branch': return this.checkoutDefaultBranch(message); case 'pr.update-branch': @@ -138,47 +111,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 && isTeam(targetReviewer.reviewer)) { - teamReviewers.push(targetReviewer.reviewer); - } else if (targetReviewer && !isTeam(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 { @@ -189,9 +126,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; @@ -200,131 +150,145 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W disposeAll(this._prDisposables); } this._prDisposables = []; - this._prDisposables.push(pullRequestModel.onDidInvalidate(() => this.updatePullRequest(pullRequestModel))); + this._prDisposables.push(pullRequestModel.onDidChange(e => { + if ((e.state || e.comments || e.reviewers) && !this._isUpdating) { + this.updatePullRequest(pullRequestModel); + } + })); this._prDisposables.push(pullRequestModel.onDidChangePendingReviewState(() => this.updatePullRequest(pullRequestModel))); } private _updatePendingVisibility: vscode.Disposable | undefined = undefined; public async updatePullRequest(pullRequestModel: PullRequestModel): Promise { - if (this._view && !this._view.visible) { - this._updatePendingVisibility?.dispose(); - this._updatePendingVisibility = this._view.onDidChangeVisibility(async () => { - this.updatePullRequest(pullRequestModel); - this._updatePendingVisibility?.dispose(); - }); + if (this._isUpdating) { + throw new Error('Already updating pull request view'); } + this._isUpdating = true; - if ((this._prDisposables === undefined) || (pullRequestModel.number !== this._item.number)) { - this.registerPrSpecificListeners(pullRequestModel); - } - this._item = pullRequestModel; - return Promise.all([ - this._folderRepositoryManager.resolvePullRequest( - pullRequestModel.remote.owner, - pullRequestModel.remote.repositoryName, - pullRequestModel.number, - ), - this._folderRepositoryManager.getPullRequestRepositoryAccessAndMergeMethods(pullRequestModel), - pullRequestModel.getTimelineEvents(), - pullRequestModel.getReviewRequests(), - this._folderRepositoryManager.getBranchNameForPullRequest(pullRequestModel), - this._folderRepositoryManager.getPullRequestRepositoryDefaultBranch(pullRequestModel), - this._folderRepositoryManager.getCurrentUser(pullRequestModel.githubRepository), - pullRequestModel.canEdit(), - pullRequestModel.validateDraftMode() - ]) - .then(result => { - const [pullRequest, repositoryAccess, timelineEvents, requestedReviewers, branchInfo, defaultBranch, currentUser, viewerCanEdit, hasReviewDraft] = result; - if (!pullRequest) { - throw new Error( - `Fail to resolve Pull Request #${pullRequestModel.number} in ${pullRequestModel.remote.owner}/${pullRequestModel.remote.repositoryName}`, - ); - } + try { + if (this._view && !this._view.visible) { + this._updatePendingVisibility?.dispose(); + this._updatePendingVisibility = this._view.onDidChangeVisibility(async () => { + this.updatePullRequest(pullRequestModel); + this._updatePendingVisibility?.dispose(); + }); + } - this._item = pullRequest; - if (!this._view) { - // If the there is no PR webview, then there is nothing else to update. - return; - } + if ((this._prDisposables === undefined) || (pullRequestModel.number !== this._item.number)) { + this.registerPrSpecificListeners(pullRequestModel); + } + this._item = pullRequestModel; + const [pullRequest, repositoryAccess, timelineEvents, requestedReviewers, branchInfo, defaultBranch, currentUser, viewerCanEdit, hasReviewDraft, coAuthors] = await Promise.all([ + this._folderRepositoryManager.resolvePullRequest( + pullRequestModel.remote.owner, + pullRequestModel.remote.repositoryName, + pullRequestModel.number, + ), + this._folderRepositoryManager.getPullRequestRepositoryAccessAndMergeMethods(pullRequestModel), + pullRequestModel.getTimelineEvents(), + pullRequestModel.getReviewRequests(), + this._folderRepositoryManager.getBranchNameForPullRequest(pullRequestModel), + this._folderRepositoryManager.getPullRequestRepositoryDefaultBranch(pullRequestModel), + this._folderRepositoryManager.getCurrentUser(pullRequestModel.githubRepository), + pullRequestModel.canEdit(), + pullRequestModel.validateDraftMode(), + pullRequestModel.getCoAuthors(), + ensureEmojis(this._folderRepositoryManager.context) + ]); + + if (!pullRequest) { + throw new Error( + `Fail to resolve Pull Request #${pullRequestModel.number} in ${pullRequestModel.remote.owner}/${pullRequestModel.remote.repositoryName}`, + ); + } - try { - this._view.title = `${vscode.l10n.t('Review Pull Request')} #${pullRequestModel.number.toString()}`; - } catch (e) { - // If we ry to set the title of the webview too early it will throw an error. - } + this._item = pullRequest; + if (!this._view) { + // If the there is no PR webview, then there is nothing else to update. + return; + } - const isCurrentlyCheckedOut = pullRequestModel.equals(this._folderRepositoryManager.activePullRequest); - const hasWritePermission = repositoryAccess!.hasWritePermission; - const mergeMethodsAvailability = repositoryAccess!.mergeMethodsAvailability; - const canEdit = hasWritePermission || viewerCanEdit; - const defaultMergeMethod = getDefaultMergeMethod(mergeMethodsAvailability); - this._existingReviewers = parseReviewers( - requestedReviewers ?? [], - timelineEvents ?? [], - pullRequest.author, - ); + try { + this._view.title = `${vscode.l10n.t('Review Pull Request')} #${pullRequestModel.number.toString()}`; + } catch (e) { + // If we ry to set the title of the webview too early it will throw an error. + } - const isCrossRepository = - pullRequest.base && - pullRequest.head && - !pullRequest.base.repositoryCloneUrl.equals(pullRequest.head.repositoryCloneUrl); - - const continueOnGitHub = !!(isCrossRepository && isInCodespaces()); - const reviewState = this.getCurrentUserReviewState(this._existingReviewers, currentUser); - - const context: Partial = { - number: pullRequest.number, - title: pullRequest.title, - url: pullRequest.html_url, - createdAt: pullRequest.createdAt, - body: pullRequest.body, - bodyHTML: pullRequest.bodyHTML, - labels: pullRequest.item.labels, - author: { - login: pullRequest.author.login, - name: pullRequest.author.name, - avatarUrl: pullRequest.userAvatar, - url: pullRequest.author.url, - email: pullRequest.author.email, - id: pullRequest.author.id, - accountType: pullRequest.author.accountType, - }, - state: pullRequest.state, - isCurrentlyCheckedOut: isCurrentlyCheckedOut, - isRemoteBaseDeleted: pullRequest.isRemoteBaseDeleted, - base: pullRequest.base.label, - isRemoteHeadDeleted: pullRequest.isRemoteHeadDeleted, - isLocalHeadDeleted: !branchInfo, - head: pullRequest.head?.label ?? '', - canEdit: canEdit, - hasWritePermission, - mergeable: pullRequest.item.mergeable, - isDraft: pullRequest.isDraft, - status: null, - reviewRequirement: null, - canUpdateBranch: pullRequest.item.viewerCanUpdate, - events: timelineEvents, - mergeMethodsAvailability, - defaultMergeMethod, - repositoryDefaultBranch: defaultBranch, - isIssue: false, - isAuthor: currentUser.login === pullRequest.author.login, - reviewers: this._existingReviewers, - continueOnGitHub, - isDarkTheme: vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark, - isEnterprise: pullRequest.githubRepository.remote.isEnterprise, - hasReviewDraft, - currentUserReviewState: reviewState - }; - - this._postMessage({ - command: 'pr.initialize', - pullrequest: context, - }); - }) - .catch(e => { - vscode.window.showErrorMessage(`Error updating active pull request view: ${formatError(e)}`); + const isCurrentlyCheckedOut = pullRequestModel.equals(this._folderRepositoryManager.activePullRequest); + const hasWritePermission = repositoryAccess!.hasWritePermission; + const mergeMethodsAvailability = repositoryAccess!.mergeMethodsAvailability; + const canEdit = hasWritePermission || viewerCanEdit; + const defaultMergeMethod = getDefaultMergeMethod(mergeMethodsAvailability); + this._existingReviewers = parseReviewers( + requestedReviewers ?? [], + timelineEvents ?? [], + pullRequest.author, + ); + + const isCrossRepository = + pullRequest.base && + pullRequest.head && + !pullRequest.base.repositoryCloneUrl.equals(pullRequest.head.repositoryCloneUrl); + + const continueOnGitHub = !!(isCrossRepository && isInCodespaces()); + const reviewState = this.getCurrentUserReviewState(this._existingReviewers, currentUser); + + const context: Partial = { + number: pullRequest.number, + title: pullRequest.title, + url: pullRequest.html_url, + createdAt: pullRequest.createdAt, + body: pullRequest.body, + bodyHTML: pullRequest.bodyHTML, + labels: pullRequest.item.labels.map(label => ({ ...label, displayName: emojify(label.name) })), + author: { + login: pullRequest.author.login, + name: pullRequest.author.name, + avatarUrl: pullRequest.userAvatar, + url: pullRequest.author.url, + email: pullRequest.author.email, + id: pullRequest.author.id, + accountType: pullRequest.author.accountType, + }, + state: pullRequest.state, + isCurrentlyCheckedOut: isCurrentlyCheckedOut, + isRemoteBaseDeleted: pullRequest.isRemoteBaseDeleted, + base: pullRequest.base.label, + isRemoteHeadDeleted: pullRequest.isRemoteHeadDeleted, + isLocalHeadDeleted: !branchInfo, + head: pullRequest.head?.label ?? '', + canEdit: canEdit, + hasWritePermission, + mergeable: pullRequest.item.mergeable, + isDraft: pullRequest.isDraft, + status: null, + reviewRequirement: null, + canUpdateBranch: pullRequest.item.viewerCanUpdate, + events: timelineEvents, + mergeMethodsAvailability, + defaultMergeMethod, + repositoryDefaultBranch: defaultBranch, + isIssue: false, + isAuthor: currentUser.login === pullRequest.author.login, + reviewers: this._existingReviewers, + continueOnGitHub, + isDarkTheme: vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark, + isEnterprise: pullRequest.githubRepository.remote.isEnterprise, + hasReviewDraft, + currentUserReviewState: reviewState, + isCopilotOnMyBehalf: await isCopilotOnMyBehalf(pullRequest, currentUser, coAuthors) + }; + + this._postMessage({ + command: 'pr.initialize', + pullrequest: context, }); + + } catch (e) { + vscode.window.showErrorMessage(`Error updating active pull request view: ${formatError(e)}`); + } finally { + this._isUpdating = false; + } } private close(message: IRequestMessage): void { @@ -349,84 +313,52 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W }); } - private updateReviewers(review?: CommonReviewEvent): 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 = { - command: 'pr.append-review', - event: review, - reviewers: this._existingReviewers - }; - 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' }); - } + private async doReviewCommand(context: { body: string }, reviewType: ReviewType, action: (body: string) => Promise) { + 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); - this._replyMessage(message, { - review: review, - reviewers: this._existingReviewers, - }); - } catch (e) { - vscode.window.showErrorMessage(vscode.l10n.t('Submitting review failed. {0}', formatError(e))); - this._throwError(message, `${formatError(e)}`); - } + private async doReviewMessage(message: IRequestMessage, action: (body) => Promise) { + return PullRequestReviewCommon.doReviewMessage( + this.getReviewContext(), + message, + false, + action + ); } - private approvePullRequest(body: string): Promise { + 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 { + 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 { - return this._item.submitReview(ReviewEvent.Comment, body); + private submitReview(body: string): Promise { + return this._item.submitReview(ReviewEventEnum.Comment, body); } private submitReviewCommand(context: { body: string }) { @@ -438,7 +370,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 { @@ -446,18 +378,16 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W } } - private setReadyForReview(message: IRequestMessage>): void { - this._item - .setReadyForReview() - .then(result => { - vscode.commands.executeCommand('pr.refreshList'); - - this._replyMessage(message, result); - }) - .catch(e => { - vscode.window.showErrorMessage(vscode.l10n.t('Unable to set PR 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 { + return PullRequestReviewCommon.readyForReviewCommand(this.getReviewContext()); + } + + private async readyForReviewAndMergeCommand(context: { mergeMethod: MergeMethod }): Promise { + return PullRequestReviewCommon.readyForReviewAndMergeCommand(this.getReviewContext(), context); } private async mergePullRequest( @@ -475,28 +405,32 @@ export class PullRequestViewProvider extends WebviewViewBase implements vscode.W this._replyMessage(message, { state: GithubItemStateEnum.Open }); return; } + try { + const result = await this._item.merge(this._folderRepositoryManager.repository, title, description, method, email); - this._folderRepositoryManager - .mergePullRequest(this._item, title, description, method, email) - .then(result => { - vscode.commands.executeCommand('pr.refreshList'); - - if (!result.merged) { - vscode.window.showErrorMessage(vscode.l10n.t('Merging PR failed: {0}', result?.message ?? '')); + if (!result.merged) { + vscode.window.showErrorMessage(vscode.l10n.t('Merging pull request failed: {0}', result?.message ?? '')); + } else { + // Check if auto-delete branch setting is enabled + const deleteBranchAfterMerge = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(DELETE_BRANCH_AFTER_MERGE, false); + if (deleteBranchAfterMerge) { + // Automatically delete the branch after successful merge + await PullRequestReviewCommon.autoDeleteBranchesAfterMerge(this._folderRepositoryManager, this._item); } + } - this._replyMessage(message, { - state: result.merged ? GithubItemStateEnum.Merged : GithubItemStateEnum.Open, - }); - }) - .catch(e => { - vscode.window.showErrorMessage(vscode.l10n.t('Unable to merge pull request. {0}', formatError(e))); - this._throwError(message, {}); + this._replyMessage(message, { + state: result.merged ? GithubItemStateEnum.Merged : GithubItemStateEnum.Open, }); + + } catch (e) { + vscode.window.showErrorMessage(vscode.l10n.t('Unable to merge pull request. {0}', formatError(e))); + this._throwError(message, ''); + } } 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/common.ts b/src/github/common.ts index 8524f3a4af..6d9b424d56 100644 --- a/src/github/common.ts +++ b/src/github/common.ts @@ -4,6 +4,15 @@ *--------------------------------------------------------------------------------------------*/ import * as OctokitRest from '@octokit/rest'; import { Endpoints } from '@octokit/types'; +import { DocumentNode } from 'graphql'; +import { ChatSessionStatus, Uri } from 'vscode'; +import { SessionInfo, SessionSetupStep } from './copilotApi'; +import { FolderRepositoryManager } from './folderRepositoryManager'; +import { GitHubRepository } from './githubRepository'; +import { Repository } from '../api/api'; +import { CopilotPRStatus } from '../common/copilot'; +import { GitHubRemote } from '../common/remote'; +import { EventType, TimelineEvent } from '../common/timelineEvent'; export namespace OctokitCommon { export type IssuesAssignParams = OctokitRest.RestEndpointMethodTypes['issues']['addAssignees']['parameters']; @@ -36,9 +45,12 @@ export namespace OctokitCommon { user_view_type: string; } export type PullsCreateParams = OctokitRest.RestEndpointMethodTypes['pulls']['create']['parameters']; - export type PullsCreateReviewResponseData = Endpoints['POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews']['response']['data']; + export type PullsCreateReviewResponseData = Endpoints['POST /repos/{owner}/{repo}/pulls/{pull_number}/reviews']['response']['data'] & { + submitted_at: string; + }; export type PullsCreateReviewCommentResponseData = Endpoints['POST /repos/{owner}/{repo}/pulls/{pull_number}/comments']['response']['data']; export type PullsGetResponseData = OctokitRest.RestEndpointMethodTypes['pulls']['get']['response']['data']; + export type IssuesGetResponseData = OctokitRest.RestEndpointMethodTypes['issues']['get']['response']['data']; export type PullsGetResponseUser = Exclude; export type PullsListCommitsResponseData = Endpoints['GET /repos/{owner}/{repo}/pulls/{pull_number}/commits']['response']['data']; export type PullsListRequestedReviewersResponseData = Endpoints['GET /repos/{owner}/{repo}/pulls/{pull_number}/requested_reviewers']['response']['data']; @@ -70,10 +82,14 @@ export namespace OctokitCommon { export type Commit = CompareCommits['commits'][0]; export type CommitFiles = CompareCommits['files'] export type Notification = Endpoints['GET /notifications']['response']['data'][0]; + export type ListEventsForTimelineResponse = Endpoints['GET /repos/{owner}/{repo}/issues/{issue_number}/timeline']['response']['data'][0]; + export type ListWorkflowRunsForRepo = Endpoints['GET /repos/{owner}/{repo}/actions/runs']['response']['data']; + export type WorkflowRun = Endpoints['GET /repos/{owner}/{repo}/actions/runs']['response']['data']['workflow_runs'][0]; + export type WorkflowJob = Endpoints['GET /repos/{owner}/{repo}/actions/jobs/{job_id}']['response']['data']; + export type WorkflowJobs = Endpoints['GET /repos/{owner}/{repo}/actions/runs/{run_id}/jobs']['response']['data']; } -export type Schema = { [key: string]: any, definitions: any[]; }; -export function mergeQuerySchemaWithShared(sharedSchema: Schema, schema: Schema) { +export function mergeQuerySchemaWithShared(sharedSchema: DocumentNode, schema: DocumentNode) { const sharedSchemaDefinitions = sharedSchema.definitions; const schemaDefinitions = schema.definitions; const mergedDefinitions = schemaDefinitions.concat(sharedSchemaDefinitions); @@ -82,4 +98,95 @@ export function mergeQuerySchemaWithShared(sharedSchema: Schema, schema: Schema) ...sharedSchema, definitions: mergedDefinitions }; -} \ No newline at end of file +} + +type RemoteAgentSuccessResult = { link: string; state: 'success'; number: number; webviewUri: Uri; llmDetails: string; sessionId: string }; +type RemoteAgentErrorResult = { error: string; innerError?: string; state: 'error' }; +export type RemoteAgentResult = RemoteAgentSuccessResult | RemoteAgentErrorResult; + +export interface IAPISessionLogs { + readonly info: SessionInfo; + readonly logs: string; + readonly setupSteps: SessionSetupStep[] | undefined; +} + +export interface ICopilotRemoteAgentCommandArgs { + userPrompt: string; + summary?: string; + source?: 'prompt' | (string & {}); + followup?: string; + _version?: number; // TODO(jospicer): Remove once stabilized/engine version enforced +} + +export interface ICopilotRemoteAgentCommandResponse { + uri: string; + title: string; + description: string; + author: string; + linkTag: string; +} + +export interface ToolCall { + function: { + arguments: string; + name: 'bash' | 'reply_to_comment' | (string & {}); + }; + id: string; + type: string; + index: number; +} + +export interface AssistantDelta { + content?: string; + role: 'assistant' | (string & {}); + tool_calls?: ToolCall[]; +} + +export interface Choice { + finish_reason?: 'tool_calls' | (string & {}); + delta: { + content?: string; + role: 'assistant' | (string & {}); + tool_calls?: ToolCall[]; + }; +} + +export interface RepoInfo { + owner: string; + repo: string; + baseRef: string; + remote: GitHubRemote; + repository: Repository; + ghRepository: GitHubRepository; + fm: FolderRepositoryManager; +} + +export function copilotEventToSessionStatus(event: TimelineEvent | undefined): ChatSessionStatus { + if (!event) { + return ChatSessionStatus.InProgress; + } + + switch (event.event) { + case EventType.CopilotStarted: + return ChatSessionStatus.InProgress; + case EventType.CopilotFinished: + return ChatSessionStatus.Completed; + case EventType.CopilotFinishedError: + return ChatSessionStatus.Failed; + default: + return ChatSessionStatus.InProgress; + } +} + +export function copilotPRStatusToSessionStatus(event: CopilotPRStatus): ChatSessionStatus { + switch (event) { + case CopilotPRStatus.Started: + return ChatSessionStatus.InProgress; + case CopilotPRStatus.Completed: + return ChatSessionStatus.Completed; + case CopilotPRStatus.Failed: + return ChatSessionStatus.Failed; + default: + return ChatSessionStatus.InProgress; + } +} diff --git a/src/github/conflictResolutionCoordinator.ts b/src/github/conflictResolutionCoordinator.ts index 8878ae52d9..a0bb0e8bbc 100644 --- a/src/github/conflictResolutionCoordinator.ts +++ b/src/github/conflictResolutionCoordinator.ts @@ -5,6 +5,8 @@ import * as buffer from 'buffer'; import * as vscode from 'vscode'; +import { Conflict, ConflictResolutionModel } from './conflictResolutionModel'; +import { GitHubRepository } from './githubRepository'; import { commands, contexts } from '../common/executeCommands'; import { Disposable } from '../common/lifecycle'; import { ITelemetry } from '../common/telemetry'; @@ -12,8 +14,6 @@ import { Schemes } from '../common/uri'; import { asPromise } from '../common/utils'; import { ConflictResolutionTreeView } from '../view/conflictResolution/conflictResolutionTreeView'; import { GitHubContentProvider } from '../view/gitHubContentProvider'; -import { Conflict, ConflictResolutionModel } from './conflictResolutionModel'; -import { GitHubRepository } from './githubRepository'; interface MergeEditorInputData { uri: vscode.Uri; title?: string; detail?: string; description?: string } const ORIGINAL_FILE = diff --git a/src/github/copilotApi.ts b/src/github/copilotApi.ts new file mode 100644 index 0000000000..499ba2a253 --- /dev/null +++ b/src/github/copilotApi.ts @@ -0,0 +1,462 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import fetch from 'cross-fetch'; +import JSZip from 'jszip'; +import * as vscode from 'vscode'; +import { CredentialStore, GitHub } from './credentials'; +import { PRType } from './interface'; +import { LoggingOctokit } from './loggingOctokit'; +import { PullRequestModel } from './pullRequestModel'; +import { RepositoriesManager } from './repositoriesManager'; +import { hasEnterpriseUri } from './utils'; +import { AuthProvider } from '../common/authentication'; +import { COPILOT_SWE_AGENT } from '../common/copilot'; +import Logger from '../common/logger'; +import { ITelemetry } from '../common/telemetry'; + +const LEARN_MORE_URL = 'https://aka.ms/coding-agent-docs'; +const PREMIUM_REQUESTS_URL = 'https://docs.github.com/en/copilot/concepts/copilot-billing/understanding-and-managing-requests-in-copilot#what-are-premium-requests'; +// https://github.com/github/sweagentd/blob/59e7d9210ca3ebba029918387e525eea73cb1f4a/internal/problemstatement/problemstatement.go#L36-L53 +export const MAX_PROBLEM_STATEMENT_LENGTH = 30_000 - 50; // 50 character buffer +// https://github.com/github/sweagentd/blob/0ad8f81a9c64754cb8a83d10777de4638bba1a6e/docs/adr/0001-create-job-api.md#post-jobsownerrepo---create-job-task +const JOBS_API_VERSION = 'v1'; + +export interface RemoteAgentJobPayload { + problem_statement: string; + event_type: string; + pull_request?: { + title?: string; + body_placeholder?: string; + body_suffix?: string; + base_ref?: string; + head_ref?: string; + }; + run_name?: string; +} + +export interface RemoteAgentJobResponse { + job_id: string; + session_id: string; + actor: { + id: number; + login: string; + }; + created_at: string; + updated_at: string; +} + +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'; + + constructor( + private octokit: LoggingOctokit, + private token: string, + private credentialStore: CredentialStore, + private telemetry: ITelemetry + ) { } + + private get baseUrl(): string { + return 'https://api.githubcopilot.com'; + } + + private async makeApiCallFullUrl(url: string, init: RequestInit): Promise { + const apiCall = () => fetch(url, init); + return this.octokit.call(apiCall); + } + private async makeApiCall(api: string, init: RequestInit): Promise { + return this.makeApiCallFullUrl(`${this.baseUrl}${api}`, init); + } + + private get userAgent(): string { + const extensionVersion = vscode.extensions.getExtension('GitHub.vscode-pull-request-github')?.packageJSON.version ?? 'unknown'; + return `vscode-pull-request-github/${extensionVersion}`; + } + + async postRemoteAgentJob( + owner: string, + name: string, + payload: RemoteAgentJobPayload, + isTruncated: boolean, + ): Promise { + const repoSlug = `${owner}/${name}`; + const apiUrl = `/agents/swe/${JOBS_API_VERSION}/jobs/${repoSlug}`; + let status: number | undefined; + + const problemStatementLength = payload.problem_statement.length.toString(); + const payloadJson = JSON.stringify(payload); + const payloadLength = payloadJson.length.toString(); + Logger.trace(`postRemoteAgentJob: Posting job to ${apiUrl} with payload: ${JSON.stringify(payload)}`, CopilotApi.ID); + try { + const response = await this.makeApiCall(apiUrl, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${this.token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': this.userAgent, + }, + body: payloadJson + }); + + status = response.status; + if (!response.ok) { + throw new Error(await this.formatRemoteAgentJobError(status, repoSlug, response)); + } + + const data = await response.json(); + this.validateRemoteAgentJobResponse(data); + /* + __GDPR__ + "remoteAgent.postRemoteAgentJob" : { + "status" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "payloadLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "problemStatementLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "isTruncated": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetry.sendTelemetryEvent('remoteAgent.postRemoteAgentJob', { + status: status.toString(), + payloadLength, + problemStatementLength, + isTruncated: isTruncated.toString(), + }); + return data; + } catch (error) { + /* __GDPR__ + "remoteAgent.postRemoteAgentJob" : { + "status" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "payloadLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "problemStatementLength": { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "isTruncated": { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetry.sendTelemetryErrorEvent('remoteAgent.postRemoteAgentJob', { + status: status?.toString() || '999', + payloadLength, + problemStatementLength, + isTruncated: isTruncated.toString(), + }); + throw error; + } + } + + // https://github.com/github/sweagentd/blob/371ea6db280b9aecf790ccc20660e39a7ecb8d1c/internal/api/jobapi/handler.go#L110-L120 + private async formatRemoteAgentJobError(status: number, repoSlug: string, response: Response): Promise { + Logger.error(`Error in remote agent job: ${await response.text()}`, CopilotApi.ID); + switch (status) { + case 400: + return vscode.l10n.t('Bad request'); + case 401: + return vscode.l10n.t('Unauthorized'); + case 402: + return vscode.l10n.t('[Premium request]({0}) quota exceeded', PREMIUM_REQUESTS_URL); + case 403: + return vscode.l10n.t('[GitHub coding agent]({0}) is not enabled for repository \'{1}\'', LEARN_MORE_URL, repoSlug); + case 404: + return vscode.l10n.t('Repository \'{0}\' not found', repoSlug); + case 409: + return vscode.l10n.t('A coding agent pull request already exists'); + case 500: + return vscode.l10n.t('Server error. Please see logs for details.'); + default: + return vscode.l10n.t('Error: {0}. Please see logs for details', status); + } + } + + private validateRemoteAgentJobResponse(data: any): asserts data is RemoteAgentJobResponse { + if (!data || typeof data !== 'object') { + throw new Error('Invalid response from coding agent'); + } + if (typeof data.job_id !== 'string') { + throw new Error('Invalid job_id in response'); + } + if (typeof data.session_id !== 'string') { + throw new Error('Invalid session_id in response'); + } + if (!data.actor || typeof data.actor !== 'object') { + throw new Error('Invalid actor in response'); + } + if (typeof data.actor.id !== 'number') { + throw new Error('Invalid actor.id in response'); + } + if (typeof data.actor.login !== 'string') { + throw new Error('Invalid actor.login in response'); + } + if (typeof data.created_at !== 'string') { + throw new Error('Invalid created_at in response'); + } + if (typeof data.updated_at !== 'string') { + throw new Error('Invalid updated_at in response'); + } + } + + public async getLogsFromZipUrl(logsUrl: string): Promise { + const logsZip = await this.makeApiCallFullUrl(logsUrl, { + headers: { + Authorization: `Bearer ${this.token}`, + Accept: 'application/json', + }, + }); + if (!logsZip.ok) { + throw new Error(`Failed to fetch logs zip: ${logsZip.statusText}`); + } + const logsText = await logsZip.arrayBuffer(); + const copilotSteps: string[] = []; + const zip = await JSZip.loadAsync(logsText); + for (const fileName of Object.keys(zip.files)) { + const file = zip.files[fileName]; + if (!file.dir && fileName.endsWith('Processing Request.txt')) { + const content = await file.async('string'); + copilotSteps.push(...content.split('\n')); + } + } + return copilotSteps; + } + + public async getAllSessions(pullRequestId: number | undefined): Promise { + const response = await this.makeApiCall( + pullRequestId + ? `/agents/sessions/resource/pull/${pullRequestId}` + : `/agents/sessions`, + { + headers: { + Authorization: `Bearer ${this.token}`, + Accept: 'application/json', + }, + }); + if (!response.ok) { + await this.handleApiError(response, 'getAllSessions'); + } + const sessions = await response.json(); + return sessions.sessions; + } + + public async getAllCodingAgentPRs(repositoriesManager: RepositoriesManager): Promise { + const hub = this.getHub(); + const username = (await hub?.currentUser)?.login; + if (!username) { + Logger.error('Failed to get GitHub username from auth provider', CopilotApi.ID); + return []; + } + const query = `is:open author:${COPILOT_SWE_AGENT}[bot] assignee:${username} is:pr repo:\${owner}/\${repository}`; + const allItems = await Promise.all( + repositoriesManager.folderManagers.map(async fm => { + const result = await fm.getPullRequests(PRType.Query, undefined, query); + return result.items; + }) + ); + return allItems.flat(); + } + + public async getSessionInfo(sessionId: string): Promise { + const response = await this.makeApiCall(`/agents/sessions/${sessionId}`, { + method: 'GET', + headers: { + Authorization: `Bearer ${this.token}`, + 'Accept': 'application/json' + } + }); + if (!response.ok) { + await this.handleApiError(response, 'getSessionInfo'); + } + + return (await response.json()) as SessionInfo; + } + + public async getLogsFromSession(sessionId: string): Promise { + const logsResponse = await this.makeApiCall(`/agents/sessions/${sessionId}/logs`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${this.token}`, + 'Content-Type': 'application/json', + }, + }); + if (!logsResponse.ok) { + await this.handleApiError(logsResponse, 'getLogsFromSession'); + } + return await logsResponse.text(); + } + + public async getJobByJobId(owner: string, repo: string, jobId: string): Promise { + try { + const response = await this.makeApiCall(`/agents/swe/v1/jobs/${owner}/${repo}/${jobId}`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${this.token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': this.userAgent, + } + }); + if (!response.ok) { + Logger.warn(`Failed to fetch job info for job ${jobId}: ${response.statusText}`, CopilotApi.ID); + return; + } + const data = await response.json() as JobInfo; + return data; + } catch (error) { + Logger.warn(`Error fetching job info for job ${jobId}: ${error}`, CopilotApi.ID); + return; + } + } + + public async getJobBySessionId(owner: string, repo: string, sessionId: string): Promise { + try { + const response = await this.makeApiCall(`/agents/swe/${JOBS_API_VERSION}/jobs/${owner}/${repo}/session/${sessionId}`, { + method: 'GET', + headers: { + 'Authorization': `Bearer ${this.token}`, + 'Content-Type': 'application/json', + 'Accept': 'application/json', + 'User-Agent': this.userAgent, + } + }); + if (!response.ok) { + Logger.warn(`Failed to fetch job info for session ${sessionId}: ${response.statusText}`, CopilotApi.ID); + return; + } + const data = await response.json() as JobInfo; + return data; + } catch (error) { + Logger.warn(`Error fetching job info for session ${sessionId}: ${error}`, CopilotApi.ID); + return; + } + } + + private getHub(): GitHub | undefined { + let authProvider: AuthProvider | undefined; + if (this.credentialStore.isAuthenticated(AuthProvider.githubEnterprise) && hasEnterpriseUri()) { + authProvider = AuthProvider.githubEnterprise; + } else if (this.credentialStore.isAuthenticated(AuthProvider.github)) { + authProvider = AuthProvider.github; + } else { + return; + } + + return this.credentialStore.getHub(authProvider); + } + + private async handleApiError(response: Response, action: string): Promise { + let errorBody: string | undefined = undefined; + try { + errorBody = await response.text(); + } catch (e) { /* ignore */ } + const msg = `'${action}' failed with ${response.statusText} ${errorBody ? `: ${errorBody}` : ''}`; + Logger.error(msg, CopilotApi.ID); + + /* __GDPR__ + "remoteAgent.apiError" : { + "action" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "status" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "body" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetry.sendTelemetryErrorEvent('remoteAgent.apiError', { + action, + status: response.status.toString(), + body: errorBody || '', + }); + + throw new Error(msg); + } +} + + +export interface SessionInfo { + id: string; + name: string; + user_id: number; + agent_id: number; + logs: string; + logs_blob_id: string; + state: 'completed' | 'in_progress' | 'failed' | 'queued'; + owner_id: number; + repo_id: number; + resource_type: string; + resource_id: number; + last_updated_at: string; + created_at: string; + completed_at: string; + event_type: string; + workflow_run_id: number; + premium_requests: number; + error: string | null; +} + +export interface SessionSetupStep { + name: string; + status: 'completed' | 'in_progress' | 'queued'; +} + +export interface JobInfo { + job_id: string; + session_id: string; + problem_statement: string; + content_filter_mode?: string; + status: string; + result?: string; + actor: { + id: number; + login: string; + }; + created_at: string; + updated_at: string; + pull_request: { + id: number; + number: number; + }; + workflow_run?: { + id: number; + }; + error?: { + message: string; + }; + event_type?: string; + event_url?: string; + event_identifiers?: string[]; +} + +export async function getCopilotApi(credentialStore: CredentialStore, telemetry: ITelemetry, authProvider?: AuthProvider): Promise { + if (!authProvider) { + if (credentialStore.isAuthenticated(AuthProvider.githubEnterprise) && hasEnterpriseUri()) { + authProvider = AuthProvider.githubEnterprise; + } else if (credentialStore.isAuthenticated(AuthProvider.github)) { + authProvider = AuthProvider.github; + } else { + return; + } + } + + const github = credentialStore.getHub(authProvider); + if (!github || !github.octokit) { + return; + } + + const { token } = await github.octokit.api.auth() as { token: string }; + return new CopilotApi(github.octokit, token, credentialStore, telemetry); +} \ No newline at end of file diff --git a/src/github/copilotPrWatcher.ts b/src/github/copilotPrWatcher.ts new file mode 100644 index 0000000000..3382ac6825 --- /dev/null +++ b/src/github/copilotPrWatcher.ts @@ -0,0 +1,294 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { GithubItemStateEnum } from './interface'; +import { PullRequestModel } from './pullRequestModel'; +import { PullRequestOverviewPanel } from './pullRequestOverview'; +import { RepositoriesManager } from './repositoriesManager'; +import { debounce } from '../common/async'; +import { COPILOT_ACCOUNTS } from '../common/comment'; +import { COPILOT_LOGINS, copilotEventToStatus, CopilotPRStatus } from '../common/copilot'; +import { Disposable } from '../common/lifecycle'; +import { PR_SETTINGS_NAMESPACE, QUERIES } from '../common/settingKeys'; +import { PrsTreeModel } from '../view/prsTreeModel'; + +export function isCopilotQuery(query: string): boolean { + const lowerQuery = query.toLowerCase(); + return COPILOT_LOGINS.some(login => lowerQuery.includes(`author:${login.toLowerCase()}`)); +} + +export function getCopilotQuery(): string | undefined { + const queries = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<{ label: string; query: string }[]>(QUERIES, []); + return queries.find(query => isCopilotQuery(query.query))?.query; +} + +export interface CodingAgentPRAndStatus { + item: PullRequestModel; + status: CopilotPRStatus; +} + +export class CopilotStateModel extends Disposable { + public static ID = 'CopilotStateModel'; + private _isInitialized = false; + private readonly _states: Map = new Map(); + private readonly _showNotification: Set = new Set(); + private readonly _onDidChangeStates = this._register(new vscode.EventEmitter()); + readonly onDidChangeCopilotStates = this._onDidChangeStates.event; + private readonly _onDidChangeNotifications = this._register(new vscode.EventEmitter()); + readonly onDidChangeCopilotNotifications = this._onDidChangeNotifications.event; + + makeKey(owner: string, repo: string, prNumber?: number): string { + if (prNumber === undefined) { + return `${owner}/${repo}`; + } + return `${owner}/${repo}#${prNumber}`; + } + + deleteKey(key: string): void { + if (this._states.has(key)) { + const item = this._states.get(key)!; + this._states.delete(key); + if (this._showNotification.has(key)) { + this._showNotification.delete(key); + this._onDidChangeNotifications.fire([item.item]); + } + this._onDidChangeStates.fire(); + } + } + + set(statuses: CodingAgentPRAndStatus[]): void { + const changedModels: PullRequestModel[] = []; + const changedKeys: string[] = []; + for (const { item, status } of statuses) { + const key = this.makeKey(item.remote.owner, item.remote.repositoryName, item.number); + const currentStatus = this._states.get(key); + if (currentStatus?.status === status) { + continue; + } + this._states.set(key, { item, status }); + changedModels.push(item); + changedKeys.push(key); + } + if (changedModels.length > 0) { + if (this._isInitialized) { + changedKeys.forEach(key => this._showNotification.add(key)); + this._onDidChangeNotifications.fire(changedModels); + } + this._onDidChangeStates.fire(); + } + } + + get(owner: string, repo: string, prNumber: number): CopilotPRStatus { + const key = this.makeKey(owner, repo, prNumber); + return this._states.get(key)?.status ?? CopilotPRStatus.None; + } + + keys(): string[] { + return Array.from(this._states.keys()); + } + + clearNotification(owner: string, repo: string, prNumber: number): void { + const key = this.makeKey(owner, repo, prNumber); + if (this._showNotification.has(key)) { + this._showNotification.delete(key); + const item = this._states.get(key)?.item; + if (item) { + this._onDidChangeNotifications.fire([item]); + } + } + } + + clearAllNotifications(owner?: string, repo?: string): void { + if (this._showNotification.size > 0) { + const items: PullRequestModel[] = []; + + // If owner and repo are specified, only clear notifications for that repo + if (owner && repo) { + const keysToRemove: string[] = []; + const prefix = `${this.makeKey(owner, repo)}#`; + for (const key of this._showNotification.keys()) { + if (key.startsWith(prefix)) { + const item = this._states.get(key)?.item; + if (item) { + items.push(item); + } + keysToRemove.push(key); + } + } + keysToRemove.forEach(key => this._showNotification.delete(key)); + } else { + // Clear all notifications + for (const key of this._showNotification.keys()) { + const item = this._states.get(key)?.item; + if (item) { + items.push(item); + } + } + this._showNotification.clear(); + } + + if (items.length > 0) { + this._onDidChangeNotifications.fire(items); + } + } + } + + get notifications(): ReadonlySet { + return this._showNotification; + } + + getNotificationsCount(owner: string, repo: string): number { + let total = 0; + const partialKey = `${this.makeKey(owner, repo)}#`; + for (const state of this._showNotification.values()) { + if (state.startsWith(partialKey)) { + total++; + } + } + return total; + } + + setInitialized() { + this._isInitialized = true; + } + + get isInitialized(): boolean { + return this._isInitialized; + } + + getCounts(owner: string, repo: string): { total: number; inProgress: number; error: number } { + let inProgressCount = 0; + let errorCount = 0; + + for (const state of this._states.values()) { + if (state.item.remote.owner !== owner || state.item.remote.repositoryName !== repo) { + continue; + } + if (state.status === CopilotPRStatus.Started) { + inProgressCount++; + } else if (state.status === CopilotPRStatus.Failed) { + errorCount++; + } + } + + return { + total: this._states.size, + inProgress: inProgressCount, + error: errorCount + }; + } + + get all(): CodingAgentPRAndStatus[] { + return Array.from(this._states.values()); + } +} + +export class CopilotPRWatcher extends Disposable { + private readonly _model: CopilotStateModel; + + constructor(private readonly _reposManager: RepositoriesManager, private readonly _prsTreeModel: PrsTreeModel) { + super(); + this._model = _prsTreeModel.copilotStateModel; + if (this._reposManager.folderManagers.length === 0) { + const initDisposable = this._reposManager.onDidChangeAnyGitHubRepository(() => { + initDisposable.dispose(); + this._initialize(); + }); + } else { + this._initialize(); + } + } + + private _initialize() { + this._prsTreeModel.refreshCopilotStateChanges(true); + this._pollForChanges(); + const updateFullState = debounce(() => this._prsTreeModel.refreshCopilotStateChanges(true), 50); + this._register(this._reposManager.onDidChangeAnyPullRequests(e => { + if (e.some(pr => COPILOT_ACCOUNTS[pr.model.author.login])) { + if (!this._model.isInitialized) { + return; + } + if (e.some(pr => this._model.get(pr.model.remote.owner, pr.model.remote.repositoryName, pr.model.number) === CopilotPRStatus.None)) { + // A PR we don't know about was updated + updateFullState(); + } else { + for (const pr of e) { + if (pr.model instanceof PullRequestModel) { + this._updateSingleState(pr.model); + } + } + } + } + })); + this._register(PullRequestOverviewPanel.onVisible(e => this._model.clearNotification(e.remote.owner, e.remote.repositoryName, e.number))); + + this._register(vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(`${PR_SETTINGS_NAMESPACE}.${QUERIES}`)) { + this._pollForChanges(); + } + })); + this._register(vscode.window.onDidChangeWindowState(e => { + if (e.active || e.focused) { + // If we are becoming active/focused, and it's been more than the poll interval since the last poll, poll now + if (Date.now() - this._lastPollTime > this._pollInterval) { + this._pollForChanges(); + } + } + })); + this._register({ dispose: () => this._pollTimeout && clearTimeout(this._pollTimeout) }); + } + + private get _pollInterval(): number { + if (vscode.window.state.active || vscode.window.state.focused) { + return 60 * 1000 * 2; // Poll every 2 minutes + } + return 60 * 1000 * 5; // Poll every 5 minutes + } + + private _pollTimeout: NodeJS.Timeout | undefined; + private _lastPollTime = 0; + private async _pollForChanges(): Promise { + if (this._pollTimeout) { + clearTimeout(this._pollTimeout); + this._pollTimeout = undefined; + } + this._lastPollTime = Date.now(); + const shouldContinue = await this._prsTreeModel.refreshCopilotStateChanges(true); + + if (shouldContinue) { + this._pollTimeout = setTimeout(() => { + this._pollForChanges(); + }, this._pollInterval); + } + } + + private async _updateSingleState(pr: PullRequestModel): Promise { + const changes: CodingAgentPRAndStatus[] = []; + + const copilotEvents = await pr.getCopilotTimelineEvents(false, !this._model.isInitialized); + let latestEvent = copilotEventToStatus(copilotEvents[copilotEvents.length - 1]); + if (latestEvent === CopilotPRStatus.None) { + if (!COPILOT_ACCOUNTS[pr.author.login]) { + return; + } + latestEvent = CopilotPRStatus.Started; + } + + if (pr.state !== GithubItemStateEnum.Open) { + // PR has been closed or merged, time to remove it. + const key = this._model.makeKey(pr.remote.owner, pr.remote.repositoryName, pr.number); + this._model.deleteKey(key); + return; + } + + const lastStatus = this._model.get(pr.remote.owner, pr.remote.repositoryName, pr.number) ?? CopilotPRStatus.None; + if (latestEvent !== lastStatus) { + changes.push({ item: pr, status: latestEvent }); + } + this._model.set(changes); + } + +} \ No newline at end of file diff --git a/src/github/copilotRemoteAgent.ts b/src/github/copilotRemoteAgent.ts new file mode 100644 index 0000000000..8b2f53540b --- /dev/null +++ b/src/github/copilotRemoteAgent.ts @@ -0,0 +1,1685 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +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'; +import { ChatSessionWithPR, CopilotApi, getCopilotApi, JobInfo, RemoteAgentJobPayload, SessionInfo, SessionSetupStep } from './copilotApi'; +import { CodingAgentPRAndStatus, CopilotPRWatcher } from './copilotPrWatcher'; +import { parseSessionLogs, parseToolCallDetails, StrReplaceEditorToolData } from '../../common/sessionParsing'; +import { GitApiImpl } from '../api/api1'; +import { COPILOT_ACCOUNTS } from '../common/comment'; +import { CopilotRemoteAgentConfig } from '../common/config'; +import { COPILOT_CLOUD_AGENT, COPILOT_LOGINS, COPILOT_SWE_AGENT } from '../common/copilot'; +import { commands } from '../common/executeCommands'; +import { Disposable } from '../common/lifecycle'; +import Logger from '../common/logger'; +import { GitHubRemote } from '../common/remote'; +import { CODING_AGENT, CODING_AGENT_AUTO_COMMIT_AND_PUSH } from '../common/settingKeys'; +import { ITelemetry } from '../common/telemetry'; +import { toOpenPullRequestWebviewUri } from '../common/uri'; +import { ChatSessionContentBuilder } from './copilotRemoteAgent/chatSessionContentBuilder'; +import { GitOperationsManager } from './copilotRemoteAgent/gitOperationsManager'; +import { extractTitle, formatBodyPlaceholder, truncatePrompt } from './copilotRemoteAgentUtils'; +import { CredentialStore } from './credentials'; +import { FolderRepositoryManager, ReposManagerState } from './folderRepositoryManager'; +import { GitHubRepository } from './githubRepository'; +import { issueMarkdown, PlainTextRenderer } from './markdownUtils'; +import { PullRequestModel } from './pullRequestModel'; +import { chooseItem } from './quickPicks'; +import { RepositoriesManager } from './repositoriesManager'; +import { getRepositoryForFile } from './utils'; +import { PrsTreeModel } from '../view/prsTreeModel'; + +const LEARN_MORE = vscode.l10n.t('Learn about coding agent'); +// Without Pending Changes +const CONTINUE = vscode.l10n.t('Continue'); +// With Pending Changes +const PUSH_CHANGES = vscode.l10n.t('Include changes'); +const CONTINUE_WITHOUT_PUSHING = vscode.l10n.t('Ignore changes'); +const CONTINUE_AND_DO_NOT_ASK_AGAIN = vscode.l10n.t('Continue and don\'t ask again'); + +const CONTINUE_TRUNCATION = vscode.l10n.t('Continue with truncation'); +const DELEGATE_MODAL_DETAILS = vscode.l10n.t('The agent will work asynchronously to create a pull request with your requested changes.'); +const DISABLE_CODELENS = vscode.l10n.t('Disable Code Lens'); + +const COPILOT = '@copilot'; + +const body_suffix = vscode.l10n.t('Created from VS Code via the [GitHub Pull Request](https://marketplace.visualstudio.com/items?itemName=GitHub.vscode-pull-request-github) extension.'); + +const PREFERRED_GITHUB_CODING_AGENT_REMOTE_WORKSPACE_KEY = 'PREFERRED_GITHUB_CODING_AGENT_REMOTE'; + + +export namespace SessionIdForPr { + + const prefix = 'pull-session-by-index'; + + export function getResource(prNumber: number, sessionIndex: number): vscode.Uri { + return vscode.Uri.from({ + scheme: COPILOT_CLOUD_AGENT, path: `/${prefix}-${prNumber}-${sessionIndex}`, + }); + } + + 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), + sessionIndex: parseInt(match[2], 10) + }; + } + return undefined; + } +} + +type ConfirmationResult = { step: string; accepted: boolean; metadata?: CreatePromptMetadata /* | SomeOtherMetadata */ }; + +interface CreatePromptMetadata { + prompt: string; + history?: string; + references?: ChatPromptReference[]; +} + +export class CopilotRemoteAgentManager extends Disposable { + async chatParticipantImpl(request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken) { + const startSession = async (source: string, prompt: string, history?: string, references?: readonly vscode.ChatPromptReference[]) => { + /* __GDPR__ + "copilot.remoteagent.editor.invoke" : { + "promptLength" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "historyLength" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "referencesCount" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "source" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetry.sendTelemetryEvent('copilot.remoteagent.editor.invoke', { + promptLength: prompt.length.toString() ?? '0', + historyLength: history?.length.toString() ?? '0', + referencesCount: references?.length.toString() ?? '0', + source, + }); + const result = await this.invokeRemoteAgent( + prompt, + [ + this.extractFileReferences(references), + history + ].join('\n\n').trim(), + token, + false, + stream, + ); + if (result.state !== 'success') { + Logger.error(`Failed to provide new chat session item: ${result.error}${result.innerError ? `\nInner Error: ${result.innerError}` : ''}`, CopilotRemoteAgentManager.ID); + stream.warning(result.error); + return; + } + return result.number; + }; + + const handleConfirmationData = async () => { + const results: ConfirmationResult[] = []; + results.push(...(request.acceptedConfirmationData?.map(data => ({ step: data.step, accepted: true, metadata: data?.metadata })) ?? [])); + results.push(...((request.rejectedConfirmationData ?? []).filter(data => !results.some(r => r.step === data.step)).map(data => ({ step: data.step, accepted: false, metadata: data?.metadata })))); + for (const data of results) { + switch (data.step) { + case 'create': + if (!data.accepted) { + stream.markdown(vscode.l10n.t('Coding agent request cancelled.')); + return {}; + } + const { prompt, history, references } = data.metadata as CreatePromptMetadata; + const number = await startSession('chat', prompt, history, references); + if (!number) { + return {}; + } + const pullRequest = await this.findPullRequestById(number, true); + if (!pullRequest) { + stream.warning(vscode.l10n.t('Could not find the associated pull request {0} for this chat session.', number)); + return {}; + } + const uri = await toOpenPullRequestWebviewUri({ owner: pullRequest.remote.owner, repo: pullRequest.remote.repositoryName, pullRequestNumber: pullRequest.number }); + const plaintextBody = marked.parse(pullRequest.body, { renderer: new PlainTextRenderer(true), smartypants: true }).trim(); + 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.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`); + break; + } + } + return {}; + }; + + if (request.acceptedConfirmationData || request.rejectedConfirmationData) { + return await handleConfirmationData(); + } + + if (context.chatSessionContext?.isUntitled) { + /* Generate new coding agent session from an 'untitled' session */ + const number = await startSession( + 'untitledChatSession', + request.prompt, + undefined, + request.references + ); + if (!number) { + return {}; + } + // Tell UI to the new chat session + 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 */ + try { + if (token.isCancellationRequested) { + return {}; + } + + // Validate user input + const userPrompt = request.prompt; + if (!userPrompt || userPrompt.trim().length === 0) { + stream.markdown(vscode.l10n.t('Please provide a message for the coding agent.')); + return {}; + } + + stream.progress(vscode.l10n.t('Preparing')); + + 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.resource.toString())); + return {}; + } + + stream.progress(vscode.l10n.t('Delegating request to coding agent')); + + // Add follow-up comment to the PR + const result = await this.addFollowUpToExistingPR(pullRequest.number, userPrompt); + if (!result) { + stream.markdown(vscode.l10n.t('Failed to add follow-up comment to the pull request.')); + return {}; + } + + // Show initial success message + stream.markdown(result); + stream.markdown('\n\n'); + + stream.progress(vscode.l10n.t('Attaching to session')); + + // Wait for new session and stream its progress + const newSession = await this.waitForNewSession(pullRequest, stream, token, true); + if (!newSession) { + return {}; + } + + // Stream the new session logs + stream.markdown(vscode.l10n.t('Coding agent has begun work on your request')); + stream.markdown('\n\n'); + + await this.streamSessionLogs(stream, pullRequest, newSession.id, token); + + return {}; + } catch (error) { + Logger.error(`Error in request handler: ${error}`, CopilotRemoteAgentManager.ID); + stream.markdown(vscode.l10n.t('An error occurred while processing your request.')); + return { errorDetails: { message: error.message } }; + } + } else { + /* @copilot invoked from a 'normal' chat or 'cloud button' */ + stream.confirmation( + vscode.l10n.t('Delegate to agent'), + DELEGATE_MODAL_DETAILS, + { + step: 'create', + metadata: { + prompt: request.prompt, + history: undefined, + references: request.references, + } + }, + ['Delegate', 'Cancel'] + ); + } + } + + public static ID = 'CopilotRemoteAgentManager'; + + private readonly _onDidChangeStates = this._register(new vscode.EventEmitter()); + readonly onDidChangeStates = this._onDidChangeStates.event; + private readonly _onDidChangeNotifications = this._register(new vscode.EventEmitter()); + readonly onDidChangeNotifications = this._onDidChangeNotifications.event; + private readonly _onDidCreatePullRequest = this._register(new vscode.EventEmitter()); + readonly onDidCreatePullRequest = this._onDidCreatePullRequest.event; + private readonly _onDidChangeChatSessions = this._register(new vscode.EventEmitter()); + readonly onDidChangeChatSessions = this._onDidChangeChatSessions.event; + private readonly _onDidCommitChatSession = this._register(new vscode.EventEmitter<{ original: ChatSessionItem; modified: ChatSessionItem }>()); + readonly onDidCommitChatSession = this._onDidCommitChatSession.event; + + private readonly gitOperationsManager: GitOperationsManager; + private _isAssignable: boolean | undefined; + + constructor( + private credentialStore: CredentialStore, + public repositoriesManager: RepositoriesManager, + private telemetry: ITelemetry, + private context: vscode.ExtensionContext, + private gitAPI: GitApiImpl, + private readonly prsTreeModel: PrsTreeModel, + ) { + super(); + this.gitOperationsManager = new GitOperationsManager(CopilotRemoteAgentManager.ID); + this._register(this.credentialStore.onDidChangeSessions((e: vscode.AuthenticationSessionsChangeEvent) => { + if (e.provider.id === 'github') { + this._copilotApiPromise = undefined; // Invalidate cached session + } + })); + + this._register(new CopilotPRWatcher(this.repositoriesManager, this.prsTreeModel)); + this._register(this.prsTreeModel.onDidChangeCopilotStates(() => { + this._onDidChangeStates.fire(); + this._onDidChangeChatSessions.fire(); + })); + this._register(this.prsTreeModel.onDidChangeCopilotNotifications(items => this._onDidChangeNotifications.fire(items))); + + this._register(this.repositoriesManager.onDidChangeFolderRepositories((event) => { + if (event.added) { + this._register(event.added.onDidChangeAssignableUsers(() => { + this.updateAssignabilityContext(); + })); + } + this.updateAssignabilityContext(); + })); + this.repositoriesManager.folderManagers.forEach(manager => { + this._register(manager.onDidChangeAssignableUsers(() => { + this.updateAssignabilityContext(); + })); + }); + this._register(vscode.workspace.onDidChangeConfiguration((e) => { + if (e.affectsConfiguration(CODING_AGENT)) { + this.updateAssignabilityContext(); + } + })); + + // Set initial context + this.updateAssignabilityContext(); + } + + private _copilotApiPromise: Promise | undefined; + private get copilotApi(): Promise { + if (!this._copilotApiPromise) { + this._copilotApiPromise = this.initializeCopilotApi(); + } + return this._copilotApiPromise; + } + + private async initializeCopilotApi(): Promise { + return await getCopilotApi(this.credentialStore, this.telemetry); + } + + public get enabled(): boolean { + return CopilotRemoteAgentConfig.getEnabled(); + } + + public get autoCommitAndPushEnabled(): boolean { + return CopilotRemoteAgentConfig.getAutoCommitAndPushEnabled(); + } + + private _repoManagerInitializationPromise: Promise | undefined; + private async waitRepoManagerInitialization() { + if (this.repositoriesManager.state === ReposManagerState.RepositoriesLoaded || this.repositoriesManager.state === ReposManagerState.NeedsAuthentication) { + return; + } + + if (!this._repoManagerInitializationPromise) { + this._repoManagerInitializationPromise = new Promise((resolve) => { + const disposable = this.repositoriesManager.onDidChangeState(() => { + if (this.repositoriesManager.state === ReposManagerState.RepositoriesLoaded || this.repositoriesManager.state === ReposManagerState.NeedsAuthentication) { + disposable.dispose(); + resolve(); + } + }); + }); + } + + return this._repoManagerInitializationPromise; + } + + async isAssignable(): Promise { + const setCachedResult = (b: boolean) => { + this._isAssignable = b; + return b; + }; + + if (this._isAssignable !== undefined) { + return this._isAssignable; + } + + const repoInfo = await this.repoInfo(); + if (!repoInfo) { + return setCachedResult(false); + } + + const { fm } = repoInfo; + + try { + // Ensure assignable users are loaded + await fm.getAssignableUsers(); + const allAssignableUsers = fm.getAllAssignableUsers(); + + if (!allAssignableUsers) { + return setCachedResult(false); + } + return setCachedResult(allAssignableUsers.some(user => COPILOT_LOGINS.includes(user.login))); + } catch (error) { + // If there's an error fetching assignable users, assume not assignable + return setCachedResult(false); + } + } + + async isAvailable(): Promise { + // Check if the manager is enabled, copilot API is available, and it's assignable + if (!CopilotRemoteAgentConfig.getEnabled()) { + return false; + } + + if (!this.credentialStore.isAnyAuthenticated()) { + // If not signed in, then we optimistically say it's available. + return true; + } + + const repoInfo = await this.repoInfo(); + if (!repoInfo) { + return false; + } + + const copilotApi = await this.copilotApi; + if (!copilotApi) { + return false; + } + + return await this.isAssignable(); + } + + private async updateAssignabilityContext(): Promise { + try { + this._isAssignable = undefined; // Invalidate cache + const available = await this.isAvailable(); + commands.setContext('copilotCodingAgentAssignable', available); + } catch (error) { + // Presume false + commands.setContext('copilotCodingAgentAssignable', false); + } + } + + private firstFolderManager(): FolderRepositoryManager | undefined { + if (!this.repositoriesManager.folderManagers.length) { + return; + } + return this.repositoriesManager.folderManagers[0]; + } + + private chooseFolderManager(): Promise { + return chooseItem( + this.repositoriesManager.folderManagers, + itemValue => ({ label: pathLib.basename(itemValue.repository.rootUri.fsPath) }), + ); + } + + public async resetCodingAgentPreferences() { + await this.context.workspaceState.update(PREFERRED_GITHUB_CODING_AGENT_REMOTE_WORKSPACE_KEY, undefined); + } + + public async promptAndUpdatePreferredGitHubRemote(skipIfValueAlreadyCached = false): Promise { + if (skipIfValueAlreadyCached) { + const cachedValue = await this.context.workspaceState.get(PREFERRED_GITHUB_CODING_AGENT_REMOTE_WORKSPACE_KEY); + if (cachedValue) { + return; + } + } + + const fm = this.firstFolderManager(); + if (!fm) { + return; + } + + const ghRemotes = await fm.getAllGitHubRemotes(); + Logger.trace(`There are ${ghRemotes.length} GitHub remotes available to select from`, CopilotRemoteAgentManager.ID); + + if (!ghRemotes || ghRemotes.length <= 1) { + // Unexpected if we reach here, command should be hidden. + Logger.trace('No need to select a coding agent GitHub remote, skipping prompt', CopilotRemoteAgentManager.ID); + return; + } + + // Sort so that origin is first if it exists + ghRemotes.sort((a, b) => (a.remoteName === 'origin' ? -1 : b.remoteName === 'origin' ? 1 : 0)); + + const result = await chooseItem( + ghRemotes, + itemValue => ({ label: itemValue.remoteName, description: `(${itemValue.owner}/${itemValue.repositoryName})` }), + { + title: vscode.l10n.t('Coding agent will create pull requests against the selected remote.'), + } + ); + + if (!result) { + Logger.warn('No coding agent GitHub remote selected.', CopilotRemoteAgentManager.ID); + Logger.warn(`Keeping previous value of: ${this.context.workspaceState.get(PREFERRED_GITHUB_CODING_AGENT_REMOTE_WORKSPACE_KEY)}`, CopilotRemoteAgentManager.ID); + return; + } + + Logger.appendLine(`Updated '${result.remoteName}' as preferred coding agent remote`, CopilotRemoteAgentManager.ID); + await this.context.workspaceState.update(PREFERRED_GITHUB_CODING_AGENT_REMOTE_WORKSPACE_KEY, result.remoteName); + } + + async repoInfo(fm?: FolderRepositoryManager): Promise { + fm = fm || this.firstFolderManager(); + const repository = fm?.repository; + const ghRepository = fm?.gitHubRepositories.find(repo => repo.remote instanceof GitHubRemote) as GitHubRepository | undefined; + if (!fm || !repository || !ghRepository) { + return; + } + const baseRef = repository.state.HEAD?.name; // TODO: Consider edge cases + const preferredRemoteName = this.context.workspaceState.get(PREFERRED_GITHUB_CODING_AGENT_REMOTE_WORKSPACE_KEY); + const ghRemotes = await fm.getGitHubRemotes(); + if (!ghRemotes || ghRemotes.length === 0) { + return; + } + + const remote = + preferredRemoteName + ? ghRemotes.find(remote => remote.remoteName === preferredRemoteName) // Cached preferred value + : (ghRemotes.find(remote => remote.remoteName === 'origin') || ghRemotes[0]); // Fallback to the first remote + + if (!remote) { + Logger.error(`no valid remotes for coding agent`, CopilotRemoteAgentManager.ID); + // Clear preference, something is wrong + this.context.workspaceState.update(PREFERRED_GITHUB_CODING_AGENT_REMOTE_WORKSPACE_KEY, undefined); + return; + } + + // Extract repo data from target remote + const { owner, repositoryName: repo } = remote; + if (!owner || !repo || !baseRef || !repository) { + return; + } + return { owner, repo, baseRef, remote, repository, ghRepository, fm }; + } + + async addFollowUpToExistingPR(pullRequestNumber: number, userPrompt: string, summary?: string): Promise { + try { + const pr = await this.findPullRequestById(pullRequestNumber, true); + if (!pr) { + Logger.error(`Could not find pull request #${pullRequestNumber}`, CopilotRemoteAgentManager.ID); + return; + } + // Add a comment tagging @copilot with the user's prompt + const commentBody = `${COPILOT} ${userPrompt} \n\n --- \n\n ${summary ?? ''}`; + const commentResult = await pr.createIssueComment(commentBody); + if (!commentResult) { + Logger.error(`Failed to add comment to PR #${pullRequestNumber}`, CopilotRemoteAgentManager.ID); + return; + } + Logger.appendLine(`Added comment ${commentResult.htmlUrl}`, CopilotRemoteAgentManager.ID); + // allow-any-unicode-next-line + return vscode.l10n.t('🚀 Follow-up comment added to [#{0}]({1})', pullRequestNumber, commentResult.htmlUrl); + } catch (err) { + Logger.error(`Failed to add follow-up comment to PR #${pullRequestNumber}: ${err}`, CopilotRemoteAgentManager.ID); + return; + } + } + + async tryPromptForAuthAndRepo(): Promise { + const authResult = await this.credentialStore.tryPromptForCopilotAuth(); + if (!authResult) { + return undefined; + } + // Wait for repos to update + const fm = await this.chooseFolderManager(); + await fm?.updateRepositories(); + return fm; + } + + async commandImpl(args?: ICopilotRemoteAgentCommandArgs): Promise { + if (!args) { + return; + } + const { userPrompt, summary, source, followup, _version } = args; + const fm = await this.tryPromptForAuthAndRepo(); + if (!fm) { + return; + } + + /* __GDPR__ + "remoteAgent.command.args" : { + "source" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "isFollowup" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "userPromptLength" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "summaryLength" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "version" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetry.sendTelemetryEvent('remoteAgent.command.args', { + source: source?.toString() || 'unknown', + isFollowup: !!followup ? 'true' : 'false', + userPromptLength: userPrompt.length.toString(), + summaryLength: summary ? summary.length.toString() : '0', + version: _version?.toString() || 'unknown' + }); + + if (!userPrompt || userPrompt.trim().length === 0) { + return; + } + + const repoInfo = await this.repoInfo(fm); + if (!repoInfo) { + /* __GDPR__ + "remoteAgent.command.result" : { + "reason" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetry.sendTelemetryErrorEvent('remoteAgent.command.result', { reason: 'noRepositoryInfo' }); + return; + } + const { repository, owner, repo } = repoInfo; + + const repoName = `${owner}/${repo}`; + const hasChanges = repository.state.workingTreeChanges.length > 0 || repository.state.indexChanges.length > 0; + const learnMoreCb = async () => { + /* __GDPR__ + "remoteAgent.command.result" : { + "reason" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetry.sendTelemetryErrorEvent('remoteAgent.command.result', { reason: 'learnMore' }); + vscode.env.openExternal(vscode.Uri.parse('https://aka.ms/coding-agent-docs')); + }; + + let autoPushAndCommit = false; + const message = vscode.l10n.t('Copilot coding agent will continue your work in \'{0}\'.', repoName); + const detail = DELEGATE_MODAL_DETAILS; + if (source !== 'prompt' && hasChanges && CopilotRemoteAgentConfig.getAutoCommitAndPushEnabled()) { + + const buttons = [PUSH_CHANGES, CONTINUE_WITHOUT_PUSHING, LEARN_MORE]; + if (source === 'todo') { + buttons.push(DISABLE_CODELENS); + } + + const modalResult = await vscode.window.showInformationMessage( + message, + { + modal: true, + detail, + }, + ...buttons, + ); + + if (!modalResult) { + /* __GDPR__ + "remoteAgent.command.result" : { + "reason" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetry.sendTelemetryErrorEvent('remoteAgent.command.result', { reason: 'cancel' }); + return; + } + + if (modalResult === LEARN_MORE) { + learnMoreCb(); + return; + } + + if (modalResult === DISABLE_CODELENS) { + vscode.commands.executeCommand('workbench.action.openSettings', 'githubPullRequests.codingAgent.codeLens'); + return; + } + + if (modalResult === PUSH_CHANGES) { + autoPushAndCommit = true; + } + } else if (CopilotRemoteAgentConfig.getPromptForConfirmation()) { + // No pending changes modal + const modalResult = await vscode.window.showInformationMessage( + source !== 'prompt' ? message : vscode.l10n.t('Copilot coding agent will implement the specification outlined in this prompt file'), + { + modal: true, + detail: source !== 'prompt' ? detail : undefined + }, + CONTINUE, + CONTINUE_AND_DO_NOT_ASK_AGAIN, + LEARN_MORE, + ); + if (!modalResult) { + return; + } + + if (modalResult === CONTINUE_AND_DO_NOT_ASK_AGAIN) { + await CopilotRemoteAgentConfig.disablePromptForConfirmation(); + } + + if (modalResult === LEARN_MORE) { + learnMoreCb(); + return; + } + } + + const result = await this.invokeRemoteAgent( + userPrompt, + summary, + undefined, + autoPushAndCommit, + undefined, + fm + ); + + if (result.state !== 'success') { + /* __GDPR__ + "remoteAgent.command.result" : { + "reason" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetry.sendTelemetryErrorEvent('remoteAgent.command.result', { reason: 'invocationFailure' }); + vscode.window.showErrorMessage(result.error); + return; + } + + const { webviewUri, link, number } = result; + + /* __GDPR__ + "remoteAgent.command.success" : { + "source" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "hasFollowup" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "outcome" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetry.sendTelemetryEvent('remoteAgent.command.success', { + source: source || 'unknown', + hasFollowup: (!!followup).toString(), + outcome: 'success' + }); + + const viewLocationSetting = vscode.workspace.getConfiguration('chat').get('agentSessionsViewLocation'); + const pr = await (async () => { + const capi = await this.copilotApi; + if (!capi) { + return; + } + const sessions = await capi.getAllCodingAgentPRs(this.repositoriesManager); + return sessions.find(session => session.number === number); + })(); + + if (!viewLocationSetting || viewLocationSetting === 'disabled') { + vscode.commands.executeCommand('vscode.open', webviewUri); + } else { + await this.provideChatSessions(new vscode.CancellationTokenSource().token); + if (pr) { + vscode.commands.executeCommand('vscode.open', vscode.Uri.from({ scheme: COPILOT_SWE_AGENT, path: '/' + pr.number })); + } + } + + if (pr && (_version && _version === 2)) { /* version 2 means caller knows how to render this */ + const plaintextBody = marked.parse(pr.body, { renderer: new PlainTextRenderer(true), smartypants: true }).trim(); + + return { + uri: webviewUri.toString(), + title: pr.title, + description: plaintextBody, + author: COPILOT_ACCOUNTS[pr.author.login].name, + linkTag: `#${pr.number}` + }; + } + + // allow-any-unicode-next-line + return vscode.l10n.t('🚀 Coding agent will continue work in [#{0}]({1}). Track progress [here]({2}).', number, link, webviewUri.toString()); + } + + async invokeRemoteAgent(prompt: string, problemContext?: string, token?: vscode.CancellationToken, autoPushAndCommit = true, chatStream?: vscode.ChatResponseStream, fm?: FolderRepositoryManager): Promise { + const capiClient = await this.copilotApi; + if (!capiClient) { + return { error: vscode.l10n.t('Failed to initialize Copilot API. Please try again later.'), state: 'error' }; + } + + await this.promptAndUpdatePreferredGitHubRemote(true); + + const repoInfo = await this.repoInfo(fm); + if (!repoInfo) { + return { error: vscode.l10n.t('No repository information found. Please open a workspace with a GitHub repository.'), state: 'error' }; + } + const { owner, repo, remote, repository, ghRepository, baseRef } = repoInfo; + + // Check if user has permission to access the repository + try { + await ghRepository.octokit.api.repos.get({ owner, repo }); + } catch (error) { + if (error.status === 404 || error.status === 403) { + const currentUser = await this.credentialStore.getCurrentUser(remote.authProviderId); + return { + error: vscode.l10n.t( + 'Unable to access {0} as user {1}. Please check your permissions and try again.', + `\`${owner}/${repo}\``, + `\`${currentUser.login}\``, + ), + state: 'error', + }; + } + + // Re-throw other errors to be handled by the outer catch block + throw error; + } + + // Check if user has permission to assign Copilot in repository + if (!(await this.isAssignable())) { + return { + error: vscode.l10n.t( + 'Unable to assign GitHub Copilot coding agent in {0}. Please check your permissions and try again.', + `\`${owner}/${repo}\`` + ), + state: 'error', + }; + } + + // NOTE: This is as unobtrusive as possible with the current high-level APIs. + // We only create a new branch and commit if there are staged or working changes. + // This could be improved if we add lower-level APIs to our git extension (e.g. in-memory temp git index). + + const base_ref = baseRef; // This is the ref the PR will merge into + let head_ref: string | undefined; // This is the ref coding agent starts work from (omitted unless we push local changes) + const hasChanges = autoPushAndCommit && (repository.state.workingTreeChanges.length > 0 || repository.state.indexChanges.length > 0); + if (hasChanges) { + if (!CopilotRemoteAgentConfig.getAutoCommitAndPushEnabled()) { + return { error: vscode.l10n.t('Uncommitted changes detected. Please commit or stash your changes before starting the remote agent. Enable \'{0}\' to push your changes automatically.', CODING_AGENT_AUTO_COMMIT_AND_PUSH), state: 'error' }; + } + try { + chatStream?.progress(vscode.l10n.t('Waiting for local changes')); + head_ref = await this.gitOperationsManager.commitAndPushChanges(repoInfo); + } catch (error) { + return { error: vscode.l10n.t('Failed to commit and push changes. Please try again later.'), innerError: error.message, state: 'error' }; + } + } + + try { + if (!(await ghRepository.hasBranch(base_ref))) { + if (!CopilotRemoteAgentConfig.getAutoCommitAndPushEnabled()) { + // We won't auto-push a branch if the user has disabled the setting + return { error: vscode.l10n.t('The branch \'{0}\' does not exist on the remote repository \'{1}/{2}\'. Please create the remote branch first.', base_ref, owner, repo), state: 'error' }; + } + // Push the branch + Logger.appendLine(`Base ref needs to exist on remote. Auto pushing base_ref '${base_ref}' to remote repository '${owner}/${repo}'`, CopilotRemoteAgentManager.ID); + await repository.push(remote.remoteName, base_ref, true); + } + } catch (error) { + return { error: vscode.l10n.t('Failed to configure base branch \'{0}\' does not exist on the remote repository \'{1}/{2}\'. Please create the remote branch first.', base_ref, owner, repo), state: 'error' }; + } + + const title = extractTitle(prompt, problemContext); + const { problemStatement, isTruncated } = truncatePrompt(prompt, problemContext); + + if (isTruncated) { + chatStream?.progress(vscode.l10n.t('Truncating context')); + const truncationResult = await vscode.window.showWarningMessage( + vscode.l10n.t('Prompt size exceeded'), { modal: true, detail: vscode.l10n.t('Your prompt will be truncated to fit within coding agent\'s context window. This may affect the quality of the response.') }, CONTINUE_TRUNCATION); + const userCancelled = token?.isCancellationRequested || !truncationResult || truncationResult !== CONTINUE_TRUNCATION; + /* __GDPR__ + "remoteAgent.truncation" : { + "isCancelled" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetry.sendTelemetryEvent('remoteAgent.truncation', { + isCancelled: String(userCancelled), + }); + if (userCancelled) { + return { error: vscode.l10n.t('User cancelled due to truncation.'), state: 'error' }; + } + } + + const payload: RemoteAgentJobPayload = { + problem_statement: problemStatement, + event_type: 'visual_studio_code_remote_agent_tool_invoked', + pull_request: { + title, + body_placeholder: formatBodyPlaceholder(title), + base_ref, + body_suffix, + ...(head_ref && { head_ref }) + } + }; + + try { + chatStream?.progress(vscode.l10n.t('Delegating to coding agent')); + const response = await capiClient.postRemoteAgentJob(owner, repo, payload, isTruncated); + + // For v1 API, we need to fetch the job details to get the PR info + // Since the PR might not be created immediately, we need to poll for it + chatStream?.progress(vscode.l10n.t('Creating pull request')); + const jobInfo = await this.waitForJobWithPullRequest(capiClient, owner, repo, response.job_id, token); + if (!jobInfo || !jobInfo.pull_request) { + return { error: vscode.l10n.t('Failed to retrieve pull request information from job'), state: 'error' }; + } + + const { number } = jobInfo.pull_request; + + // Find the actual PR to get the HTML URL + const pullRequest = await this.findPullRequestById(number, true); + const htmlUrl = pullRequest?.html_url || `https://github.com/${owner}/${repo}/pull/${number}`; + + const webviewUri = await toOpenPullRequestWebviewUri({ owner, repo, pullRequestNumber: number }); + const prLlmString = `The remote agent has begun work and has created a pull request. Details about the pull request are being shown to the user. If the user wants to track progress or iterate on the agent's work, they should use the pull request.`; + + return { + state: 'success', + number, + link: htmlUrl, + webviewUri, + llmDetails: head_ref ? `Local pending changes have been pushed to branch '${head_ref}'. ${prLlmString}` : prLlmString, + sessionId: response.session_id + }; + } catch (error) { + return { error: vscode.l10n.t('Failed delegating to coding agent. Please try again later.'), innerError: error.message, state: 'error' }; + } + } + + async getSessionLogsFromAction(pullRequest: PullRequestModel) { + const capi = await this.copilotApi; + if (!capi) { + return []; + } + const lastRun = await this.getLatestCodingAgentFromAction(pullRequest); + if (!lastRun) { + return []; + } + + return await capi.getLogsFromZipUrl(lastRun.logs_url); + } + + async getWorkflowStepsFromAction(pullRequest: PullRequestModel): Promise { + const lastRun = await this.getLatestCodingAgentFromAction(pullRequest, 0, false); + if (!lastRun) { + return []; + } + + try { + const jobs = await pullRequest.githubRepository.getWorkflowJobs(lastRun.id); + const steps: SessionSetupStep[] = []; + + for (const job of jobs) { + if (job.steps) { + for (const step of job.steps) { + steps.push({ name: step.name, status: step.status }); + } + } + } + + return steps; + } catch (error) { + Logger.error(`Failed to get workflow steps: ${error}`, CopilotRemoteAgentManager.ID); + return []; + } + } + + async getLatestCodingAgentFromAction(pullRequest: PullRequestModel, sessionIndex = 0, completedOnly = true): Promise { + const capi = await this.copilotApi; + if (!capi) { + return; + } + const runs = await pullRequest.githubRepository.getWorkflowRunsFromAction(pullRequest.createdAt); + const workflowRuns = runs.flatMap(run => run.workflow_runs); + const padawanRuns = workflowRuns + .filter(run => run.path && run.path.startsWith(`dynamic/${COPILOT_SWE_AGENT}`)) + .filter(run => run.pull_requests?.some(pr => pr.id === pullRequest.id)); + + const session = padawanRuns.filter(s => !completedOnly || s.status === 'completed').at(sessionIndex); + if (!session) { + return; + } + + return this.getLatestRun(padawanRuns); + } + + async getSessionLogFromPullRequest(pullRequest: PullRequestModel, sessionIndex = 0, completedOnly = true): Promise { + const capi = await this.copilotApi; + if (!capi) { + return undefined; + } + + const sessions = await capi.getAllSessions(pullRequest.id); + const session = sessions.filter(s => !completedOnly || s.state === 'completed').at(sessionIndex); + if (!session) { + return undefined; + } + + const logs = await capi.getLogsFromSession(session.id); + + // If session is in progress, try to fetch workflow steps to show setup progress + let setupSteps: SessionSetupStep[] | undefined; + if (session.state === 'in_progress' || logs.trim().length === 0) { + try { + // Get workflow steps instead of logs + setupSteps = await this.getWorkflowStepsFromAction(pullRequest); + } catch (error) { + // If we can't fetch workflow steps, don't fail the entire request + Logger.warn(`Failed to fetch workflow steps for session ${session.id}: ${error}`, CopilotRemoteAgentManager.ID); + } + } + + return { info: session, logs, setupSteps }; + } + + async getSessionUrlFromPullRequest(pullRequest: PullRequestModel): Promise { + const capi = await this.copilotApi; + if (!capi) { + return; + } + + const sessions = await this.getLatestCodingAgentFromAction(pullRequest); + if (!sessions) { + return; + } + return sessions.html_url; + } + + private getLatestRun(runs: T[]): T { + return runs + .slice() + .sort((a, b) => { + const dateA = new Date(a.last_updated_at ?? a.updated_at ?? 0).getTime(); + const dateB = new Date(b.last_updated_at ?? b.updated_at ?? 0).getTime(); + return dateB - dateA; + })[0]; + } + + async extractHistory(history: ReadonlyArray): Promise { + if (!history) { + return; + } + const parts: string[] = []; + for (const turn of history) { + if (turn instanceof vscode.ChatRequestTurn) { + parts.push(`User: ${turn.prompt}`); + } else if (turn instanceof vscode.ChatResponseTurn) { + const textParts = turn.response + .filter(part => part instanceof vscode.ChatResponseMarkdownPart) + .map(part => part.value); + if (textParts.length > 0) { + parts.push(`Copilot: ${textParts.join('\n')}`); + } + } + } + const fullText = parts.join('\n'); // TODO: Summarization if too long + return fullText; + } + + private extractFileReferences(references: readonly ChatPromptReference[] | undefined): string | undefined { + if (!references || references.length === 0) { + return; + } + // 'file:///Users/jospicer/dev/joshbot/.github/workflows/build-vsix.yml' -> '.github/workflows/build-vsix.yml' + const parts: string[] = []; + for (const ref of references) { + if (ref.value instanceof vscode.Uri && ref.value.scheme === 'file') { // TODO: Add support for more kinds of references + const repositoryForFile = getRepositoryForFile(this.gitAPI, ref.value); + if (repositoryForFile) { + const relativePath = pathLib.relative(repositoryForFile.rootUri.fsPath, ref.value.fsPath); + parts.push(` - ${relativePath}`); + } + } + } + + if (!parts.length) { + return; + } + + parts.unshift('The user has attached the following files as relevant context:'); + return parts.join('\n'); + } + + public async provideChatSessions(token: vscode.CancellationToken): Promise { + try { + const capi = await this.copilotApi; + if (!capi) { + return []; + } + + // Check if the token is already cancelled + if (token.isCancellationRequested) { + return []; + } + + await this.waitRepoManagerInitialization(); + + let codingAgentPRs: CodingAgentPRAndStatus[] = await this.prsTreeModel.getCopilotPullRequests(); + Logger.debug(`Fetched PRs from API: ${codingAgentPRs.length}`, CopilotRemoteAgentManager.ID); + + return await Promise.all(codingAgentPRs.map(async prAndStatus => { + const timestampNumber = new Date(prAndStatus.item.createdAt).getTime(); + const status = copilotPRStatusToSessionStatus(prAndStatus.status); + const pullRequest = prAndStatus.item; + const tooltip = await issueMarkdown(pullRequest, this.context, this.repositoriesManager); + + const uri = await toOpenPullRequestWebviewUri({ owner: pullRequest.remote.owner, repo: pullRequest.remote.repositoryName, pullRequestNumber: pullRequest.number }); + const prLinkTitle = vscode.l10n.t('Open pull request in VS Code'); + + // If we have multiple repositories, include the repo name in the link text + // e.g., 'owner/repo #123' instead of just '#123' + let repoInfo = ''; + if (this.repositoriesManager.folderManagers.length > 1) { + const owner = pullRequest.remote.owner; + 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 }), + label: pullRequest.title || `Session ${pullRequest.number}`, + iconPath: this.getIconForSession(status), + pullRequest: pullRequest, + description: description, + tooltip, + status, + timing: { + startTime: timestampNumber + }, + changes: pullRequest.item.additions !== undefined && pullRequest.item.deletions !== undefined && (pullRequest.item.additions > 0 || pullRequest.item.deletions > 0) ? { + insertions: pullRequest.item.additions, + deletions: pullRequest.item.deletions, + files: fileCount + } : undefined + }; + return chatSession; + })); + } catch (error) { + Logger.error(`Failed to provide coding agents information: ${error}`, CopilotRemoteAgentManager.ID); + } + return []; + } + + public async provideChatSessionContent(resource: URI, token: vscode.CancellationToken): Promise { + try { + const capi = await this.copilotApi; + if (!capi || token.isCancellationRequested) { + return this.createEmptySession(); + } + + await this.waitRepoManagerInitialization(); + + let pullRequestNumber: number | undefined; + let sessionIndex: number | undefined; + + const indexedSessionId = SessionIdForPr.parse(resource); + if (indexedSessionId) { + pullRequestNumber = indexedSessionId.prNumber; + sessionIndex = indexedSessionId.sessionIndex; + } + + if (typeof pullRequestNumber === 'undefined') { + pullRequestNumber = parseInt(resource.path.slice(1)); + if (isNaN(pullRequestNumber)) { + Logger.error(`Invalid pull request number: ${resource}`, CopilotRemoteAgentManager.ID); + return this.createEmptySession(); + } + } + + const pullRequest = await this.findPullRequestById(pullRequestNumber, true); + if (!pullRequest) { + Logger.error(`Pull request not found: ${pullRequestNumber}`, CopilotRemoteAgentManager.ID); + return this.createEmptySession(); + } + + // Parallelize independent operations + const timelineEvents = pullRequest.getTimelineEvents(); + const changeModels = this.getChangeModels(pullRequest); + let sessions = await capi.getAllSessions(pullRequest.id); + + if (!sessions || sessions.length === 0) { + Logger.warn(`No sessions found for pull request ${pullRequestNumber}`, CopilotRemoteAgentManager.ID); + return this.createEmptySession(); + } + + if (!Array.isArray(sessions)) { + Logger.error(`getAllSessions returned non-array: ${typeof sessions}`, CopilotRemoteAgentManager.ID); + return this.createEmptySession(); + } + + if (typeof sessionIndex === 'number') { + const target = sessions.at(sessionIndex); + if (!target) { + Logger.error(`Session not found: ${sessionIndex}`, CopilotRemoteAgentManager.ID); + return this.createEmptySession(); + } + + sessions = [target]; + } + + // Create content builder with pre-fetched change models + const contentBuilder = new ChatSessionContentBuilder(CopilotRemoteAgentManager.ID, COPILOT, changeModels); + + // Parallelize operations that don't depend on each other + const history = await contentBuilder.buildSessionHistory(sessions, pullRequest, capi, timelineEvents); + return { + history, + activeResponseCallback: this.findActiveResponseCallback(sessions, pullRequest), + requestHandler: undefined // TODO(jospicer): chatSessionsProvider@2 uses a single chat participant to handle requests + }; + } catch (error) { + Logger.error(`Failed to provide chat session content: ${error}`, CopilotRemoteAgentManager.ID); + return this.createEmptySession(); + } + } + + private findActiveResponseCallback( + sessions: SessionInfo[], + pullRequest: PullRequestModel + ): ((stream: vscode.ChatResponseStream, token: vscode.CancellationToken) => Thenable) | undefined { + // Only the latest in-progress or queued session gets activeResponseCallback + const pendingSession = sessions + .slice() + .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()) + .find(session => session.state === 'in_progress' || session.state === 'queued'); + + if (pendingSession) { + return this.createActiveResponseCallback(pullRequest, pendingSession.id); + } + return undefined; + } + + private createEmptySession(): vscode.ChatSession { + return { + history: [], + requestHandler: undefined + }; + } + + private createActiveResponseCallback(pullRequest: PullRequestModel, sessionId: string): (stream: vscode.ChatResponseStream, token: vscode.CancellationToken) => Thenable { + return async (stream: vscode.ChatResponseStream, token: vscode.CancellationToken) => { + // Use the shared streaming logic + await this.waitForQueuedToInProgress(sessionId, stream, token); + this._onDidCreatePullRequest.fire(pullRequest.number); + return this.streamSessionLogs(stream, pullRequest, sessionId, token); + }; + } + + private async streamNewLogContent(pullRequest: PullRequestModel, stream: vscode.ChatResponseStream, newLogContent: string): Promise<{ hasStreamedContent: boolean; hasSetupStepProgress: boolean }> { + try { + if (!newLogContent.trim()) { + return { hasStreamedContent: false, hasSetupStepProgress: false }; + } + + // Parse the new log content + const logChunks = parseSessionLogs(newLogContent); + let hasStreamedContent = false; + let hasSetupStepProgress = false; + + for (const chunk of logChunks) { + for (const choice of chunk.choices) { + const delta = choice.delta; + + if (delta.role === 'assistant') { + // Handle special case for run_custom_setup_step/run_setup + if (choice.finish_reason === 'tool_calls' && delta.tool_calls?.length && (delta.tool_calls[0].function.name === 'run_custom_setup_step' || delta.tool_calls[0].function.name === 'run_setup')) { + const toolCall = delta.tool_calls[0]; + let args: any = {}; + try { + args = JSON.parse(toolCall.function.arguments); + } catch { + // fallback to empty args + } + + if (delta.content && delta.content.trim()) { + // Finished setup step - create/update tool part + const toolPart = this.createToolInvocationPart(pullRequest, toolCall, args.name || delta.content); + if (toolPart) { + stream.push(toolPart); + hasStreamedContent = true; + } + } else { + // Running setup step - just track progress + hasSetupStepProgress = true; + Logger.appendLine(`Setup step in progress: ${args.name || 'Unknown step'}`, CopilotRemoteAgentManager.ID); + } + } else { + if (delta.content) { + if (!delta.content.startsWith('')) { + stream.markdown(delta.content); + hasStreamedContent = true; + } + } + + if (delta.tool_calls) { + for (const toolCall of delta.tool_calls) { + const toolPart = this.createToolInvocationPart(pullRequest, toolCall, delta.content || ''); + if (toolPart) { + stream.push(toolPart); + hasStreamedContent = true; + } + } + } + } + } + + // Handle finish reasons + if (choice.finish_reason && choice.finish_reason !== 'null') { + Logger.appendLine(`Streaming finish_reason: ${choice.finish_reason}`, CopilotRemoteAgentManager.ID); + } + } + } + + if (hasStreamedContent) { + Logger.appendLine(`Streamed content (markdown or tool parts), progress should be cleared`, CopilotRemoteAgentManager.ID); + } else if (hasSetupStepProgress) { + Logger.appendLine(`Setup step progress detected, keeping progress indicator`, CopilotRemoteAgentManager.ID); + } else { + Logger.appendLine(`No actual content streamed, progress may still be showing`, CopilotRemoteAgentManager.ID); + } + return { hasStreamedContent, hasSetupStepProgress }; + } catch (error) { + Logger.error(`Error streaming new log content: ${error}`, CopilotRemoteAgentManager.ID); + return { hasStreamedContent: false, hasSetupStepProgress: false }; + } + } + + private async streamSessionLogs(stream: vscode.ChatResponseStream, pullRequest: PullRequestModel, sessionId: string, token: vscode.CancellationToken): Promise { + const capi = await this.copilotApi; + if (!capi || token.isCancellationRequested) { + return; + } + + let lastLogLength = 0; + let lastProcessedLength = 0; + let hasActiveProgress = false; + const pollingInterval = 3000; // 3 seconds + + return new Promise((resolve, reject) => { + let isCompleted = false; + + const complete = async () => { + if (isCompleted) { + return; + } + isCompleted = true; + + await pullRequest.getFileChangesInfo(); + const multiDiffPart = await this.getFileChangesMultiDiffPart(pullRequest); + if (multiDiffPart) { + stream.push(multiDiffPart); + } + + resolve(); + }; + + const pollForUpdates = async (): Promise => { + try { + if (token.isCancellationRequested) { + complete(); + return; + } + + // Get the specific session info + const sessionInfo = await capi.getSessionInfo(sessionId); + if (!sessionInfo || token.isCancellationRequested) { + complete(); + return; + } + + // Get session logs + const logs = await capi.getLogsFromSession(sessionId); + + // Check if session is still in progress + if (sessionInfo.state !== 'in_progress') { + if (logs.length > lastProcessedLength) { + const newLogContent = logs.slice(lastProcessedLength); + const streamResult = await this.streamNewLogContent(pullRequest, stream, newLogContent); + if (streamResult.hasStreamedContent) { + hasActiveProgress = false; + } + } + hasActiveProgress = false; + complete(); + return; + } + + if (logs.length > lastLogLength) { + Logger.appendLine(`New logs detected, attempting to stream content`, CopilotRemoteAgentManager.ID); + const newLogContent = logs.slice(lastProcessedLength); + const streamResult = await this.streamNewLogContent(pullRequest, stream, newLogContent); + lastProcessedLength = logs.length; + + if (streamResult.hasStreamedContent) { + Logger.appendLine(`Content was streamed, resetting hasActiveProgress to false`, CopilotRemoteAgentManager.ID); + hasActiveProgress = false; + } else if (streamResult.hasSetupStepProgress) { + Logger.appendLine(`Setup step progress detected, keeping progress active`, CopilotRemoteAgentManager.ID); + // Keep hasActiveProgress as is, don't reset it + } else { + Logger.appendLine(`No content was streamed, keeping hasActiveProgress as ${hasActiveProgress}`, CopilotRemoteAgentManager.ID); + } + } + + lastLogLength = logs.length; + + if (!token.isCancellationRequested && sessionInfo.state === 'in_progress') { + if (!hasActiveProgress) { + Logger.appendLine(`Showing progress indicator (hasActiveProgress was false)`, CopilotRemoteAgentManager.ID); + stream.progress('Working...'); + hasActiveProgress = true; + } else { + Logger.appendLine(`NOT showing progress indicator (hasActiveProgress was true)`, CopilotRemoteAgentManager.ID); + } + setTimeout(pollForUpdates, pollingInterval); + } else { + complete(); + } + } catch (error) { + Logger.error(`Error polling for session updates: ${error}`, CopilotRemoteAgentManager.ID); + if (!token.isCancellationRequested) { + setTimeout(pollForUpdates, pollingInterval); + } else { + reject(error); + } + } + }; + + // Start polling + setTimeout(pollForUpdates, pollingInterval); + }); + } + + private async getChangeModels(pullRequest: PullRequestModel) { + try { + const repoInfo = await this.repoInfo(); + if (!repoInfo) { + return []; + } + + const { fm: folderManager } = repoInfo; + return await PullRequestModel.getChangeModels(folderManager, pullRequest); + } catch (error) { + Logger.error(`Failed to get change models: ${error}`, CopilotRemoteAgentManager.ID); + return []; + } + } + + private async getFileChangesMultiDiffPart(pullRequest: PullRequestModel): Promise { + try { + const changeModels = await this.getChangeModels(pullRequest); + Logger.warn('No file changes found for pull request, not showing diff.', CopilotRemoteAgentManager.ID); + if (changeModels.length === 0) { + return undefined; + } + + const diffEntries: vscode.ChatResponseDiffEntry[] = []; + for (const changeModel of changeModels) { + const { added, removed } = await changeModel.calculateChangedLinesCount(); + Logger.trace(`DiffEntry -> original='${changeModel.parentFilePath}' modified='${changeModel.filePath}' (+${added} -${removed})`, CopilotRemoteAgentManager.ID); + diffEntries.push({ + originalUri: changeModel.parentFilePath, + modifiedUri: changeModel.filePath, + goToFileUri: changeModel.filePath, + added, + removed, + }); + } + + const title = `Changes in Pull Request #${pullRequest.number}`; + return new vscode.ChatResponseMultiDiffPart(diffEntries, title); + } catch (error) { + Logger.error(`Failed to get file changes multi diff part: ${error}`, CopilotRemoteAgentManager.ID); + return undefined; + } + } + + private async findPullRequestById(number: number, fetch: boolean): Promise { + for (const folderManager of this.repositoriesManager.folderManagers) { + for (const githubRepo of folderManager.gitHubRepositories) { + const pullRequest = githubRepo.pullRequestModels.find(pr => pr.number === number); + if (pullRequest) { + return pullRequest; + } + + if (fetch) { + try { + const pullRequest = await githubRepo.getPullRequest(number, false); + if (pullRequest) { + return pullRequest; + } + } catch (error) { + // Continue to next repository if this one doesn't have the PR + Logger.debug(`PR ${number} not found in ${githubRepo.remote.owner}/${githubRepo.remote.repositoryName} (remote=${githubRepo.remote.url}): ${error}`, CopilotRemoteAgentManager.ID); + } + } + } + } + return undefined; + } + + private createToolInvocationPart(pullRequest: PullRequestModel, toolCall: any, deltaContent: string = ''): vscode.ChatToolInvocationPart | vscode.ChatResponseThinkingProgressPart | undefined { + if (!toolCall.function?.name || !toolCall.id) { + return undefined; + } + + // Hide reply_to_comment tool + if (toolCall.function.name === 'reply_to_comment') { + return undefined; + } + + const toolPart = new vscode.ChatToolInvocationPart(toolCall.function.name, toolCall.id); + toolPart.isComplete = true; + toolPart.isError = false; + toolPart.isConfirmed = true; + + try { + const toolDetails = parseToolCallDetails(toolCall, deltaContent); + toolPart.toolName = toolDetails.toolName; + + if (toolCall.toolName === 'think') { + return new vscode.ChatResponseThinkingProgressPart(toolCall.invocationMessage); + } + + if (toolCall.function.name === 'bash') { + toolPart.invocationMessage = new vscode.MarkdownString(`\`\`\`bash\n${toolDetails.invocationMessage}\n\`\`\``); + } else { + toolPart.invocationMessage = new vscode.MarkdownString(toolDetails.invocationMessage); + } + + if (toolDetails.pastTenseMessage) { + toolPart.pastTenseMessage = new vscode.MarkdownString(toolDetails.pastTenseMessage); + } + if (toolDetails.originMessage) { + toolPart.originMessage = new vscode.MarkdownString(toolDetails.originMessage); + } + if (toolDetails.toolSpecificData) { + if (StrReplaceEditorToolData.is(toolDetails.toolSpecificData)) { + if ((toolDetails.toolSpecificData.command === 'view' || toolDetails.toolSpecificData.command === 'edit') && toolDetails.toolSpecificData.fileLabel) { + const uri = vscode.Uri.file(pathLib.join(pullRequest.githubRepository.rootUri.fsPath, toolDetails.toolSpecificData.fileLabel)); + toolPart.invocationMessage = new vscode.MarkdownString(`${toolPart.toolName} [](${uri.toString()})`); + toolPart.invocationMessage.supportHtml = true; + toolPart.pastTenseMessage = new vscode.MarkdownString(`${toolPart.toolName} [](${uri.toString()})`); + } + } else { + toolPart.toolSpecificData = toolDetails.toolSpecificData; + } + } + } catch (error) { + toolPart.toolName = toolCall.function.name || 'unknown'; + toolPart.invocationMessage = new vscode.MarkdownString(`Tool: ${toolCall.function.name}`); + toolPart.isError = true; + } + + return toolPart; + } + + private async waitForQueuedToInProgress( + sessionId: string, + stream?: vscode.ChatResponseStream, + token?: vscode.CancellationToken + ): Promise { + const capi = await this.copilotApi; + if (!capi) { + return undefined; + } + + let sessionInfo: SessionInfo | undefined; + + const waitForQueuedMaxRetries = 3; + const waitForQueuedDelay = 5_000; // 5 seconds + + // Allow for a short delay before the session is marked as 'queued' + let waitForQueuedCount = 0; + do { + sessionInfo = await capi.getSessionInfo(sessionId); + if (sessionInfo && sessionInfo.state === 'queued') { + stream?.progress(vscode.l10n.t('Attaching to session')); + Logger.trace('Queued session found', CopilotRemoteAgentManager.ID); + break; + } + if (waitForQueuedCount < waitForQueuedMaxRetries) { + Logger.trace('Session not yet queued, waiting...', CopilotRemoteAgentManager.ID); + await new Promise(resolve => setTimeout(resolve, waitForQueuedDelay)); + } + ++waitForQueuedCount; + } while (waitForQueuedCount <= waitForQueuedMaxRetries && (!token || !token.isCancellationRequested)); + + if (!sessionInfo || sessionInfo.state !== 'queued') { + if (sessionInfo?.state === 'in_progress') { + Logger.trace('Session already in progress', CopilotRemoteAgentManager.ID); + return sessionInfo; + } + // Failure + Logger.trace('Failed to find queued session', CopilotRemoteAgentManager.ID); + return; + } + + const maxWaitTime = 2 * 60 * 1_000; // 2 minutes + const pollInterval = 3_000; // 3 seconds + const startTime = Date.now(); + + Logger.appendLine(`Session ${sessionInfo.id} is queued, waiting for transition to in_progress...`, CopilotRemoteAgentManager.ID); + while (Date.now() - startTime < maxWaitTime && (!token || !token.isCancellationRequested)) { + const sessionInfo = await capi.getSessionInfo(sessionId); + if (sessionInfo?.state === 'in_progress') { + Logger.appendLine(`Session ${sessionInfo.id} now in progress.`, CopilotRemoteAgentManager.ID); + return sessionInfo; + } + await new Promise(resolve => setTimeout(resolve, pollInterval)); + } + Logger.warn(`Timed out waiting for session ${sessionId} to transition from queued to in_progress`, CopilotRemoteAgentManager.ID); + } + + private async waitForNewSession( + pullRequest: PullRequestModel, + stream: vscode.ChatResponseStream, + token: vscode.CancellationToken, + waitForTransitionToInProgress: boolean = false + ): Promise { + // Get the current number of sessions + const capi = await this.copilotApi; + if (!capi) { + stream.markdown(vscode.l10n.t('Failed to connect to Copilot API.')); + return; + } + + const initialSessions = await capi.getAllSessions(pullRequest.id); + const initialSessionCount = initialSessions.length; + + // Poll for a new session to start + const maxWaitTime = 5 * 60 * 1000; // 5 minutes + const pollInterval = 3000; // 3 seconds + const startTime = Date.now(); + + while (Date.now() - startTime < maxWaitTime && !token.isCancellationRequested) { + const currentSessions = await capi.getAllSessions(pullRequest.id); + + // Check if a new session has started + if (currentSessions.length > initialSessionCount) { + const newSession = currentSessions + .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime())[0]; + if (!waitForTransitionToInProgress) { + return newSession; + } + const inProgressSession = await this.waitForQueuedToInProgress(newSession.id, stream, token); + if (!inProgressSession) { + stream.markdown(vscode.l10n.t('Timed out waiting for coding agent to begin work. Please try again shortly.')); + return; + } + return inProgressSession; + } + + await new Promise(resolve => setTimeout(resolve, pollInterval)); + } + + stream.markdown(vscode.l10n.t('Timed out waiting for the coding agent to respond. The agent may still be processing your request.')); + return; + } + + private getIconForSession(status: vscode.ChatSessionStatus): vscode.Uri | vscode.ThemeIcon { + // Fallback to theme icons if no theme data available + switch (status) { + case vscode.ChatSessionStatus.Completed: + return new vscode.ThemeIcon('issues', new vscode.ThemeColor('testing.iconPassed')); + case vscode.ChatSessionStatus.Failed: + return new vscode.ThemeIcon('error', new vscode.ThemeColor('testing.iconFailed')); + default: + return new vscode.ThemeIcon('issue-reopened', new vscode.ThemeColor('editorLink.activeForeground')); + } + } + + public refreshChatSessions(): void { + this.prsTreeModel.clearCopilotCaches(); + this._onDidChangeChatSessions.fire(); + } + + private async waitForJobWithPullRequest( + capiClient: CopilotApi, + owner: string, + repo: string, + jobId: string, + token?: vscode.CancellationToken + ): Promise { + const maxWaitTime = 30 * 1000; // 30 seconds + const pollInterval = 2000; // 2 seconds + const startTime = Date.now(); + + Logger.appendLine(`Waiting for job ${jobId} to have pull request information...`, CopilotRemoteAgentManager.ID); + + while (Date.now() - startTime < maxWaitTime && (!token || !token.isCancellationRequested)) { + const jobInfo = await capiClient.getJobByJobId(owner, repo, jobId); + if (jobInfo && jobInfo.pull_request && jobInfo.pull_request.number) { + Logger.appendLine(`Job ${jobId} now has pull request #${jobInfo.pull_request.number}`, CopilotRemoteAgentManager.ID); + return jobInfo; + } + await new Promise(resolve => setTimeout(resolve, pollInterval)); + } + + Logger.warn(`Timed out waiting for job ${jobId} to have pull request information`, CopilotRemoteAgentManager.ID); + return undefined; + } + + public async cancelMostRecentChatSession(pullRequest: PullRequestModel): Promise { + const capi = await this.copilotApi; + if (!capi) { + Logger.warn(`No Copilot API instance found`); + return; + } + + const folderManager = this.repositoriesManager.getManagerForIssueModel(pullRequest) ?? this.repositoriesManager.folderManagers[0]; + if (!folderManager) { + Logger.warn(`No folder manager found for pull request`); + return; + } + + const sessions = await capi.getAllSessions(pullRequest.id); + if (sessions.length > 0) { + const mostRecentSession = sessions[sessions.length - 1]; + const folder = folderManager.gitHubRepositories.find(repo => repo.remote.remoteName === pullRequest.remote.remoteName); + folder?.cancelWorkflow(mostRecentSession.workflow_run_id); + } else { + Logger.warn(`No active chat session found for pull request ${pullRequest.id}`); + } + } +} \ No newline at end of file diff --git a/src/github/copilotRemoteAgent/chatSessionContentBuilder.ts b/src/github/copilotRemoteAgent/chatSessionContentBuilder.ts new file mode 100644 index 0000000000..626c3fccc1 --- /dev/null +++ b/src/github/copilotRemoteAgent/chatSessionContentBuilder.ts @@ -0,0 +1,468 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as nodePath from 'path'; +import * as marked from 'marked'; +import vscode from 'vscode'; +import { parseSessionLogs, parseToolCallDetails, StrReplaceEditorToolData } from '../../../common/sessionParsing'; +import { COPILOT_SWE_AGENT } from '../../common/copilot'; +import Logger from '../../common/logger'; +import { CommentEvent, CopilotFinishedEvent, CopilotStartedEvent, EventType, ReviewEvent, TimelineEvent } from '../../common/timelineEvent'; +import { toOpenPullRequestWebviewUri } from '../../common/uri'; +import { InMemFileChangeModel, RemoteFileChangeModel } from '../../view/fileChangeModel'; +import { AssistantDelta, Choice, ToolCall } from '../common'; +import { CopilotApi, SessionInfo } from '../copilotApi'; +import { PlainTextRenderer } from '../markdownUtils'; +import { PullRequestModel } from '../pullRequestModel'; + +export class ChatSessionContentBuilder { + constructor( + private loggerId: string, + private readonly handler: string, + private getChangeModels: Promise<(RemoteFileChangeModel | InMemFileChangeModel)[]> + ) { } + + public async buildSessionHistory( + sessions: SessionInfo[], + pullRequest: PullRequestModel, + capi: CopilotApi, + timelineEventsPromise: Promise + ): Promise> { + const sortedSessions = sessions + .filter((session, index, array) => + array.findIndex(s => s.id === session.id) === index + ) + .slice().sort((a, b) => + new Date(a.created_at).getTime() - new Date(b.created_at).getTime() + ); + + // Process all sessions concurrently while maintaining order + const sessionResults = await Promise.all( + sortedSessions.map(async (session, sessionIndex) => { + const firstHistoryEntry = async () => { + const sessionPrompt = await this.determineSessionPrompt(session, sessionIndex, pullRequest, timelineEventsPromise, capi); + + // Create request turn for this session + const sessionRequest = new vscode.ChatRequestTurn2( + sessionPrompt, + undefined, // command + [], // references + COPILOT_SWE_AGENT, + [], // toolReferences + [] + ); + return sessionRequest; + }; + const secondHistoryEntry = async () => { + const logs = await capi.getLogsFromSession(session.id); + // Create response turn + const responseHistory = await this.createResponseTurn(pullRequest, logs, session); + return responseHistory; + }; + const [first, second] = await Promise.all([ + firstHistoryEntry(), + secondHistoryEntry(), + ]); + + return { first, second, sessionIndex }; + }) + ); + + const history: Array = []; + + // Build history array in the correct order + for (const { first, second, sessionIndex } of sessionResults) { + history.push(first); + + if (second) { + // if this is the first response, then also add the PR card + if (sessionIndex === 0) { + const uri = await toOpenPullRequestWebviewUri({ owner: pullRequest.remote.owner, repo: pullRequest.remote.repositoryName, pullRequestNumber: pullRequest.number }); + const plaintextBody = marked.parse(pullRequest.body, { renderer: new PlainTextRenderer(true), smartypants: true }).trim(); + + const card = new vscode.ChatResponsePullRequestPart(uri, pullRequest.title, plaintextBody, pullRequest.author.specialDisplayName ?? pullRequest.author.login, `#${pullRequest.number}`); + const cardTurn = new vscode.ChatResponseTurn2([card], {}, COPILOT_SWE_AGENT); + history.push(cardTurn); + } + history.push(second); + } + } + return history; + } + + private async createResponseTurn(pullRequest: PullRequestModel, logs: string, session: SessionInfo): Promise { + if (logs.trim().length > 0) { + return await this.parseSessionLogsIntoResponseTurn(pullRequest, logs, session); + } else if (session.state === 'in_progress' || session.state === 'queued') { + // For in-progress sessions without logs, create a placeholder response + const placeholderParts = [new vscode.ChatResponseProgressPart('Initializing session')]; + const responseResult: vscode.ChatResult = {}; + return new vscode.ChatResponseTurn2(placeholderParts, responseResult, COPILOT_SWE_AGENT); + } else { + // For completed sessions without logs, add an empty response to maintain pairing + const emptyParts = [new vscode.ChatResponseMarkdownPart('_No logs available for this session_')]; + const responseResult: vscode.ChatResult = {}; + return new vscode.ChatResponseTurn2(emptyParts, responseResult, COPILOT_SWE_AGENT); + } + } + + private async determineSessionPrompt( + session: SessionInfo, + sessionIndex: number, + pullRequest: PullRequestModel, + timelineEventsPromise: Promise, + capi: CopilotApi + ): Promise { + let sessionPrompt = session.name || `Session ${sessionIndex + 1} (ID: ${session.id})`; + + if (sessionIndex === 0) { + sessionPrompt = await this.getInitialSessionPrompt(session, pullRequest, capi, sessionPrompt); + } else { + sessionPrompt = await this.getFollowUpSessionPrompt(sessionIndex, timelineEventsPromise, sessionPrompt); + } + + // TODO: @rebornix, remove @copilot prefix from session prompt for now + sessionPrompt = sessionPrompt.replace(/@copilot\s*/gi, '').trim(); + return sessionPrompt; + } + + private async getFollowUpSessionPrompt( + sessionIndex: number, + timelineEventsPromise: Promise, + defaultPrompt: string + ): Promise { + const timelineEvents = await timelineEventsPromise; + Logger.appendLine(`Found ${timelineEvents.length} timeline events`, this.loggerId); + const copilotStartedEvents = timelineEvents + .filter((event): event is CopilotStartedEvent => event.event === EventType.CopilotStarted) + .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); + + const copilotFinishedEvents = timelineEvents + .filter((event): event is CopilotFinishedEvent => event.event === EventType.CopilotFinished) + .sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()); + + Logger.appendLine(`Session ${sessionIndex}: Found ${copilotStartedEvents.length} CopilotStarted events and ${copilotFinishedEvents.length} CopilotFinished events`, this.loggerId); + + const copilotStartedEvent = copilotStartedEvents[sessionIndex]; + if (!copilotStartedEvent) { + Logger.appendLine(`Session ${sessionIndex}: No CopilotStarted event found at index ${sessionIndex}`, this.loggerId); + return defaultPrompt; + } + + const currentSessionStartTime = new Date(copilotStartedEvent.createdAt).getTime(); + const previousSessionEndTime = this.getPreviousSessionEndTime(sessionIndex, copilotFinishedEvents); + + const relevantEvents = this.findRelevantTimelineEvents(timelineEvents, previousSessionEndTime, currentSessionStartTime); + + const matchingEvent = relevantEvents[0]; + if (matchingEvent) { + const prompt = this.extractPromptFromEvent(matchingEvent); + Logger.appendLine(`Session ${sessionIndex}: Found matching event - ${matchingEvent.event}`, this.loggerId); + return prompt; + } else { + Logger.appendLine(`Session ${sessionIndex}: No matching event found between times ${previousSessionEndTime} and ${currentSessionStartTime}`, this.loggerId); + Logger.appendLine(`Session ${sessionIndex}: Relevant events found: ${relevantEvents.length}`, this.loggerId); + return defaultPrompt; + } + } + + private getPreviousSessionEndTime(sessionIndex: number, copilotFinishedEvents: CopilotFinishedEvent[]): number { + if (sessionIndex > 0 && copilotFinishedEvents[sessionIndex - 1]) { + return new Date(copilotFinishedEvents[sessionIndex - 1].createdAt).getTime(); + } + return 0; + } + + private findRelevantTimelineEvents( + timelineEvents: readonly TimelineEvent[], + previousSessionEndTime: number, + currentSessionStartTime: number + ): TimelineEvent[] { + return timelineEvents + .filter(event => { + if (event.event !== EventType.Commented && event.event !== EventType.Reviewed) { + return false; + } + + const eventTime = new Date( + event.event === EventType.Commented ? (event as CommentEvent).createdAt : + event.event === EventType.Reviewed ? (event as ReviewEvent).submittedAt : '' + ).getTime(); + + // Must be after previous session and before current session + return eventTime > previousSessionEndTime && eventTime < currentSessionStartTime; + }) + .filter(event => { + if (event.event === EventType.Commented) { + const comment = event as CommentEvent; + return comment.body.includes('@copilot') || comment.body.includes(this.handler); + } else if (event.event === EventType.Reviewed) { + const review = event as ReviewEvent; + return review.body.includes('@copilot') || review.body.includes(this.handler); + } + return false; + }) + .sort((a, b) => { + const timeA = new Date( + a.event === EventType.Commented ? (a as CommentEvent).createdAt : + a.event === EventType.Reviewed ? (a as ReviewEvent).submittedAt : '' + ).getTime(); + const timeB = new Date( + b.event === EventType.Commented ? (b as CommentEvent).createdAt : + b.event === EventType.Reviewed ? (b as ReviewEvent).submittedAt : '' + ).getTime(); + return timeB - timeA; // Most recent first (closest to session start) + }); + } + + private extractPromptFromEvent(event: TimelineEvent): string { + let body = ''; + if (event.event === EventType.Commented) { + body = (event as CommentEvent).body; + } else if (event.event === EventType.Reviewed) { + body = (event as ReviewEvent).body; + } + + // Extract the prompt before any separator pattern (used in addFollowUpToExistingPR) + // but keep the @copilot mention + const separatorMatch = body.match(/^(.*?)\s*\n\n\s*---\s*\n\n/s); + if (separatorMatch) { + return separatorMatch[1].trim(); + } + + return body.trim(); + } + + private async getInitialSessionPrompt( + session: SessionInfo, + pullRequest: PullRequestModel, + capi: CopilotApi, + defaultPrompt: string + ): Promise { + try { + const jobInfo = await capi.getJobBySessionId( + pullRequest.base.repositoryCloneUrl.owner, + pullRequest.base.repositoryCloneUrl.repositoryName, + session.id + ); + if (jobInfo && jobInfo.problem_statement) { + let prompt = jobInfo.problem_statement; + const titleMatch = jobInfo.problem_statement.match(/TITLE: \s*(.*)/i); + if (titleMatch && titleMatch[1]) { + prompt = titleMatch[1].trim(); + } else { + const split = jobInfo.problem_statement.split('\n'); + if (split.length > 0) { + prompt = split[0].trim(); + } + } + Logger.appendLine(`Session 0: Found problem_statement from Jobs API: ${prompt}`, this.loggerId); + return prompt; + } + } catch (error) { + Logger.warn(`Failed to get job info for session ${session.id}: ${error}`, this.loggerId); + } + return defaultPrompt; + } + + private async parseSessionLogsIntoResponseTurn(pullRequest: PullRequestModel, logs: string, session: SessionInfo): Promise { + try { + const logChunks = parseSessionLogs(logs); + const responseParts: Array = []; + let currentResponseContent = ''; + + for (const chunk of logChunks) { + if (!chunk.choices || !Array.isArray(chunk.choices)) { + continue; + } + + for (const choice of chunk.choices) { + const delta = choice.delta; + if (delta.role === 'assistant') { + this.processAssistantDelta(delta, choice, pullRequest, responseParts, currentResponseContent); + } + + } + } + + if (currentResponseContent.trim()) { + responseParts.push(new vscode.ChatResponseMarkdownPart(currentResponseContent.trim())); + } + + if (session.state === 'completed' || session.state === 'failed' /** session can fail with proposed changes */) { + const fileChangesPart = await this.getFileChangesMultiDiffPart(pullRequest); + if (fileChangesPart) { + responseParts.push(fileChangesPart); + } + } + + if (responseParts.length > 0) { + const responseResult: vscode.ChatResult = {}; + return new vscode.ChatResponseTurn2(responseParts, responseResult, COPILOT_SWE_AGENT); + } + + return undefined; + } catch (error) { + Logger.error(`Failed to parse session logs into response turn: ${error}`, this.loggerId); + return undefined; + } + } + + private processAssistantDelta( + delta: AssistantDelta, + choice: Choice, + pullRequest: PullRequestModel, + responseParts: Array, + currentResponseContent: string, + ): string { + if (delta.role === 'assistant') { + // Handle special case for run_custom_setup_step + if ( + choice.finish_reason === 'tool_calls' && + delta.tool_calls?.length && + (delta.tool_calls[0].function.name === 'run_custom_setup_step' || delta.tool_calls[0].function.name === 'run_setup') + ) { + const toolCall = delta.tool_calls[0]; + let args: { name?: string } = {}; + try { + args = JSON.parse(toolCall.function.arguments); + } catch { + // fallback to empty args + } + + // Ignore if delta.content is empty/undefined (running state) + if (delta.content && delta.content.trim()) { + // Add any accumulated content as markdown first + if (currentResponseContent.trim()) { + responseParts.push(new vscode.ChatResponseMarkdownPart(currentResponseContent.trim())); + currentResponseContent = ''; + } + + const toolPart = this.createToolInvocationPart(pullRequest, toolCall, args.name || delta.content); + if (toolPart) { + responseParts.push(toolPart); + } + } + // Skip if content is empty (running state) + } else { + if (delta.content) { + if (!delta.content.startsWith('') && !delta.content.startsWith('')) { + currentResponseContent += delta.content; + } + } + + const isError = delta.content?.startsWith(''); + if (delta.tool_calls) { + // Add any accumulated content as markdown first + if (currentResponseContent.trim()) { + responseParts.push(new vscode.ChatResponseMarkdownPart(currentResponseContent.trim())); + currentResponseContent = ''; + } + + for (const toolCall of delta.tool_calls) { + const toolPart = this.createToolInvocationPart(pullRequest, toolCall, delta.content || ''); + if (toolPart) { + responseParts.push(toolPart); + } + } + + if (isError) { + const toolPart = new vscode.ChatToolInvocationPart('Command', 'command'); + // Remove at the start and at the end + const cleaned = (delta.content ?? '').replace(/^\s*\s*/i, '').replace(/\s*<\/error>\s*$/i, ''); + toolPart.invocationMessage = cleaned; + toolPart.isError = true; + responseParts.push(toolPart); + } + } + } + } + return currentResponseContent; + } + + private createToolInvocationPart(pullRequest: PullRequestModel, toolCall: ToolCall, deltaContent: string = ''): vscode.ChatToolInvocationPart | vscode.ChatResponseThinkingProgressPart | undefined { + if (!toolCall.function?.name || !toolCall.id) { + return undefined; + } + + // Hide reply_to_comment tool + if (toolCall.function.name === 'reply_to_comment') { + return undefined; + } + + const toolPart = new vscode.ChatToolInvocationPart(toolCall.function.name, toolCall.id); + toolPart.isComplete = true; + toolPart.isError = false; + toolPart.isConfirmed = true; + + try { + const toolDetails = parseToolCallDetails(toolCall, deltaContent); + toolPart.toolName = toolDetails.toolName; + + if (toolPart.toolName === 'think') { + return new vscode.ChatResponseThinkingProgressPart(toolDetails.invocationMessage); + } + + if (toolCall.function.name === 'bash') { + toolPart.invocationMessage = new vscode.MarkdownString(`\`\`\`bash\n${toolDetails.invocationMessage}\n\`\`\``); + } else { + toolPart.invocationMessage = new vscode.MarkdownString(toolDetails.invocationMessage); + } + + if (toolDetails.pastTenseMessage) { + toolPart.pastTenseMessage = new vscode.MarkdownString(toolDetails.pastTenseMessage); + } + if (toolDetails.originMessage) { + toolPart.originMessage = new vscode.MarkdownString(toolDetails.originMessage); + } + if (toolDetails.toolSpecificData) { + if (StrReplaceEditorToolData.is(toolDetails.toolSpecificData)) { + if ((toolDetails.toolSpecificData.command === 'view' || toolDetails.toolSpecificData.command === 'edit') && toolDetails.toolSpecificData.fileLabel) { + const uri = vscode.Uri.file(nodePath.join(pullRequest.githubRepository.rootUri.fsPath, toolDetails.toolSpecificData.fileLabel)); + toolPart.invocationMessage = new vscode.MarkdownString(`${toolPart.toolName} [](${uri.toString()})` + (toolDetails.toolSpecificData?.viewRange ? `, lines ${toolDetails.toolSpecificData.viewRange?.start} to ${toolDetails.toolSpecificData.viewRange?.end}` : '')); + toolPart.invocationMessage.supportHtml = true; + toolPart.pastTenseMessage = new vscode.MarkdownString(`${toolPart.toolName} [](${uri.toString()})` + (toolDetails.toolSpecificData?.viewRange ? `, lines ${toolDetails.toolSpecificData.viewRange?.start} to ${toolDetails.toolSpecificData.viewRange?.end}` : '')); + } + } else { + toolPart.toolSpecificData = toolDetails.toolSpecificData; + } + } + } catch (error) { + toolPart.toolName = toolCall.function.name || 'unknown'; + toolPart.invocationMessage = new vscode.MarkdownString(`Tool: ${toolCall.function.name}`); + toolPart.isError = true; + } + + return toolPart; + } + + private async getFileChangesMultiDiffPart(pullRequest: PullRequestModel): Promise { + try { + const changeModels = await this.getChangeModels; + + if (changeModels.length === 0) { + return undefined; + } + + const diffEntries: vscode.ChatResponseDiffEntry[] = []; + for (const changeModel of changeModels) { + const { added, removed } = await changeModel.calculateChangedLinesCount(); + diffEntries.push({ + originalUri: changeModel.parentFilePath, + modifiedUri: changeModel.filePath, + goToFileUri: changeModel.filePath, + added, + removed, + }); + } + + const title = `Changes in Pull Request #${pullRequest.number}`; + return new vscode.ChatResponseMultiDiffPart(diffEntries, title); + } catch (error) { + Logger.error(`Failed to get file changes multi diff part: ${error}`, this.loggerId); + return undefined; + } + } +} \ No newline at end of file diff --git a/src/github/copilotRemoteAgent/gitOperationsManager.ts b/src/github/copilotRemoteAgent/gitOperationsManager.ts new file mode 100644 index 0000000000..dd9d49640d --- /dev/null +++ b/src/github/copilotRemoteAgent/gitOperationsManager.ts @@ -0,0 +1,193 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { adjectives, animals, colors, NumberDictionary, uniqueNamesGenerator } from '@joaomoreno/unique-names-generator'; +import vscode from 'vscode'; +import { Repository } from '../../api/api'; +import Logger from '../../common/logger'; +import { BRANCH_RANDOM_NAME_DICTIONARY, BRANCH_WHITESPACE_CHAR, GIT } from '../../common/settingKeys'; +import { RepoInfo } from '../common'; + +export class GitOperationsManager { + constructor(private loggerID: string) { } + + async commitAndPushChanges(repoInfo: RepoInfo) { + const { repository, remote, baseRef } = repoInfo; + const asyncBranch = await this.generateRandomBranchName(repository, 'copilot'); + + try { + await repository.createBranch(asyncBranch, true); + const commitMessage = 'Checkpoint from VS Code for coding agent session'; + + await this.performCommit(asyncBranch, repository, commitMessage); + await repository.push(remote.remoteName, asyncBranch, true); + this.showBranchSwitchNotification(repository, baseRef, asyncBranch); + return asyncBranch; // This is the new head ref + } catch (error) { + await this.rollbackToOriginalBranch(repository, baseRef); + Logger.error(`Failed to auto-commit and push pending changes: ${error}`, this.loggerID); + throw new Error(vscode.l10n.t('Could not auto-push pending changes. Manually commit or stash your changes and try again. ({0})', error.message)); + } + } + + private async performCommit(asyncBranch: string, repository: Repository, commitMessage: string): Promise { + try { + await repository.commit(commitMessage, { all: true }); + + if (repository.state.HEAD?.name !== asyncBranch || repository.state.workingTreeChanges.length > 0 || repository.state.indexChanges.length > 0) { + throw new Error(vscode.l10n.t('Uncommitted changes still detected.')); + } + } catch (error) { + // Fallback to interactive commit + const commitSuccessful = await this.handleInteractiveCommit(repository); + if (!commitSuccessful) { + throw new Error(vscode.l10n.t('Exclude your uncommitted changes and try again.')); + } + } + } + + private async handleInteractiveCommit(repository: Repository): Promise { + const COMMIT_YOUR_CHANGES = vscode.l10n.t('Commit your changes to continue coding agent session. Close integrated terminal to cancel.'); + + return vscode.window.withProgress({ + title: COMMIT_YOUR_CHANGES, + cancellable: true, + location: vscode.ProgressLocation.Notification + }, async (progress, token) => { + return new Promise((resolve) => { + const startingCommit = repository.state.HEAD?.commit; + const terminal = vscode.window.createTerminal({ + name: 'GitHub Coding Agent', + cwd: repository.rootUri.fsPath, + message: `\x1b[1m${COMMIT_YOUR_CHANGES}\x1b[0m` + }); + + terminal.show(); + + let disposed = false; + let timeoutId: NodeJS.Timeout; + let stateListener: vscode.Disposable | undefined; + let disposalListener: vscode.Disposable | undefined; + let cancellationListener: vscode.Disposable | undefined; + const cleanup = () => { + if (disposed) return; + disposed = true; + clearTimeout(timeoutId); + stateListener?.dispose(); + disposalListener?.dispose(); + cancellationListener?.dispose(); + terminal.dispose(); + }; + // Listen for cancellation if token is provided + if (token) { + cancellationListener = token.onCancellationRequested(() => { + cleanup(); + resolve(false); + }); + } + + // Listen for repository state changes + stateListener = repository.state.onDidChange(() => { + // Check if commit was successful (HEAD changed and no more staged changes) + if (repository.state.HEAD?.commit !== startingCommit) { + cleanup(); + resolve(true); + } + }); + // Set a timeout to avoid waiting forever + timeoutId = setTimeout(() => { + cleanup(); + resolve(false); + }, 5 * 60 * 1000); // 5 minutes timeout + // Listen for terminal disposal (user closed it) + disposalListener = vscode.window.onDidCloseTerminal((closedTerminal) => { + if (closedTerminal === terminal) { + setTimeout(() => { + if (!disposed) { + cleanup(); + // Check one more time if commit happened just before terminal was closed + resolve(repository.state.HEAD?.commit !== startingCommit); + } + }, 1000); + } + }); + }); + }); + } + + private showBranchSwitchNotification(repository: Repository, baseRef: string, newRef: string): void { + if (repository.state.HEAD?.name !== baseRef) { + const SWAP_BACK_TO_ORIGINAL_BRANCH = vscode.l10n.t(`Swap back to '{0}'`, baseRef); + vscode.window.showInformationMessage( + vscode.l10n.t(`Pending changes pushed to remote branch '{0}'.`, newRef), + SWAP_BACK_TO_ORIGINAL_BRANCH, + ).then(async (selection) => { + if (selection === SWAP_BACK_TO_ORIGINAL_BRANCH) { + await repository.checkout(baseRef); + } + }); + } + } + + private async rollbackToOriginalBranch(repository: Repository, baseRef: string): Promise { + if (repository.state.HEAD?.name !== baseRef) { + try { + await repository.checkout(baseRef); + } catch (checkoutError) { + Logger.error(`Failed to checkout back to original branch '${baseRef}': ${checkoutError}`, this.loggerID); + } + } + } + + // Adapted from https://github.com/microsoft/vscode/blob/e35e3b4e057450ea3d90c724fae5e3e9619b96fe/extensions/git/src/commands.ts#L3007 + private async generateRandomBranchName(repository: Repository, prefix: string): Promise { + const config = vscode.workspace.getConfiguration(GIT); + const branchWhitespaceChar = config.get(BRANCH_WHITESPACE_CHAR); + const branchRandomNameDictionary = config.get(BRANCH_RANDOM_NAME_DICTIONARY); + + // Default to legacy behaviour if config mismatches core + if (branchWhitespaceChar === undefined || branchRandomNameDictionary === undefined) { + return `copilot/vscode${Date.now()}`; + } + + const separator = branchWhitespaceChar; + const dictionaries: string[][] = []; + for (const dictionary of branchRandomNameDictionary) { + if (dictionary.toLowerCase() === 'adjectives') { + dictionaries.push(adjectives); + } + if (dictionary.toLowerCase() === 'animals') { + dictionaries.push(animals); + } + if (dictionary.toLowerCase() === 'colors') { + dictionaries.push(colors); + } + if (dictionary.toLowerCase() === 'numbers') { + dictionaries.push(NumberDictionary.generate({ length: 3 })); + } + } + + if (dictionaries.length === 0) { + return ''; + } + + // 5 attempts to generate a random branch name + for (let index = 0; index < 5; index++) { + const randomName = `${prefix}/${uniqueNamesGenerator({ + dictionaries, + length: dictionaries.length, + separator + })}`; + + // Check for local ref conflict + const refs = await repository.getRefs?.({ pattern: `refs/heads/${randomName}` }); + if (!refs || refs.length === 0) { + return randomName; + } + } + + return ''; + } +} diff --git a/src/github/copilotRemoteAgentUtils.ts b/src/github/copilotRemoteAgentUtils.ts new file mode 100644 index 0000000000..306efb2449 --- /dev/null +++ b/src/github/copilotRemoteAgentUtils.ts @@ -0,0 +1,64 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { MAX_PROBLEM_STATEMENT_LENGTH } from './copilotApi'; +import Logger from '../common/logger'; + +/** + * Truncation utility to ensure the problem statement sent to Copilot API is under the maximum length. + * Truncation is not ideal. The caller providing the prompt/context should be summarizing so this is a no-op whenever possible. + * + * @param prompt The final message submitted by the user + * @param context Any additional context collected by the caller (chat history, open files, etc...) + * @returns A complete 'problem statement' string that is under the maximum length, and a flag indicating if truncation occurred + */ +export function truncatePrompt(prompt: string, context?: string): { problemStatement: string; isTruncated: boolean } { + // Prioritize the userPrompt + // Take the last n characters that fit within the limit + if (prompt.length >= MAX_PROBLEM_STATEMENT_LENGTH) { + Logger.warn(`Truncation: Prompt length ${prompt.length} exceeds max of ${MAX_PROBLEM_STATEMENT_LENGTH}`); + prompt = prompt.slice(-MAX_PROBLEM_STATEMENT_LENGTH); + return { problemStatement: prompt, isTruncated: true }; + } + + if (context && (prompt.length + context.length >= MAX_PROBLEM_STATEMENT_LENGTH)) { + const availableLength = MAX_PROBLEM_STATEMENT_LENGTH - prompt.length - 2 /* new lines */; + Logger.warn(`Truncation: Combined prompt and context length ${prompt.length + context.length} exceeds max of ${MAX_PROBLEM_STATEMENT_LENGTH}`); + context = context.slice(-availableLength); + return { + problemStatement: prompt + (context ? `\n\n${context}` : ''), + isTruncated: true + }; + } + + // No truncation occurred + return { + problemStatement: prompt + (context ? `\n\n${context}` : ''), + isTruncated: false + }; +} + +export function extractTitle(prompt: string, context: string | undefined): string | undefined { + const fromTitle = () => { + if (!prompt) { + return; + } + if (prompt.length <= 20) { + return prompt; + } + return prompt.substring(0, 20) + '...'; + }; + const titleMatch = context?.match(/TITLE: \s*(.*)/i); + if (titleMatch && titleMatch[1]) { + return titleMatch[1].trim(); + } + return fromTitle(); + +} + +export function formatBodyPlaceholder(title: string | undefined): string { + return vscode.l10n.t('Coding agent has begun work on **{0}** and will update this pull request as work progresses.', title || vscode.l10n.t('your request')); +} \ No newline at end of file diff --git a/src/github/createPRLinkProvider.ts b/src/github/createPRLinkProvider.ts index b2b6d7b1db..543de0559c 100644 --- a/src/github/createPRLinkProvider.ts +++ b/src/github/createPRLinkProvider.ts @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { FolderRepositoryManager } from './folderRepositoryManager'; import { PR_SETTINGS_NAMESPACE, TERMINAL_LINK_HANDLER } from '../common/settingKeys'; import { ReviewManager } from '../view/reviewManager'; -import { FolderRepositoryManager } from './folderRepositoryManager'; interface GitHubCreateTerminalLink extends vscode.TerminalLink { url: string; diff --git a/src/github/createPRViewProvider.ts b/src/github/createPRViewProvider.ts index 317f19061e..40763443bb 100644 --- a/src/github/createPRViewProvider.ts +++ b/src/github/createPRViewProvider.ts @@ -4,9 +4,25 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { ChooseBaseRemoteAndBranchResult, ChooseCompareRemoteAndBranchResult, ChooseRemoteAndBranchArgs, CreateParamsNew, CreatePullRequestNew, RemoteInfo, TitleAndDescriptionArgs } from '../../common/views'; +import { + byRemoteName, + FolderRepositoryManager, + PullRequestDefaults, + titleAndBodyFrom, +} from './folderRepositoryManager'; +import { GitHubRepository } from './githubRepository'; +import { IAccount, ILabel, IMilestone, IProject, isITeam, ITeam, MergeMethod, RepoAccessAndMergeMethods } from './interface'; +import { BaseBranchMetadata, PullRequestGitHelper } from './pullRequestGitHelper'; +import { PullRequestModel } from './pullRequestModel'; +import { getDefaultMergeMethod } from './pullRequestOverview'; +import { getAssigneesQuickPickItems, getLabelOptions, getMilestoneFromQuickPick, getProjectFromQuickPick, reviewersQuickPick } from './quickPicks'; +import { getIssueNumberLabelFromParsed, ISSUE_EXPRESSION, ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput, variableSubstitution } from './utils'; +import { DisplayLabel, PreReviewState } from './views'; +import { RemoteInfo } from '../../common/types'; +import { ChooseBaseRemoteAndBranchResult, ChooseCompareRemoteAndBranchResult, ChooseRemoteAndBranchArgs, CreateParamsNew, CreatePullRequestNew, TitleAndDescriptionArgs } from '../../common/views'; import type { Branch, Ref } from '../api/api'; import { GitHubServerType } from '../common/authentication'; +import { emojify, ensureEmojis } from '../common/emoji'; import { commands, contexts } from '../common/executeCommands'; import Logger from '../common/logger'; import { Protocol } from '../common/protocol'; @@ -22,23 +38,10 @@ 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'; -import { - byRemoteName, - FolderRepositoryManager, - PullRequestDefaults, - titleAndBodyFrom, -} from './folderRepositoryManager'; -import { GitHubRepository } from './githubRepository'; -import { IAccount, ILabel, IMilestone, IProject, isTeam, ITeam, MergeMethod, RepoAccessAndMergeMethods } from './interface'; -import { BaseBranchMetadata, PullRequestGitHelper } from './pullRequestGitHelper'; -import { PullRequestModel } from './pullRequestModel'; -import { getDefaultMergeMethod } from './pullRequestOverview'; -import { getAssigneesQuickPickItems, getLabelOptions, getMilestoneFromQuickPick, getProjectFromQuickPick, reviewersQuickPick } from './quickPicks'; -import { getIssueNumberLabelFromParsed, ISSUE_EXPRESSION, ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput, variableSubstitution } from './utils'; -import { PreReviewState } from './views'; const ISSUE_CLOSING_KEYWORDS = new RegExp('closes|closed|close|fixes|fixed|fix|resolves|resolved|resolve\s$', 'i'); // https://docs.github.com/en/issues/tracking-your-work-with-issues/linking-a-pull-request-to-an-issue#linking-a-pull-request-to-an-issue-using-a-keyword @@ -170,7 +173,9 @@ export abstract class BaseCreatePullRequestViewProvider ({ ...label, displayName: emojify(label.name) })); const params: CreateParamsNew = { canModifyBranches: true, @@ -273,13 +278,15 @@ export abstract class BaseCreatePullRequestViewProvider { - let newLabels: ILabel[] = []; + let newLabels: DisplayLabel[] = []; - const labelsToAdd = await vscode.window.showQuickPick( + const labelsToAdd = await vscode.window.showQuickPick( getLabelOptions(this._folderRepositoryManager, this.labels, this.model.baseOwner, this.model.repositoryName).then(options => { newLabels = options.newLabels; return options.labelPicks; - }) as Promise, + }), { canPickMany: true, matchOnDescription: true, placeHolder: vscode.l10n.t('Apply labels') }, ); if (labelsToAdd) { - const addedLabels: ILabel[] = labelsToAdd.map(label => newLabels.find(l => l.name === label.label)!); + const addedLabels: DisplayLabel[] = labelsToAdd.map(label => newLabels.find(l => l.name === label.name)!); this.labels = addedLabels; this._postMessage({ command: 'set-labels', @@ -543,7 +550,7 @@ export abstract class BaseCreatePullRequestViewProvider { - const existingPR = await PullRequestGitHelper.getMatchingPullRequestMetadataForBranch(this._folderRepositoryManager.repository, this.model.compareBranch); - return existingPR ? vscode.l10n.t('A pull request already exists for this branch.') : ''; + const [existingPR, hasUpstream] = await Promise.all([PullRequestGitHelper.getMatchingPullRequestMetadataForBranch(this._folderRepositoryManager.repository, this.model.compareBranch), this.model.getCompareHasUpstream()]); + if (!existingPR || !hasUpstream) { + return undefined; + } + + const [pr, compareBranch] = await Promise.all([this._folderRepositoryManager.resolvePullRequest(existingPR.owner, existingPR.repositoryName, existingPR.prNumber), this._folderRepositoryManager.repository.getBranch(this.model.compareBranch)]); + return (pr?.head?.sha === compareBranch.commit) ? vscode.l10n.t('A pull request already exists for this branch.') : undefined; } 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 { @@ -693,7 +699,7 @@ export class CreatePullRequestViewProvider extends BaseCreatePullRequestViewProv const [totalCommits, lastCommit, pullRequestTemplate] = await Promise.all([ this.getTotalGitHubCommits(compareBranch, baseBranch), name ? titleAndBodyFrom(promiseWithTimeout(this._folderRepositoryManager.getTipCommitMessage(name), 5000)) : undefined, - descrptionSource === 'template' ? await this.getPullRequestTemplate() : undefined + descrptionSource === 'template' ? this.getPullRequestTemplate() : undefined ]); const totalNonMergeCommits = totalCommits?.filter(commit => commit.parents.length < 2); @@ -1017,11 +1023,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 }; @@ -1292,10 +1300,12 @@ export class CreatePullRequestViewProvider extends BaseCreatePullRequestViewProv } else { try { compareBranch = await this._folderRepositoryManager.repository.getBranch(newBranch); - await this.model.setCompareBranch(newBranch); } catch (e) { vscode.window.showErrorMessage(vscode.l10n.t('Branch does not exist locally.')); } + if (compareBranch) { + await this.model.setCompareBranch(newBranch); + } } compareBranch = compareBranch ?? await this._folderRepositoryManager.repository.getBranch(this.model.compareBranch); diff --git a/src/github/credentials.ts b/src/github/credentials.ts index 26c6144ffd..ce2f62b102 100644 --- a/src/github/credentials.ts +++ b/src/github/credentials.ts @@ -9,16 +9,18 @@ import { setContext } from 'apollo-link-context'; import { createHttpLink } from 'apollo-link-http'; import fetch from 'cross-fetch'; import * as vscode from 'vscode'; +import { IAccount } from './interface'; +import { LoggingApolloClient, LoggingOctokit, RateLogger } from './loggingOctokit'; +import { convertRESTUserToAccount, getEnterpriseUri, hasEnterpriseUri, isEnterprise } from './utils'; import { AuthProvider } from '../common/authentication'; +import { commands } from '../common/executeCommands'; import { Disposable } from '../common/lifecycle'; import Logger from '../common/logger'; import * as PersistentState from '../common/persistentState'; import { GITHUB_ENTERPRISE, URI } from '../common/settingKeys'; +import { initBasedOnSettingChange } from '../common/settingsUtils'; import { ITelemetry } from '../common/telemetry'; import { agent } from '../env/node/net'; -import { IAccount } from './interface'; -import { LoggingApolloClient, LoggingOctokit, RateLogger } from './loggingOctokit'; -import { convertRESTUserToAccount, getEnterpriseUri, hasEnterpriseUri, isEnterprise } from './utils'; const TRY_AGAIN = vscode.l10n.t('Try again?'); const CANCEL = vscode.l10n.t('Cancel'); @@ -120,6 +122,10 @@ export class CredentialStore extends Disposable { this._scopesEnterprise = this.context.globalState.get(LAST_USED_SCOPES_ENTERPRISE_KEY, SCOPES_OLD); } + get scopes() { + return this._scopes; + } + private async saveScopesInState() { await this.context.globalState.update(LAST_USED_SCOPES_GITHUB_KEY, this._scopes); await this.context.globalState.update(LAST_USED_SCOPES_ENTERPRISE_KEY, this._scopesEnterprise); @@ -221,25 +227,22 @@ export class CredentialStore extends Disposable { private async doCreate(options: vscode.AuthenticationGetSessionOptions, additionalScopes: boolean = false): Promise { let enterprise: AuthResult | undefined; - const initializeEnterprise = () => this.initialize(AuthProvider.githubEnterprise, options, additionalScopes ? SCOPES_WITH_ADDITIONAL : undefined, additionalScopes); + const initializeEnterprise = async () => { + enterprise = await this.initialize(AuthProvider.githubEnterprise, options, additionalScopes ? SCOPES_WITH_ADDITIONAL : undefined, additionalScopes); + }; if (hasEnterpriseUri()) { - enterprise = await initializeEnterprise(); + await initializeEnterprise(); } else { // Listen for changes to the enterprise URI and try again if it changes. - const disposable = vscode.workspace.onDidChangeConfiguration(async e => { - if (e.affectsConfiguration(`${GITHUB_ENTERPRISE}.${URI}`) && hasEnterpriseUri()) { - enterprise = await initializeEnterprise(); - disposable.dispose(); - } - }); - this.context.subscriptions.push(disposable); + initBasedOnSettingChange(GITHUB_ENTERPRISE, URI, hasEnterpriseUri, initializeEnterprise, this.context.subscriptions); } - let github: AuthResult | undefined; - if (!enterprise) { - github = await this.initialize(AuthProvider.github, options, additionalScopes ? SCOPES_WITH_ADDITIONAL : undefined, additionalScopes); + const githubOptions = { ...options }; + if (enterprise && !enterprise.canceled) { + githubOptions.silent = true; } + const github = await this.initialize(AuthProvider.github, githubOptions, additionalScopes ? SCOPES_WITH_ADDITIONAL : undefined, additionalScopes); return { - canceled: !!(github && github.canceled) || !!(enterprise && enterprise.canceled) + canceled: github.canceled || !!(enterprise && enterprise.canceled) }; } @@ -289,6 +292,33 @@ export class CredentialStore extends Disposable { return !this.allScopesIncluded(this._scopesEnterprise, SCOPES_OLD); } + async tryPromptForCopilotAuth(): Promise { + if (this.isAnyAuthenticated()) { + return true; + } + + const chatSetupResult = await commands.executeCommand(commands.CHAT_SETUP_ACTION_ID, 'agent', { additionalScopes: this.scopes }); + if (!chatSetupResult) { + return false; + } + + const result = await this.create({ createIfNone: { detail: vscode.l10n.t('Sign in to start delegating tasks to the GitHub coding agent.') } }); + + /* __GDPR__ + "remoteAgent.command.auth" : { + "succeeded" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this._telemetry.sendTelemetryEvent('remoteAgent.command.auth', { + succeeded: result.canceled ? 'false' : 'true' + }); + + if (result.canceled) { + return false; + } + return true; + } + public areScopesExtra(authProviderId: AuthProvider): boolean { if (!isEnterprise(authProviderId)) { return this.allScopesIncluded(this._scopes, SCOPES_WITH_ADDITIONAL); @@ -393,8 +423,9 @@ export class CredentialStore extends Disposable { return result; } - public async isCurrentUser(username: string): Promise { - return (await this._githubAPI?.currentUser)?.login === username || (await this._githubEnterpriseAPI?.currentUser)?.login == username; + public async isCurrentUser(authProviderId: AuthProvider, username: string): Promise { + const api = authProviderId === AuthProvider.github ? this._githubAPI : this._githubEnterpriseAPI; + return (await api?.currentUser)?.login === username; } public async getIsEmu(authProviderId: AuthProvider): Promise { @@ -409,14 +440,14 @@ export class CredentialStore extends Disposable { } private setCurrentUser(github: GitHub): void { - const getUser: ReturnType = new Promise(resolve => { + const getUser: ReturnType = new Promise((resolve, reject) => { Logger.debug('Getting current user', CredentialStore.ID); github.octokit.call(github.octokit.api.users.getAuthenticated, {}).then(result => { Logger.debug(`Got current user ${result.data.login}`, CredentialStore.ID); resolve(result); }).catch(e => { - vscode.window.showErrorMessage(vscode.l10n.t('Unable to get the currently logged in user, GitHub Pull Requests will not work correctly')); Logger.error(`Failed to get current user: ${e}, ${e.message}`, CredentialStore.ID); + reject(e); }); }); github.currentUser = getUser.then(result => convertRESTUserToAccount(result.data)); @@ -520,7 +551,7 @@ const link = (url: string, token: string) => createHttpLink({ uri: `${url}/graphql`, // https://github.com/apollographql/apollo-link/issues/513 - fetch: fetch as any, + fetch: fetch as (((input: URL | string, init?: RequestInit) => Promise) | undefined), }), ); 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/folderRepositoryManager.ts b/src/github/folderRepositoryManager.ts index e9ced0e55f..5a6e300c7d 100644 --- a/src/github/folderRepositoryManager.ts +++ b/src/github/folderRepositoryManager.ts @@ -6,6 +6,27 @@ import * as nodePath from 'path'; import { bulkhead } from 'cockatiel'; import * as vscode from 'vscode'; +import { OctokitCommon } from './common'; +import { ConflictModel } from './conflictGuide'; +import { ConflictResolutionCoordinator } from './conflictResolutionCoordinator'; +import { Conflict, ConflictResolutionModel } from './conflictResolutionModel'; +import { CredentialStore } from './credentials'; +import { CopilotWorkingStatus, GitHubRepository, ItemsData, PULL_REQUEST_PAGE_SIZE, PullRequestChangeEvent, PullRequestData, TeamReviewerRefreshKind, ViewerPermission } from './githubRepository'; +import { PullRequestResponse, PullRequestState } from './graphql'; +import { IAccount, ILabel, IMilestone, IProject, IPullRequestsPagingOptions, Issue, ITeam, MergeMethod, PRType, PullRequestMergeability, RepoAccessAndMergeMethods, User } from './interface'; +import { IssueModel } from './issueModel'; +import { PullRequestGitHelper, PullRequestMetadata } from './pullRequestGitHelper'; +import { IResolvedPullRequestModel, PullRequestModel } from './pullRequestModel'; +import { + convertRESTIssueToRawPullRequest, + convertRESTPullRequestToRawPullRequest, + getOverrideBranch, + getPRFetchQuery, + loginComparator, + parseGraphQLPullRequest, + teamComparator, + variableSubstitution, +} from './utils'; import type { Branch, Commit, Repository, UpstreamRef } from '../api/api'; import { GitApiImpl, GitErrorCodes } from '../api/api1'; import { GitHubManager } from '../authentication/githubServer'; @@ -13,15 +34,15 @@ import { AuthProvider, GitHubServerType } from '../common/authentication'; import { commands, contexts } from '../common/executeCommands'; import { InMemFileChange, SlimFileChange } from '../common/file'; import { findLocalRepoRemoteFromGitHubRef } from '../common/githubRef'; -import { Disposable } from '../common/lifecycle'; +import { Disposable, disposeAll } from '../common/lifecycle'; import Logger from '../common/logger'; import { Protocol, ProtocolType } from '../common/protocol'; import { GitHubRemote, parseRemote, parseRepositoryRemotes, Remote } from '../common/remote'; import { ALLOW_FETCH, AUTO_STASH, - DEFAULT_MERGE_METHOD, GIT, + POST_DONE, PR_SETTINGS_NAMESPACE, PULL_BEFORE_CHECKOUT, PULL_BRANCH, @@ -29,36 +50,14 @@ import { UPSTREAM_REMOTE, } from '../common/settingKeys'; import { ITelemetry } from '../common/telemetry'; -import { EventType, TimelineEvent } from '../common/timelineEvent'; +import { EventType } from '../common/timelineEvent'; import { Schemes } from '../common/uri'; -import { batchPromiseAll, compareIgnoreCase, formatError, Predicate } from '../common/utils'; +import { AsyncPredicate, batchPromiseAll, compareIgnoreCase, formatError, Predicate } from '../common/utils'; import { PULL_REQUEST_OVERVIEW_VIEW_TYPE } from '../common/webview'; -import { LAST_USED_EMAIL, NEVER_SHOW_PULL_NOTIFICATION, REPO_KEYS, ReposState } from '../extensionState'; +import { BRANCHES_ASSOCIATED_WITH_PRS, LAST_USED_EMAIL, NEVER_SHOW_PULL_NOTIFICATION, REPO_KEYS, ReposState } from '../extensionState'; import { git } from '../gitProviders/gitCommands'; +import { IThemeWatcher } from '../themeWatcher'; import { CreatePullRequestHelper } from '../view/createPullRequestHelper'; -import { OctokitCommon } from './common'; -import { ConflictModel } from './conflictGuide'; -import { ConflictResolutionCoordinator } from './conflictResolutionCoordinator'; -import { Conflict, ConflictResolutionModel } from './conflictResolutionModel'; -import { CredentialStore } from './credentials'; -import { GitHubRepository, GraphQLError, GraphQLErrorType, IMetadata, ItemsData, PULL_REQUEST_PAGE_SIZE, PullRequestData, TeamReviewerRefreshKind, ViewerPermission } from './githubRepository'; -import { MergeMethod as GraphQLMergeMethod, MergePullRequestInput, MergePullRequestResponse, PullRequestResponse, PullRequestState, UserResponse } from './graphql'; -import { IAccount, ILabel, IMilestone, IProject, IPullRequestsPagingOptions, Issue, ITeam, MergeMethod, PRType, PullRequestMergeability, RepoAccessAndMergeMethods, User } from './interface'; -import { IssueModel } from './issueModel'; -import { PullRequestGitHelper, PullRequestMetadata } from './pullRequestGitHelper'; -import { IResolvedPullRequestModel, PullRequestModel } from './pullRequestModel'; -import { - convertRESTIssueToRawPullRequest, - convertRESTPullRequestToRawPullRequest, - getOverrideBranch, - getPRFetchQuery, - loginComparator, - parseGraphQLPullRequest, - parseGraphQLTimelineEvents, - parseGraphQLUser, - teamComparator, - variableSubstitution, -} from './utils'; async function createConflictResolutionModel(pullRequest: PullRequestModel): Promise { const head = pullRequest.head; @@ -135,7 +134,7 @@ export class DetachedHeadError extends Error { } override get message() { - return vscode.l10n.t('{0} has a detached HEAD (create a branch first', this.repository.rootUri.toString()); + return vscode.l10n.t('{0} has a detached HEAD (create a branch first)', this.repository.rootUri.toString()); } } @@ -178,6 +177,7 @@ const CACHED_TEMPLATE_BODY = 'templateBody'; export class FolderRepositoryManager extends Disposable { static ID = 'FolderRepositoryManager'; + private _state: ReposManagerState = ReposManagerState.Initializing; private _activePullRequest?: PullRequestModel; private _activeIssue?: IssueModel; private _githubRepositories: GitHubRepository[]; @@ -193,11 +193,8 @@ export class FolderRepositoryManager extends Disposable { private _repositoryPageInformation: Map = new Map(); private _addedUpstreamCount: number = 0; - private _onDidMergePullRequest = this._register(new vscode.EventEmitter()); - readonly onDidMergePullRequest = this._onDidMergePullRequest.event; - - private _onDidChangeActivePullRequest = this._register(new vscode.EventEmitter<{ new: number | undefined, old: number | undefined }>()); - readonly onDidChangeActivePullRequest: vscode.Event<{ new: number | undefined, old: number | undefined }> = this._onDidChangeActivePullRequest.event; + private _onDidChangeActivePullRequest = this._register(new vscode.EventEmitter<{ new: PullRequestModel | undefined, old: PullRequestModel | undefined }>()); + readonly onDidChangeActivePullRequest: vscode.Event<{ new: PullRequestModel | undefined, old: PullRequestModel | undefined }> = this._onDidChangeActivePullRequest.event; private _onDidChangeActiveIssue = this._register(new vscode.EventEmitter()); readonly onDidChangeActiveIssue: vscode.Event = this._onDidChangeActiveIssue.event; @@ -213,6 +210,12 @@ export class FolderRepositoryManager extends Disposable { private _onDidChangeGithubRepositories = this._register(new vscode.EventEmitter()); readonly onDidChangeGithubRepositories: vscode.Event = this._onDidChangeGithubRepositories.event; + private _onDidChangePullRequestsEvents: vscode.Disposable[] = []; + private readonly _onDidChangeAnyPullRequests = this._register(new vscode.EventEmitter()); + readonly onDidChangeAnyPullRequests: vscode.Event = this._onDidChangeAnyPullRequests.event; + private readonly _onDidAddPullRequest = this._register(new vscode.EventEmitter()); + readonly onDidAddPullRequest: vscode.Event = this._onDidAddPullRequest.event; + private _onDidDispose = this._register(new vscode.EventEmitter()); readonly onDidDispose: vscode.Event = this._onDidDispose.event; @@ -225,7 +228,8 @@ export class FolderRepositoryManager extends Disposable { public readonly telemetry: ITelemetry, private readonly _git: GitApiImpl, private readonly _credentialStore: CredentialStore, - public readonly createPullRequestHelper: CreatePullRequestHelper + public readonly createPullRequestHelper: CreatePullRequestHelper, + public readonly themeWatcher: IThemeWatcher ) { super(); this._githubRepositories = []; @@ -240,6 +244,7 @@ export class FolderRepositoryManager extends Disposable { ); this._register(_credentialStore.onDidInitialize(() => this.updateRepositories())); + this._register({ dispose: () => disposeAll(this._onDidChangePullRequestsEvents) }); this.cleanStoredRepoState(); } @@ -354,7 +359,7 @@ export class FolderRepositoryManager extends Disposable { if (pullRequest === this._activePullRequest) { return; } - const oldNumber = this._activePullRequest?.number; + const oldPR = this._activePullRequest; if (this._activePullRequest) { this._activePullRequest.isActive = false; } @@ -363,10 +368,9 @@ export class FolderRepositoryManager extends Disposable { pullRequest.isActive = true; pullRequest.githubRepository.commentsHandler?.unregisterCommentController(pullRequest.number); } - const newNumber = pullRequest?.number; this._activePullRequest = pullRequest; - this._onDidChangeActivePullRequest.fire({ old: oldNumber, new: newNumber }); + this._onDidChangeActivePullRequest.fire({ old: oldPR, new: pullRequest }); } get repository(): Repository { @@ -420,6 +424,9 @@ export class FolderRepositoryManager extends Disposable { if (activeRemotes.length) { await vscode.commands.executeCommand('setContext', 'github:hasGitHubRemotes', true); Logger.appendLine(`Found GitHub remote for folder ${this.repository.rootUri.fsPath}`, this.id); + if (this._allGitHubRemotes.length > 1) { + await vscode.commands.executeCommand('setContext', 'github:hasMultipleGitHubRemotes', true); + } } else { Logger.appendLine(`No GitHub remotes found for folder ${this.repository.rootUri.fsPath}`, this.id); } @@ -453,7 +460,7 @@ export class FolderRepositoryManager extends Disposable { // good } else if ((enterpriseCount > 0) && this._credentialStore.isAuthenticated(AuthProvider.githubEnterprise)) { // also good - } else if (isAuthenticated) { + } else if (isAuthenticated && ((dotComCount > 0) || (enterpriseCount > 0))) { // Not good. We have a mismatch between auth type and server type. isAuthenticated = false; } @@ -461,6 +468,17 @@ export class FolderRepositoryManager extends Disposable { return isAuthenticated; } + get state(): ReposManagerState { + return this._state; + } + + private set state(state: ReposManagerState) { + if (state !== this._state) { + this._state = state; + this._onDidLoadRepositories.fire(state); + } + } + private async doUpdateRepositories(silent: boolean): Promise { if (this._git.state === 'uninitialized') { Logger.appendLine('Cannot updates repositories as git is uninitialized', this.id); @@ -471,9 +489,11 @@ 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) { - this._onDidLoadRepositories.fire(ReposManagerState.RepositoriesLoaded); + 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; } } @@ -524,7 +544,12 @@ export class FolderRepositoryManager extends Disposable { } } + disposeAll(this._onDidChangePullRequestsEvents); this._githubRepositories = repositories; + for (const repo of this._githubRepositories) { + this._onDidChangePullRequestsEvents.push(repo.onDidChangePullRequests(e => this._onDidChangeAnyPullRequests.fire(e))); + this._onDidChangePullRequestsEvents.push(repo.onDidAddPullRequest(e => this._onDidAddPullRequest.fire(e))); + } oldRepositories.filter(old => this._githubRepositories.indexOf(old) < 0).forEach(repo => repo.dispose()); const repositoriesAdded = @@ -550,9 +575,14 @@ export class FolderRepositoryManager extends Disposable { this.getAssignableUsers(repositoriesAdded.length > 0); if (isAuthenticated && activeRemotes.length) { - this._onDidLoadRepositories.fire(ReposManagerState.RepositoriesLoaded); + this.state = ReposManagerState.RepositoriesLoaded; + // On first activation, associate local branches with PRs + // Do this asynchronously to not block the main flow + this.associateLocalBranchesWithPRsOnFirstActivation().catch(e => { + Logger.error(`Failed to associate branches with PRs: ${e}`, this.id); + }); } else if (!isAuthenticated) { - this._onDidLoadRepositories.fire(ReposManagerState.NeedsAuthentication); + this.state = ReposManagerState.NeedsAuthentication; } if (!silent) { this._onDidChangeRepositories.fire({ added: repositoriesAdded.length > 0 }); @@ -928,6 +958,112 @@ export class FolderRepositoryManager extends Disposable { return models.filter(value => value !== undefined) as PullRequestModel[]; } + /** + * On first activation, iterate through local branches and associate them with PRs if they match. + * This helps discover PRs that were created before the extension was installed or in other ways. + */ + private async associateLocalBranchesWithPRsOnFirstActivation(): Promise { + const stateKey = `${BRANCHES_ASSOCIATED_WITH_PRS}.${this.repository.rootUri.fsPath}`; + const hasRun = this.context.globalState.get(stateKey, false); + + if (hasRun) { + Logger.debug('Branch association has already run for this workspace folder', this.id); + return; + } + + Logger.appendLine('First activation: associating local branches with PRs', this.id); + + const githubRepositories = this._githubRepositories; + if (!githubRepositories || !githubRepositories.length || !this.repository.getRefs) { + Logger.debug('No GitHub repositories or getRefs not available, skipping branch association', this.id); + await this.context.globalState.update(stateKey, true); + return; + } + + try { + // Only check the 3 most recently used branches to minimize API calls + const localBranches = (await this.repository.getRefs({ + pattern: 'refs/heads/', + sort: 'committerdate', + count: 10 + })) + .filter(r => r.name !== undefined) + .map(r => r.name!); + + Logger.debug(`Found ${localBranches.length} local branches to check`, this.id); + + const associationResults: boolean[] = []; + + // Process all branches (max 3) in parallel + const chunkResults = await Promise.all(localBranches.map(async branchName => { + try { + // Check if this branch already has PR metadata + const existingMetadata = await PullRequestGitHelper.getMatchingPullRequestMetadataForBranch( + this.repository, + branchName, + ); + + if (existingMetadata) { + // Branch already has PR metadata, skip + return false; + } + + // Get the branch to check its upstream + const branch = await this.repository.getBranch(branchName); + if (!branch.upstream) { + // No upstream, can't match to a PR + return false; + } + + // Try to find a matching PR on GitHub + const remoteName = branch.upstream.remote; + const upstreamBranchName = branch.upstream.name; + + const githubRepo = githubRepositories.find( + repo => repo.remote.remoteName === remoteName, + ); + + if (!githubRepo) { + return false; + } + + // Get the metadata of the GitHub repository to find owner + const metadata = await githubRepo.getMetadata(); + if (!metadata?.owner) { + return false; + } + + // Search for a PR with this head branch + const matchingPR = await githubRepo.getPullRequestForBranch(upstreamBranchName, metadata.owner.login); + + if (matchingPR) { + Logger.appendLine(`Found PR #${matchingPR.number} for branch ${branchName}, associating...`, this.id); + await PullRequestGitHelper.associateBranchWithPullRequest( + this.repository, + matchingPR, + branchName, + ); + return true; + } + return false; + } catch (e) { + Logger.debug(`Error checking branch ${branchName}: ${e}`, this.id); + // Continue with other branches even if one fails + return false; + } + })); + associationResults.push(...chunkResults); + + const associatedCount = associationResults.filter(r => r).length; + Logger.appendLine(`Branch association complete: ${associatedCount} branches associated with PRs`, this.id); + } catch (e) { + Logger.error(`Error during branch association: ${e}`, this.id); + } finally { + // Mark as complete even if there were errors + await this.context.globalState.update(stateKey, true); + } + } + async getLabels(issue?: IssueModel, repoInfo?: { owner: string; repo: string }): Promise { const repo = issue ? issue.githubRepository @@ -1049,8 +1185,8 @@ export class FolderRepositoryManager extends Disposable { } let pagesFetched = 0; - const itemData: ItemsData = { hasMorePages: false, items: [], totalCount: 0 }; - const addPage = (page: PullRequestData | undefined) => { + const itemData: ItemsData = { hasMorePages: false, items: [], totalCount: 0 }; + const addPage = (page: ItemsData | undefined) => { pagesFetched++; if (page) { itemData.items = itemData.items.concat(page.items); @@ -1059,7 +1195,17 @@ export class FolderRepositoryManager extends Disposable { } }; + const activeGitHubRemotes = await this.getActiveGitHubRemotes(this._allGitHubRemotes); + + // Check if user has explicitly configured remotes (not using defaults) + const remotesConfig = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).inspect(REMOTES); + const hasUserConfiguredRemotes = !!(remotesConfig?.globalValue || remotesConfig?.workspaceValue || remotesConfig?.workspaceFolderValue); + 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); @@ -1080,7 +1226,7 @@ export class FolderRepositoryManager extends Disposable { pageNumber: number, ): Promise<{ items: any[]; hasMorePages: boolean, totalCount?: number } | undefined> => { // Resolve variables in the query with each repo - const resolvedQuery = query ? await variableSubstitution(query, undefined, + const resolvedQuery = query ? variableSubstitution(query, undefined, { base: await githubRepository.getDefaultBranch(), owner: githubRepository.remote.owner, repo: githubRepository.remote.repositoryName }) : undefined; switch (pagedDataType) { case PagedDataType.PullRequest: { @@ -1117,15 +1263,17 @@ export class FolderRepositoryManager extends Disposable { pageInformation.hasMorePages = itemData.hasMorePages; - // Break early if + // Determine if we should break early from the loop: // 1) we've received data AND // 2) either we're fetching just the next page (case 2) // OR we're fetching all (cases 1&3), and we've fetched as far as we had previously (or further, in case 1). - if ( - itemData.items.length && - (options.fetchNextPage || - ((options.fetchNextPage === false) && !options.fetchOnePagePerRepo && (pagesFetched >= getTotalFetchedPages()))) - ) { + // 3) AND the user hasn't explicitly configured remotes (if they have, we should search all of them) + const hasReceivedData = itemData.items.length > 0; + const isFetchingNextPage = options.fetchNextPage; + const hasReachedPreviousFetchLimit = (options.fetchNextPage === false) && !options.fetchOnePagePerRepo && (pagesFetched >= getTotalFetchedPages()); + const shouldBreakEarly = hasReceivedData && (isFetchingNextPage || hasReachedPreviousFetchLimit) && !hasUserConfiguredRemotes; + + if (shouldBreakEarly) { if (getTotalFetchedPages() === 0) { // We're in case 1, manually set number of pages we looked through until we found first results. setTotalFetchedPages(pagesFetched); @@ -1142,24 +1290,28 @@ export class FolderRepositoryManager extends Disposable { return { items: itemData.items, - hasMorePages: false, + hasMorePages: itemData.hasMorePages, hasUnsearchedRepositories: false, totalCount: itemData.totalCount }; } async getPullRequestsForCategory(githubRepository: GitHubRepository, categoryQuery: string, page?: number): Promise { - let repo: IMetadata | undefined; try { Logger.debug(`Fetch pull request category ${categoryQuery} - enter`, this.id); const { octokit, query, schema } = await githubRepository.ensure(); - const user = await githubRepository.getAuthenticatedUser(); - // Search api will not try to resolve repo that redirects, so get full name first - repo = await githubRepository.getMetadata(); + /* __GDPR__ + "pr.search.category" : { + } + */ + this.telemetry.sendTelemetryEvent('pr.search.category'); + + const user = (await githubRepository.getAuthenticatedUser()).login; const { data, headers } = await octokit.call(octokit.api.search.issuesAndPullRequests, { q: getPRFetchQuery(user, categoryQuery), per_page: PULL_REQUEST_PAGE_SIZE, + advanced_search: 'true', page: page || 1, }); @@ -1181,8 +1333,8 @@ export class FolderRepositoryManager extends Disposable { const hasMorePages = !!headers.link && headers.link.indexOf('rel="next"') > -1; const pullRequestResponses = await Promise.all(promises); - const pullRequests = pullRequestResponses - .map(response => { + const pullRequests = (await Promise.all(pullRequestResponses + .map(async response => { if (!response?.data.repository) { Logger.appendLine('Pull request doesn\'t appear to exist.', this.id); return null; @@ -1191,9 +1343,9 @@ export class FolderRepositoryManager extends Disposable { // Pull requests fetched with a query can be from any repo. // We need to use the correct GitHubRepository for this PR. return response.repo.createOrUpdatePullRequestModel( - parseGraphQLPullRequest(response.data.repository.pullRequest, response.repo), + await parseGraphQLPullRequest(response.data.repository.pullRequest, response.repo), true ); - }) + }))) .filter(item => item !== null) as PullRequestModel[]; Logger.debug(`Fetch pull request category ${categoryQuery} - done`, this.id); @@ -1208,7 +1360,7 @@ export class FolderRepositoryManager extends Disposable { if (e.status === 404) { // not found vscode.window.showWarningMessage( - `Fetching pull requests for remote ${githubRepository.remote.remoteName} with query failed, please check if the repo ${repo?.full_name} is valid.`, + `Fetching pull requests for remote ${githubRepository.remote.remoteName} with query failed, please check if the repo ${githubRepository.remote.owner}/${githubRepository.remote.repositoryName} is valid.`, ); } else { throw e; @@ -1271,13 +1423,13 @@ export class FolderRepositoryManager extends Disposable { * Pull request defaults in the query, like owner and repository variables, will be resolved. */ async getIssues( - query?: string, + query?: string, options: IPullRequestsPagingOptions = { fetchNextPage: false, fetchOnePagePerRepo: false } ): Promise | undefined> { if (this.gitHubRepositories.length === 0) { return undefined; } try { - const data = await this.fetchPagedData({ fetchNextPage: false, fetchOnePagePerRepo: false }, `issuesKey${query}`, PagedDataType.IssueSearch, PRType.All, query); + const data = await this.fetchPagedData(options, `issuesKey${query}`, PagedDataType.IssueSearch, PRType.All, query); const mappedData: ItemsResponseResult = { items: [], hasMorePages: data.hasMorePages, @@ -1286,7 +1438,7 @@ export class FolderRepositoryManager extends Disposable { }; for (const issue of data.items) { const githubRepository = await this.getRepoForIssue(issue); - mappedData.items.push(new IssueModel(githubRepository, githubRepository.remote, issue)); + mappedData.items.push(new IssueModel(this.telemetry, githubRepository, githubRepository.remote, issue)); } return mappedData; } catch (e) { @@ -1475,7 +1627,7 @@ export class FolderRepositoryManager extends Disposable { ? first // I GUESS THAT'S WHAT WE'RE GOING WITH, THEN. : // Otherwise, let's try... this.findRepo(byRemoteName('origin')) || // by convention - this.findRepo(ownedByMe) || // bc maybe we can push there + await this.findRepoAsync(ownedByMe) || // bc maybe we can push there first; // out of raw desperation } @@ -1483,6 +1635,17 @@ export class FolderRepositoryManager extends Disposable { return this._githubRepositories.filter(where)[0]; } + findRepoAsync(where: AsyncPredicate): Promise { + return (async () => { + for (const repo of this._githubRepositories) { + if (await where(repo)) { + return repo; + } + } + return undefined; + })(); + } + get upstreamRef(): UpstreamRef | undefined { const { HEAD } = this.repository.state; return HEAD && HEAD.upstream; @@ -1583,7 +1746,7 @@ export class FolderRepositoryManager extends Disposable { // Create PR const { data } = await repo.octokit.call(repo.octokit.api.issues.create, params); const item = convertRESTIssueToRawPullRequest(data, repo); - const issueModel = new IssueModel(repo, repo.remote, item); + const issueModel = new IssueModel(this.telemetry, repo, repo.remote, item); /* __GDPR__ "issue.create.success" : { @@ -1649,108 +1812,17 @@ export class FolderRepositoryManager extends Disposable { return this._credentialStore.getCurrentUser(githubRepository.remote.authProviderId); } - async mergePullRequest( - pullRequest: PullRequestModel, - title?: string, - description?: string, - method?: 'merge' | 'squash' | 'rebase', - email?: string, - ): Promise<{ merged: boolean, message: string, timeline?: TimelineEvent[] }> { - Logger.debug(`Merging PR: ${pullRequest.number} method: ${method} for user: "${email}" - enter`, this.id); - const { mutate, schema } = await pullRequest.githubRepository.ensure(); - - const activePRSHA = this.activePullRequest && this.activePullRequest.head && this.activePullRequest.head.sha; - const workingDirectorySHA = this.repository.state.HEAD && this.repository.state.HEAD.commit; - const mergingPRSHA = pullRequest.head && pullRequest.head.sha; - const workingDirectoryIsDirty = this.repository.state.workingTreeChanges.length > 0; - let expectedHeadOid: string | undefined = pullRequest.head?.sha; - - if (activePRSHA === mergingPRSHA) { - // We're on the branch of the pr being merged. - expectedHeadOid = workingDirectorySHA; - if (workingDirectorySHA !== mergingPRSHA) { - // We are looking at different commit than what will be merged - const { ahead } = this.repository.state.HEAD!; - const pluralMessage = vscode.l10n.t('You have {0} unpushed commits on this PR branch.\n\nWould you like to proceed anyway?', ahead ?? 'unknown'); - const singularMessage = vscode.l10n.t('You have 1 unpushed commit on this PR branch.\n\nWould you like to proceed anyway?'); - if (ahead && - (await vscode.window.showWarningMessage( - ahead > 1 ? pluralMessage : singularMessage, - { modal: true }, - vscode.l10n.t('Yes'), - )) === undefined) { - - return { - merged: false, - message: vscode.l10n.t('unpushed changes'), - }; - } - } - - if (workingDirectoryIsDirty) { - // We have made changes to the PR that are not committed - if ( - (await vscode.window.showWarningMessage( - vscode.l10n.t('You have uncommitted changes on this PR branch.\n\n Would you like to proceed anyway?'), - { modal: true }, - vscode.l10n.t('Yes'), - )) === undefined - ) { - return { - merged: false, - message: vscode.l10n.t('uncommitted changes'), - }; - } - } - } - const input: MergePullRequestInput = { - pullRequestId: pullRequest.graphNodeId, - commitHeadline: title, - commitBody: description, - expectedHeadOid, - authorEmail: email, - mergeMethod: - (method?.toUpperCase() ?? - vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<'merge' | 'squash' | 'rebase'>(DEFAULT_MERGE_METHOD, 'merge')?.toUpperCase()) as GraphQLMergeMethod, - }; - - return mutate({ - mutation: schema.MergePullRequest, - variables: { - input - } - }) - .then(result => { - Logger.debug(`Merging PR: ${pullRequest.number}} - done`, this.id); - - /* __GDPR__ - "pr.merge.success" : {} - */ - this.telemetry.sendTelemetryEvent('pr.merge.success'); - this._onDidMergePullRequest.fire(); - return { merged: true, message: '', timeline: parseGraphQLTimelineEvents(result.data?.mergePullRequest.pullRequest.timelineItems.nodes ?? [], pullRequest.githubRepository) }; - }) - .catch(e => { - /* __GDPR__ - "pr.merge.failure" : {} - */ - this.telemetry.sendTelemetryErrorEvent('pr.merge.failure'); - const graphQLErrors = e.graphQLErrors as GraphQLError[] | undefined; - if (graphQLErrors?.length && graphQLErrors.find(error => error.type === GraphQLErrorType.Unprocessable && error.message?.includes('Head branch was modified'))) { - return { merged: false, message: vscode.l10n.t('Head branch was modified. Pull, review, then try again.') }; - } else { - throw e; - } - }); - } - async deleteBranch(pullRequest: PullRequestModel) { await pullRequest.githubRepository.deleteBranch(pullRequest); } private async getBranchDeletionItems() { + interface BranchDeletionMetadata extends PullRequestMetadata { + isOpen?: boolean; + } + const allConfigs = await this.repository.getConfigs(); - const branchInfos: Map = new Map(); + const branchInfos: Map = new Map(); allConfigs.forEach(config => { const key = config.key; @@ -1763,14 +1835,23 @@ export class FolderRepositoryManager extends Disposable { branchInfos.set(branchName, {}); } - const value = branchInfos.get(branchName); + const value = branchInfos.get(branchName)!; if (matches[2] === 'remote') { - value!['remote'] = config.value; + value['remote'] = config.value; } if (matches[2] === 'github-pr-owner-number') { const metadata = PullRequestGitHelper.parsePullRequestMetadata(config.value); - value!['metadata'] = metadata; + if (!value?.metadata) { + value['metadata'] = []; + } + if (metadata) { + // Check if the metadata already exists in the array + const existingMetadata = value.metadata.find(m => m.owner === metadata.owner && m.repositoryName === metadata.repositoryName && m.prNumber === metadata.prNumber); + if (!existingMetadata) { + value['metadata'].push(metadata); + } + } } branchInfos.set(branchName, value!); @@ -1779,26 +1860,24 @@ export class FolderRepositoryManager extends Disposable { Logger.debug(`Found ${branchInfos.size} possible branches to delete`, this.id); Logger.trace(`Branches to delete: ${JSON.stringify(Array.from(branchInfos.keys()))}`, this.id); - const actions: (vscode.QuickPickItem & { metadata: PullRequestMetadata; legacy?: boolean })[] = []; + const actions: (vscode.QuickPickItem & { metadata: BranchDeletionMetadata[]; legacy?: boolean })[] = []; branchInfos.forEach((value, key) => { if (value.metadata) { const activePRUrl = this.activePullRequest && this.activePullRequest.base.repositoryCloneUrl; - const matchesActiveBranch = activePRUrl - ? (activePRUrl.owner === value.metadata.owner && - activePRUrl.repositoryName === value.metadata.repositoryName && - this.activePullRequest?.number === value.metadata.prNumber) - : false; + const activeMetadata = value.metadata.find(metadata => + metadata.owner === activePRUrl?.owner && + metadata.repositoryName === activePRUrl?.repositoryName && + metadata.prNumber === this.activePullRequest?.number + ); - if (!matchesActiveBranch) { + if (!activeMetadata) { actions.push({ label: `${key}`, - description: `${value.metadata!.repositoryName}/${value.metadata!.owner} #${value.metadata.prNumber - }`, picked: false, - metadata: value.metadata!, + metadata: value.metadata, }); } else { - Logger.debug(`Skipping ${value.metadata.prNumber}, active PR is #${this.activePullRequest?.number}`, this.id); + Logger.debug(`Skipping ${activeMetadata.prNumber}, active PR is #${this.activePullRequest?.number}`, this.id); Logger.trace(`Skipping active branch ${key}`, this.id); } } @@ -1806,40 +1885,51 @@ export class FolderRepositoryManager extends Disposable { const results = await Promise.all( actions.map(async action => { - const metadata = action.metadata; - const githubRepo = this._githubRepositories.find( - repo => - repo.remote.owner.toLowerCase() === metadata!.owner.toLowerCase() && - repo.remote.repositoryName.toLowerCase() === metadata!.repositoryName.toLowerCase(), - ); + const allOld = (await Promise.all( + action.metadata.map(async metadata => { + const githubRepo = this._githubRepositories.find( + repo => + repo.remote.owner.toLowerCase() === metadata!.owner.toLowerCase() && + repo.remote.repositoryName.toLowerCase() === metadata!.repositoryName.toLowerCase(), + ); + + if (!githubRepo) { + return action; + } - if (!githubRepo) { - return action; + const { remote, query, schema } = await githubRepo.ensure(); + try { + const { data } = await query({ + query: schema.PullRequestState, + variables: { + owner: remote.owner, + name: remote.repositoryName, + number: metadata!.prNumber, + }, + }); + metadata.isOpen = data.repository?.pullRequest.state === 'OPEN'; + return data.repository?.pullRequest.state !== 'OPEN'; + } catch { } + return false; + }))).every(result => result); + if (allOld) { + action.legacy = true; } - const { remote, query, schema } = await githubRepo.ensure(); - try { - const { data } = await query({ - query: schema.PullRequestState, - variables: { - owner: remote.owner, - name: remote.repositoryName, - number: metadata!.prNumber, - }, - }); - - action.legacy = data.repository?.pullRequest.state !== 'OPEN'; - } catch { } - return action; }), ); results.forEach(result => { + if (result.metadata.length === 0) { + return; + } + result.description = `${result.metadata[0].repositoryName}/${result.metadata[0].owner} ${result.metadata.map(metadata => { + const prString = `#${metadata.prNumber}`; + return metadata.isOpen ? vscode.l10n.t('{0} is open', prString) : prString; + }).join(', ')}`; if (result.legacy) { result.picked = true; - } else { - result.description = vscode.l10n.t('{0} is still Open', result.description!); } }); @@ -2011,7 +2101,7 @@ export class FolderRepositoryManager extends Disposable { quickPick.items = results; quickPick.selectedItems = results.filter(result => { // Do not pick the default branch for the repo. - return result.picked && !((result.label === defaults.base) && (result.metadata.owner === defaults.owner) && (result.metadata.repositoryName === defaults.repo)); + return result.picked && !((result.label === defaults.base) && (result.metadata.find(metadata => metadata.owner === defaults.owner && metadata.repositoryName === defaults.repo))); }); quickPick.busy = false; if (results.length === 0) { @@ -2162,12 +2252,13 @@ export class FolderRepositoryManager extends Disposable { owner: string, repositoryName: string, pullRequestNumber: number, + useCache: boolean = false, ): Promise { const githubRepo = await this.resolveItem(owner, repositoryName); - Logger.appendLine(`Found GitHub repo for pr #${pullRequestNumber}: ${githubRepo ? 'yes' : 'no'}`, this.id); + Logger.trace(`Found GitHub repo for pr #${pullRequestNumber}: ${githubRepo ? 'yes' : 'no'}`, this.id); if (githubRepo) { - const pr = await githubRepo.getPullRequest(pullRequestNumber); - Logger.appendLine(`Found GitHub pr repo for pr #${pullRequestNumber}: ${pr ? 'yes' : 'no'}`, this.id); + const pr = await githubRepo.getPullRequest(pullRequestNumber, useCache); + Logger.trace(`Found GitHub pr repo for pr #${pullRequestNumber}: ${pr ? 'yes' : 'no'}`, this.id); return pr; } return undefined; @@ -2178,10 +2269,14 @@ export class FolderRepositoryManager extends Disposable { repositoryName: string, pullRequestNumber: number, withComments: boolean = false, + useCache: boolean = false ): Promise { const githubRepo = await this.resolveItem(owner, repositoryName); + Logger.trace(`Found GitHub repo for issue #${pullRequestNumber}: ${githubRepo ? 'yes' : 'no'}`, this.id); if (githubRepo) { - return githubRepo.getIssue(pullRequestNumber, withComments); + const issue = await githubRepo.getIssue(pullRequestNumber, withComments, useCache); + Logger.trace(`Found GitHub issue repo for issue #${pullRequestNumber}: ${issue ? 'yes' : 'no'}`, this.id); + return issue; } return undefined; } @@ -2189,23 +2284,7 @@ export class FolderRepositoryManager extends Disposable { async resolveUser(owner: string, repositoryName: string, login: string): Promise { Logger.debug(`Fetch user ${login}`, this.id); const githubRepository = await this.createGitHubRepositoryFromOwnerName(owner, repositoryName); - const { query, schema } = await githubRepository.ensure(); - - try { - const { data } = await query({ - query: schema.GetUser, - variables: { - login, - }, - }); - return parseGraphQLUser(data, githubRepository); - } catch (e) { - // Ignore cases where the user doesn't exist - if (!(e.message as (string | undefined))?.startsWith('GraphQL error: Could not resolve to a User with the login of')) { - Logger.warn(e.message); - } - } - return undefined; + return githubRepository.resolveUser(login); } async getMatchingPullRequestMetadataForBranch() { @@ -2422,6 +2501,34 @@ export class FolderRepositoryManager extends Disposable { } public async checkoutDefaultBranch(branch: string): Promise { + const CHECKOUT_DEFAULT_BRANCH = 'checkoutDefaultBranch'; + const CHECKOUT_DEFAULT_BRANCH_AND_PULL = 'checkoutDefaultBranchAndPull'; + + const postDoneAction = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(POST_DONE, CHECKOUT_DEFAULT_BRANCH); + + if (postDoneAction === CHECKOUT_DEFAULT_BRANCH_AND_PULL) { + await this.checkoutDefaultBranchAndPull(branch); + } else { + await this.checkoutDefaultBranchOnly(branch); + } + } + + private async checkoutDefaultBranchAndPull(branch: string): Promise { + await this.checkoutDefaultBranchOnly(branch); + // After checking out, pull the latest changes if the branch has an upstream + try { + const branchObj = await this.repository.getBranch(branch); + if (branchObj.upstream) { + Logger.debug(`Pulling latest changes for branch ${branch}`, this.id); + await this.repository.pull(); + } + } catch (e) { + Logger.warn(`Failed to pull latest changes for branch ${branch}: ${e}`, this.id); + // Don't throw error - checkout succeeded, pull failure is non-critical + } + } + + private async checkoutDefaultBranchOnly(branch: string): Promise { let branchObj: Branch | undefined; try { branchObj = await this.repository.getBranch(branch); @@ -2508,6 +2615,12 @@ export class FolderRepositoryManager extends Disposable { private async promptPullBrach(pr: PullRequestModel, branch: Branch, autoStashSetting?: boolean) { if (!this._updateMessageShown || autoStashSetting) { + // When the PR is from Copilot, we only want to show the notification when Copilot is done working + const copilotStatus = await pr.copilotWorkingStatus(); + if (copilotStatus === CopilotWorkingStatus.InProgress) { + return; + } + this._updateMessageShown = true; const pull = vscode.l10n.t('Pull'); const always = vscode.l10n.t('Always Pull'); @@ -2701,7 +2814,7 @@ export class FolderRepositoryManager extends Disposable { await matchingRepo.addRemote(workingRemoteName, result); // Now the extension is responding to all the git changes. await new Promise(resolve => { - if (this.gitHubRepositories.length === startingRepoCount) { + if ((this.gitHubRepositories.length === startingRepoCount) && vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(REMOTES)?.includes('upstream')) { const disposable = this.onDidChangeRepositories(() => { if (this.gitHubRepositories.length > startingRepoCount) { disposable.dispose(); @@ -2874,9 +2987,8 @@ export function getEventType(text: string) { } } -const ownedByMe: Predicate = repo => { - const { currentUser = null } = repo.octokit as any; - return currentUser && repo.remote.owner === currentUser.login; +const ownedByMe: AsyncPredicate = async repo => { + return repo.isCurrentUser(repo.remote.authProviderId, repo.remote.owner); }; export const byRemoteName = (name: string): Predicate => ({ remote: { remoteName } }) => diff --git a/src/github/githubRepository.ts b/src/github/githubRepository.ts index 39b7d274b1..a08bd61e93 100644 --- a/src/github/githubRepository.ts +++ b/src/github/githubRepository.ts @@ -4,15 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as buffer from 'buffer'; -import { ApolloQueryResult, DocumentNode, FetchResult, MutationOptions, NetworkStatus, QueryOptions } from 'apollo-boost'; +import { ApolloQueryResult, DocumentNode, FetchResult, MutationOptions, NetworkStatus, OperationVariables, QueryOptions } from 'apollo-boost'; +import LRUCache from 'lru-cache'; import * as vscode from 'vscode'; -import { AuthenticationError, AuthProvider, GitHubServerType, isSamlError } from '../common/authentication'; -import { Disposable } from '../common/lifecycle'; -import Logger from '../common/logger'; -import { GitHubRemote, parseRemote } from '../common/remote'; -import { ITelemetry } from '../common/telemetry'; -import { PRCommentControllerRegistry } from '../view/pullRequestCommentControllerRegistry'; -import { mergeQuerySchemaWithShared, OctokitCommon, Schema } from './common'; +import { mergeQuerySchemaWithShared, OctokitCommon } from './common'; import { CredentialStore, GitHub } from './credentials'; import { AssignableUsersResponse, @@ -39,6 +34,7 @@ import { RepoProjectsResponse, RevertPullRequestResponse, SuggestedActorsResponse, + UserResponse, ViewerPermissionResponse, } from './graphql'; import { @@ -53,8 +49,9 @@ import { PullRequestChecks, PullRequestReviewRequirement, RepoAccessAndMergeMethods, + User, } from './interface'; -import { IssueModel } from './issueModel'; +import { IssueChangeEvent, IssueModel } from './issueModel'; import { LoggingOctokit } from './loggingOctokit'; import { PullRequestModel } from './pullRequestModel'; import defaultSchema from './queries.gql'; @@ -69,31 +66,48 @@ import { parseAccount, parseGraphQLIssue, parseGraphQLPullRequest, + parseGraphQLUser, parseGraphQLViewerPermission, parseMergeMethod, parseMilestone, + restPaginate, } from './utils'; +import { AuthenticationError, AuthProvider, GitHubServerType, isSamlError } from '../common/authentication'; + +import { Disposable, disposeAll } from '../common/lifecycle'; + +import Logger from '../common/logger'; +import { GitHubRemote, parseRemote } from '../common/remote'; + + +import { BRANCH_LIST_TIMEOUT, PR_SETTINGS_NAMESPACE } from '../common/settingKeys'; +import { ITelemetry } from '../common/telemetry'; + +import { PullRequestCommentController } from '../view/pullRequestCommentController'; + +import { PRCommentControllerRegistry } from '../view/pullRequestCommentControllerRegistry'; + export const PULL_REQUEST_PAGE_SIZE = 20; const GRAPHQL_COMPONENT_ID = 'GraphQL'; -export interface ItemsData { - items: any[]; +export interface ItemsData { + items: T[]; hasMorePages: boolean; totalCount?: number; } -export interface IssueData extends ItemsData { +export interface IssueData extends ItemsData { items: Issue[]; hasMorePages: boolean; } -export interface PullRequestData extends ItemsData { +export interface PullRequestData extends ItemsData { items: PullRequestModel[]; } -export interface MilestoneData extends ItemsData { +export interface MilestoneData extends ItemsData<{ milestone: IMilestone; issues: IssueModel[] }> { items: { milestone: IMilestone; issues: IssueModel[] }[]; hasMorePages: boolean; } @@ -123,9 +137,7 @@ export interface ForkDetails { }; } -export interface IMetadata extends OctokitCommon.ReposGetResponseData { - currentUser: any; -} +export type IMetadata = OctokitCommon.ReposGetResponseData; export enum GraphQLErrorType { Unprocessable = 'UNPROCESSABLE', @@ -139,6 +151,18 @@ export interface GraphQLError { message?: string; } +export enum CopilotWorkingStatus { + NotCopilotIssue = 'NotCopilotIssue', + InProgress = 'InProgress', + Error = 'Error', + Done = 'Done', +} + +export interface PullRequestChangeEvent { + model: IssueModel; + event: IssueChangeEvent; +} + export class GitHubRepository extends Disposable { static ID = 'GitHubRepository'; protected _initialized: boolean = false; @@ -146,12 +170,29 @@ export class GitHubRepository extends Disposable { protected _metadata: Promise | undefined; public commentsController?: vscode.CommentController; public commentsHandler?: PRCommentControllerRegistry; - private _pullRequestModels = new Map(); + private _pullRequestModelsByNumber: LRUCache = new LRUCache({ + maxAge: 1000 * 60 * 60 * 4 /* 4 hours */, stale: true, updateAgeOnGet: true, + dispose: (_key, value) => { + disposeAll(value.disposables); + value.model.dispose(); + } + }); + private _issueModelsByNumber: LRUCache = new LRUCache({ + maxAge: 1000 * 60 * 60 * 4 /* 4 hours */, stale: true, updateAgeOnGet: true, + dispose: (_key, value) => { + disposeAll(value.disposables); + value.model.dispose(); + } + }); + // eslint-disable-next-line rulesdir/no-any-except-union-method-signature private _queriesSchema: any; private _areQueriesLimited: boolean = false; + get areQueriesLimited(): boolean { return this._areQueriesLimited; } private _onDidAddPullRequest: vscode.EventEmitter = this._register(new vscode.EventEmitter()); public readonly onDidAddPullRequest: vscode.Event = this._onDidAddPullRequest.event; + private _onDidChangePullRequests: vscode.EventEmitter = this._register(new vscode.EventEmitter()); + public readonly onDidChangePullRequests: vscode.Event = this._onDidChangePullRequests.event; public get hub(): GitHub { if (!this._hub) { @@ -168,22 +209,33 @@ export class GitHubRepository extends Disposable { return this.remote.equals(repo.remote); } - get pullRequestModels(): Map { - return this._pullRequestModels; + getExistingPullRequestModel(prNumber: number): PullRequestModel | undefined { + return this._pullRequestModelsByNumber.get(prNumber)?.model; + } + + getExistingIssueModel(issueNumber: number): IssueModel | undefined { + return this._issueModelsByNumber.get(issueNumber)?.model; + } + + get pullRequestModels(): PullRequestModel[] { + return Array.from(this._pullRequestModelsByNumber.values().map(value => value.model)); + } + + get issueModels(): IssueModel[] { + return Array.from(this._issueModelsByNumber.values().map(value => value.model)); } public async ensureCommentsController(): Promise { try { + await this.ensure(); if (this.commentsController) { return; } - - await this.ensure(); this.commentsController = vscode.comments.createCommentController( - `github-browse-${this.remote.normalizedHost}-${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); + this.commentsHandler = new PRCommentControllerRegistry(this.commentsController, this.telemetry); this._register(this.commentsHandler); this._register(this.commentsController); } catch (e) { @@ -210,11 +262,11 @@ export class GitHubRepository extends Disposable { public remote: GitHubRemote, public readonly rootUri: vscode.Uri, private readonly _credentialStore: CredentialStore, - private readonly _telemetry: ITelemetry, + public readonly telemetry: ITelemetry, silent: boolean = false ) { super(); - this._queriesSchema = mergeQuerySchemaWithShared(sharedSchema.default as unknown as Schema, defaultSchema as unknown as Schema); + this._queriesSchema = mergeQuerySchemaWithShared(sharedSchema.default, defaultSchema); // kick off the comments controller early so that the Comments view is visible and doesn't pop up later in an way that's jarring if (!silent) { this.ensureCommentsController(); @@ -240,7 +292,7 @@ export class GitHubRepository extends Disposable { "action": { "classification": "SystemMetaData", "purpose": "PerformanceAndHealth" } } */ - this._telemetry.sendTelemetryErrorEvent('pr.codespacesTokenError', { + this.telemetry.sendTelemetryErrorEvent('pr.codespacesTokenError', { action: action.context }); @@ -248,17 +300,18 @@ export class GitHubRepository extends Disposable { } } - query = async (query: QueryOptions, ignoreSamlErrors: boolean = false, legacyFallback?: { query: DocumentNode }): Promise> => { + query = async (query: QueryOptions, ignoreSamlErrors: boolean = false, legacyFallback?: { query: DocumentNode, variables?: OperationVariables }): Promise> => { const gql = this.authMatchesServer && this.hub && this.hub.graphql; if (!gql) { const logValue = (query.query.definitions[0] as { name: { value: string } | undefined }).name?.value; Logger.debug(`Not available for query: ${logValue ?? 'unknown'}`, GRAPHQL_COMPONENT_ID); - return { - data: null, + const empty: ApolloQueryResult = { + data: null as T, loading: false, networkStatus: NetworkStatus.error, stale: false, - } as any; + } satisfies ApolloQueryResult; + return empty; } let rsp; @@ -270,6 +323,7 @@ export class GitHubRepository extends Disposable { Logger.error(`Error querying GraphQL API (${logInfo}): ${e.message}${gqlErrors ? `. ${gqlErrors.map(error => error.extensions?.code).join(',')}` : ''}`, this.id); if (legacyFallback) { query.query = legacyFallback.query; + query.variables = legacyFallback.variables; return this.query(query, ignoreSamlErrors); } @@ -277,7 +331,7 @@ export class GitHubRepository extends Disposable { // We're running against a GitHub server that doesn't support the query we're trying to run. // Switch to the limited schema and try again. this._areQueriesLimited = true; - this._queriesSchema = mergeQuerySchemaWithShared(sharedSchema.default as any, limitedSchema.default as any); + this._queriesSchema = mergeQuerySchemaWithShared(sharedSchema.default, limitedSchema.default); query.query = this.schema[(query.query.definitions[0] as { name: { value: string } }).name.value]; rsp = await gql.query(query); } else if (ignoreSamlErrors && isSamlError(e)) { @@ -299,15 +353,13 @@ export class GitHubRepository extends Disposable { const gql = this.authMatchesServer && this.hub && this.hub.graphql; if (!gql) { Logger.debug(`Not available for query: ${mutation.context as string}`, GRAPHQL_COMPONENT_ID); - return { - data: null, - loading: false, - networkStatus: NetworkStatus.error, - stale: false, - } as any; + const empty: FetchResult = { + data: null + }; + return empty; } - let rsp; + let rsp: FetchResult; try { rsp = await gql.mutate(mutation); } catch (e) { @@ -344,7 +396,8 @@ export class GitHubRepository extends Disposable { repo }); Logger.debug(`Fetch metadata for repo ${owner}/${repo} - done`, this.id); - return ({ ...result.data, currentUser: (octokit as any).currentUser } as unknown) as IMetadata; + const metadata = { ...result.data, currentUser: await this._hub?.currentUser }; + return metadata; } async getMetadata(): Promise { @@ -397,14 +450,14 @@ export class GitHubRepository extends Disposable { } if (oldHub !== this._hub) { - if (this._areQueriesLimited || this._credentialStore.areScopesOld(this.remote.authProviderId)) { + if (this._areQueriesLimited || this._credentialStore.areScopesOld(this.remote.authProviderId) || (this.remote.authProviderId === AuthProvider.githubEnterprise)) { this._areQueriesLimited = true; - this._queriesSchema = mergeQuerySchemaWithShared(sharedSchema.default as any, limitedSchema.default as any); + this._queriesSchema = mergeQuerySchemaWithShared(sharedSchema.default, limitedSchema.default); } else { if (this._credentialStore.areScopesExtra(this.remote.authProviderId)) { - this._queriesSchema = mergeQuerySchemaWithShared(sharedSchema.default as any, extraSchema.default as any); + this._queriesSchema = mergeQuerySchemaWithShared(sharedSchema.default, extraSchema.default); } else { - this._queriesSchema = mergeQuerySchemaWithShared(sharedSchema.default as any, defaultSchema as any); + this._queriesSchema = mergeQuerySchemaWithShared(sharedSchema.default, defaultSchema); } } } @@ -468,7 +521,7 @@ export class GitHubRepository extends Disposable { squash: data.allow_squash_merge ?? false, rebase: data.allow_rebase_merge ?? false, }, - viewerCanAutoMerge: ((data as any).allow_auto_merge && hasWritePermission) ?? false + viewerCanAutoMerge: (data.allow_auto_merge && hasWritePermission) ?? false }; } return this._repoAccessAndMergeMethods; @@ -542,6 +595,19 @@ export class GitHubRepository extends Disposable { return success; } + async getCommitParent(ref: string): Promise { + Logger.debug(`Fetch commit for ref ${ref} - enter`, this.id); + try { + const { octokit, remote } = await this.ensure(); + const commit = (await octokit.call(octokit.api.repos.getCommit, { owner: remote.owner, repo: remote.repositoryName, ref })).data; + return commit.parents[0].sha; + } catch (e) { + Logger.error(`Fetching commit for ref ${ref} failed: ${e}`, this.id); + } + Logger.debug(`Fetch commit for ref ${ref} - done`, this.id); + } + + async getAllPullRequests(page?: number): Promise { let remote: GitHubRemote | undefined; try { @@ -620,7 +686,7 @@ export class GitHubRepository extends Disposable { Logger.debug(`Fetch pull requests for branch - done`, this.id); if (data?.repository && data.repository.pullRequests.nodes.length > 0) { - const prs = data.repository.pullRequests.nodes.map(node => parseGraphQLPullRequest(node, this)).filter(pr => pr.head?.repo.owner === headOwner); + const prs = (await Promise.all(data.repository.pullRequests.nodes.map(node => parseGraphQLPullRequest(node, this)))).filter(pr => pr.head?.repo.owner === headOwner); if (prs.length === 0) { return undefined; } @@ -775,11 +841,11 @@ export class GitHubRepository extends Disposable { const issues: Issue[] = []; if (data && data.search.edges) { - data.search.edges.forEach(raw => { + await Promise.all(data.search.edges.map(async raw => { if (raw.node.id) { - issues.push(parseGraphQLIssue(raw.node, this)); + issues.push(await parseGraphQLIssue(raw.node, this)); } - }); + })); } return { items: issues, @@ -792,29 +858,37 @@ export class GitHubRepository extends Disposable { } } - async getMaxIssue(): Promise { + private async _getMaxItem(isIssue: boolean): Promise { try { - Logger.debug(`Fetch max issue - enter`, this.id); + Logger.debug(`Fetch max ${isIssue ? 'issue' : 'pull request'} - enter`, this.id); const { query, remote, schema } = await this.ensure(); const { data } = await query({ - query: schema.MaxIssue, + query: isIssue ? schema.MaxIssue : schema.MaxPullRequest, variables: { owner: remote.owner, name: remote.repositoryName, }, }); - Logger.debug(`Fetch max issue - done`, this.id); + Logger.debug(`Fetch max ${isIssue ? 'issue' : 'pull request'} - done`, this.id); if (data?.repository && data.repository.issues.edges.length === 1) { return data.repository.issues.edges[0].node.number; } return; } catch (e) { - Logger.error(`Unable to fetch issues with query: ${e}`, this.id); + Logger.error(`Unable to fetch ${isIssue ? 'issues' : 'pull requests'} with query: ${e}`, this.id); return; } } + async getMaxIssue(): Promise { + return this._getMaxItem(true); + } + + async getMaxPullRequest(): Promise { + return this._getMaxItem(false); + } + async getViewerPermission(): Promise { try { Logger.debug(`Fetch viewer permission - enter`, this.id); @@ -834,6 +908,30 @@ export class GitHubRepository extends Disposable { } } + public async getWorkflowRunsFromAction(fromDate: string): Promise { + const { octokit, remote } = await this.ensure(); + const createdDate = new Date(fromDate); + const created = `>=${createdDate.getFullYear()}-${String(createdDate.getMonth() + 1).padStart(2, '0')}-${String(createdDate.getDate()).padStart(2, '0')}`; + const allRuns = await restPaginate(octokit.api.actions.listWorkflowRunsForRepo, { + owner: remote.owner, + repo: remote.repositoryName, + event: 'dynamic', + created + }); + + return allRuns; + } + + public async getWorkflowJobs(workflowRunId: number): Promise { + const { octokit, remote } = await this.ensure(); + const jobs = await octokit.call(octokit.api.actions.listJobsForWorkflowRun, { + owner: remote.owner, + repo: remote.repositoryName, + run_id: workflowRunId + }); + return jobs.data.jobs; + } + async fork(): Promise { try { Logger.debug(`Fork repository`, this.id); @@ -886,8 +984,8 @@ export class GitHubRepository extends Disposable { } } - async getAuthenticatedUser(): Promise { - return (await this._credentialStore.getCurrentUser(this.remote.authProviderId)).login; + async getAuthenticatedUser(): Promise { + return await this._credentialStore.getCurrentUser(this.remote.authProviderId); } async getAuthenticatedUserEmails(): Promise { @@ -897,27 +995,51 @@ export class GitHubRepository extends Disposable { const { data } = await octokit.call(octokit.api.users.listEmailsForAuthenticatedUser, {}); Logger.debug(`Fetch authenticated user emails - done`, this.id); // sort the primary email to the first index - return data.sort((a, b) => +b.primary - +a.primary).map(email => email.email); + const hasPrivate = data.some(email => email.visibility === 'private'); + return data.filter(email => hasPrivate ? email.email.endsWith('@users.noreply.github.com') : email.verified) + .sort((a, b) => +b.primary - +a.primary) + .map(email => email.email); } catch (e) { Logger.error(`Unable to fetch authenticated user emails: ${e}`, this.id); return []; } } - createOrUpdatePullRequestModel(pullRequest: PullRequest): PullRequestModel { - let model = this._pullRequestModels.get(pullRequest.number); + createOrUpdatePullRequestModel(pullRequest: PullRequest, silent: boolean = false): PullRequestModel { + let model = this._pullRequestModelsByNumber.get(pullRequest.number)?.model; if (model) { model.update(pullRequest); } else { - model = new PullRequestModel(this._credentialStore, this._telemetry, this, this.remote, pullRequest); - model.onDidInvalidate(() => this.getPullRequest(pullRequest.number)); - this._pullRequestModels.set(pullRequest.number, model); - this._onDidAddPullRequest.fire(model); + model = new PullRequestModel(this._credentialStore, this.telemetry, this, this.remote, pullRequest); + const prModel = model; + const disposables: vscode.Disposable[] = []; + disposables.push(model.onDidChange(e => this._onPullRequestModelChanged(prModel, e))); + this._pullRequestModelsByNumber.set(pullRequest.number, { model, disposables }); + if (!silent) { + this._onDidAddPullRequest.fire(model); + } } return model; } + private createOrUpdateIssueModel(issue: Issue): IssueModel { + let model = this._issueModelsByNumber.get(issue.number)?.model; + if (model) { + model.update(issue); + } else { + model = new IssueModel(this.telemetry, this, this.remote, issue); + // No issue-specific event emitters yet; store empty disposables list for symmetry/cleanup + const disposables: vscode.Disposable[] = []; + this._issueModelsByNumber.set(issue.number, { model, disposables }); + } + return model; + } + + private _onPullRequestModelChanged(model: PullRequestModel, change: IssueChangeEvent): void { + this._onDidChangePullRequests.fire([{ model, event: change }]); + } + async createPullRequest(params: OctokitCommon.PullsCreateParams): Promise { try { Logger.debug(`Create pull request - enter`, this.id); @@ -941,7 +1063,7 @@ export class GitHubRepository extends Disposable { if (!data) { throw new Error('Failed to create pull request.'); } - return this.createOrUpdatePullRequestModel(parseGraphQLPullRequest(data.createPullRequest.pullRequest, this)); + return this.createOrUpdatePullRequestModel(await parseGraphQLPullRequest(data.createPullRequest.pullRequest, this)); } catch (e) { Logger.error(`Unable to create PR: ${e}`, this.id); throw e; @@ -968,14 +1090,19 @@ export class GitHubRepository extends Disposable { if (!data) { throw new Error('Failed to create revert pull request.'); } - return this.createOrUpdatePullRequestModel(parseGraphQLPullRequest(data.revertPullRequest.revertPullRequest, this)); + return this.createOrUpdatePullRequestModel(await parseGraphQLPullRequest(data.revertPullRequest.revertPullRequest, this)); } catch (e) { Logger.error(`Unable to create revert PR: ${e}`, this.id); throw e; } } - async getPullRequest(id: number): Promise { + async getPullRequest(id: number, useCache: boolean = false): Promise { + if (useCache && this._pullRequestModelsByNumber.has(id)) { + Logger.debug(`Using cached pull request model for ${id}`, this.id); + return this._pullRequestModelsByNumber.get(id)!.model; + } + try { const { query, remote, schema } = await this.ensure(); Logger.debug(`Fetch pull request ${remote.owner}/${remote.repositoryName} ${id} - enter`, this.id); @@ -994,14 +1121,23 @@ export class GitHubRepository extends Disposable { } Logger.debug(`Fetch pull request ${id} - done`, this.id); - return this.createOrUpdatePullRequestModel(parseGraphQLPullRequest(data.repository.pullRequest, this)); + const pr = this.createOrUpdatePullRequestModel(await parseGraphQLPullRequest(data.repository.pullRequest, this)); + await pr.getLastUpdateTime(new Date(pr.item.updatedAt)); + return pr; } catch (e) { Logger.error(`Unable to fetch PR: ${e}`, this.id); return; } } - async getIssue(id: number, withComments: boolean = false): Promise { + async getIssue(id: number, withComments: boolean = false, useCache: boolean = false): Promise { + if (useCache) { + const cached = this._issueModelsByNumber.get(id)?.model; + if (cached) { + Logger.debug(`Using cached issue model for ${id}`, this.id); + return cached; + } + } try { Logger.debug(`Fetch issue ${id} - enter`, this.id); const { query, remote, schema } = await this.ensure(); @@ -1021,7 +1157,9 @@ export class GitHubRepository extends Disposable { } Logger.debug(`Fetch issue ${id} - done`, this.id); - return new IssueModel(this, remote, parseGraphQLIssue(data.repository.issue, this)); + const issue = this.createOrUpdateIssueModel(await parseGraphQLIssue(data.repository.issue, this)); + await issue.getLastUpdateTime(new Date(issue.item.updatedAt)); + return issue; } catch (e) { Logger.error(`Unable to fetch issue: ${e}`, this.id); return; @@ -1046,7 +1184,7 @@ export class GitHubRepository extends Disposable { path: filePath, ref, }, - )) as any; + )) as { data: { content: string; encoding: string; sha: string } }; if (Array.isArray(fileContent.data)) { throw new Error(`Unexpected array response when getting file ${filePath}`); @@ -1074,12 +1212,12 @@ export class GitHubRepository extends Disposable { Logger.debug(`Fetch blob file ${filePath} - done`, this.id); } - const buff = buffer.Buffer.from(contents, (fileContent.data as any).encoding); + const buff = buffer.Buffer.from(contents, fileContent.data.encoding as BufferEncoding); Logger.debug(`Fetch file ${filePath}, file length ${contents.length} - done`, this.id); return buff; } - async hasBranch(branchName: string): Promise { + async hasBranch(branchName: string): Promise { Logger.appendLine(`Fetch branch ${branchName} - enter`, this.id); const { query, remote, schema } = await this.ensure(); @@ -1092,7 +1230,7 @@ export class GitHubRepository extends Disposable { } }); Logger.appendLine(`Fetch branch ${branchName} - done: ${data.repository?.ref !== null}`, this.id); - return data.repository?.ref !== null; + return data.repository?.ref?.target.oid; } async listBranches(owner: string, repositoryName: string): Promise { @@ -1104,6 +1242,7 @@ export class GitHubRepository extends Disposable { const branches: string[] = []; const defaultBranch = (await this.getMetadataForRepo(owner, repositoryName)).default_branch; const startingTime = new Date().getTime(); + const timeout = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(BRANCH_LIST_TIMEOUT, 5000); do { try { @@ -1118,8 +1257,8 @@ export class GitHubRepository extends Disposable { }); branches.push(...data.repository.refs.nodes.map(node => node.name)); - if (new Date().getTime() - startingTime > 5000) { - Logger.warn('List branches timeout hit.', this.id); + if (new Date().getTime() - startingTime > timeout) { + Logger.warn(`List branches timeout hit after ${timeout}ms.`, this.id); break; } hasNextPage = data.repository.refs.pageInfo.hasNextPage; @@ -1198,6 +1337,27 @@ export class GitHubRepository extends Disposable { return ret; } + async resolveUser(login: string): Promise { + Logger.debug(`Fetch user ${login}`, this.id); + const { query, schema } = await this.ensure(); + + try { + const { data } = await query({ + query: schema.GetUser, + variables: { + login, + }, + }); + return parseGraphQLUser(data, this); + } catch (e) { + // Ignore cases where the user doesn't exist + if (!(e.message as (string | undefined))?.startsWith('GraphQL error: Could not resolve to a User with the login of')) { + Logger.warn(e.message); + } + } + return undefined; + } + async getAssignableUsers(): Promise { Logger.debug(`Fetch assignable users - enter`, this.id); const { query, remote, schema } = await this.ensure(); @@ -1219,6 +1379,14 @@ export class GitHubRepository extends Disposable { first: 100, after: after, }, + }, false, { + query: schema.GetAssignableUsers, + variables: { + owner: remote.owner, + name: remote.repositoryName, + first: 100, + after: after, + } }); } else { @@ -1241,9 +1409,9 @@ export class GitHubRepository extends Disposable { const users = (result.data as AssignableUsersResponse).repository?.assignableUsers ?? (result.data as SuggestedActorsResponse).repository?.suggestedActors; ret.push( - ...users?.nodes.map(node => { + ...(users?.nodes.map(node => { return parseAccount(node, this); - }), + }) || []), ); hasNextPage = users?.pageInfo.hasNextPage; @@ -1266,6 +1434,22 @@ export class GitHubRepository extends Disposable { return ret; } + async cancelWorkflow(workflowRunId: number): Promise { + Logger.debug(`Cancel workflow run - enter`, this.id); + const { octokit, remote } = await this.ensure(); + try { + const result = await octokit.call(octokit.api.actions.cancelWorkflowRun, { + owner: remote.owner, + repo: remote.repositoryName, + run_id: workflowRunId, + }); + return result.status === 202; + } catch (e) { + Logger.error(`Unable to cancel workflow run: ${e}`, this.id); + return false; + } + } + async getOrgTeamsCount(): Promise { Logger.debug(`Fetch Teams Count - enter`, this.id); if (!this._credentialStore.isAuthenticatedWithAdditionalScopes(this.remote.authProviderId)) { @@ -1420,8 +1604,8 @@ export class GitHubRepository extends Disposable { } } - isCurrentUser(login: string): Promise { - return this._credentialStore.isCurrentUser(login); + isCurrentUser(authProviderId: AuthProvider, login: string): Promise { + return this._credentialStore.isCurrentUser(authProviderId, login); } /** diff --git a/src/github/graphql.ts b/src/github/graphql.ts index 632083453b..627c7da90c 100644 --- a/src/github/graphql.ts +++ b/src/github/graphql.ts @@ -3,8 +3,8 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { DiffSide, SubjectType, ViewedState } from '../common/comment'; import { ForkDetails } from './githubRepository'; +import { DiffSide, SubjectType, ViewedState } from '../common/comment'; interface PageInfo { hasNextPage: boolean; @@ -40,13 +40,34 @@ export interface CrossReferencedEvent { actor: Actor; createdAt: string; source: { + __typename: string; number: number; url: string; title: string; + repository: { + name: string; + owner: { + login: string; + }; + } }; willCloseTarget: boolean; } +export interface ClosedEvent { + __typename: string; + id: string; + actor: Actor; + createdAt: string; +} + +export interface ReopenedEvent { + __typename: string; + id: string; + actor: Actor; + createdAt: string; +} + export interface AbbreviatedIssueComment { author: Account; body: string; @@ -54,6 +75,7 @@ export interface AbbreviatedIssueComment { reactions: { totalCount: number; }; + reactionGroups: ReactionGroup[] createdAt: string; } @@ -81,6 +103,10 @@ export interface ReactionGroup { }; } +export interface Node { + id: string; +} + export interface Actor { __typename: string; id: string; @@ -94,12 +120,14 @@ export interface Account extends Actor { email: string; } -export function isAccount(x: Actor | Team | undefined | null): x is Account { - return !!x && 'name' in x && 'email' in x; +export function isAccount(x: Actor | Team | Node | undefined | null): x is Account { + const asAccount = x as Partial; + return !!asAccount && (asAccount?.name !== undefined) && (asAccount?.email !== undefined); } -export function isTeam(x: Actor | Team | undefined | null): x is Team { - return !!x && 'slug' in x; +export function isTeam(x: Actor | Team | Node | undefined | null): x is Team { + const asTeam = x as Partial; + return !!asTeam && (asTeam?.slug !== undefined); } export interface Team { @@ -159,7 +187,10 @@ export interface Commit { }; oid: string; message: string; - authoredDate: Date; + committedDate: Date; + statusCheckRollup?: { + state: 'EXPECTED' | 'ERROR' | 'FAILURE' | 'PENDING' | 'SUCCESS'; + }; }; url: string; @@ -173,6 +204,14 @@ export interface AssignedEvent { createdAt: string; } +export interface UnassignedEvent { + __typename: string; + id: number; + actor: Actor; + user: Account; + createdAt: string; +} + export interface MergeQueueEntry { position: number; state: MergeQueueState; @@ -194,6 +233,7 @@ export interface Review { submittedAt: string; updatedAt: string; createdAt: string; + reactionGroups: ReactionGroup[]; } export interface ReviewThread { @@ -225,13 +265,55 @@ export interface TimelineEventsResponse { repository: { pullRequest: { timelineItems: { - nodes: (MergedEvent | Review | IssueComment | Commit | AssignedEvent | HeadRefDeletedEvent)[]; + nodes: (MergedEvent | Review | IssueComment | Commit | AssignedEvent | HeadRefDeletedEvent | null)[]; }; }; } | null; rateLimit: RateLimit; } +export interface LatestCommit { + commit: { + committedDate: string; + } +} + +export interface LatestReviewThread { + comments: { + nodes: { + createdAt: string; + }[]; + } +} + +export interface LatestUpdatesResponse { + repository: { + pullRequest: { + reactions: { + nodes: { + createdAt: string; + }[]; + } + updatedAt: string; + comments: { + nodes: { + updatedAt: string; + reactions: { + nodes: { + createdAt: string; + }[]; + } + }[]; + } + timelineItems: { + nodes: ({ + createdAt: string; + } | LatestCommit | LatestReviewThread)[]; + } + } + } +} + export interface LatestReviewCommitResponse { repository: { pullRequest: { @@ -258,7 +340,7 @@ export interface GetReviewRequestsResponse { pullRequest: { reviewRequests: { nodes: { - requestedReviewer: Actor | Account | Team | null; + requestedReviewer: Actor | Account | Team | Node | null; }[]; }; }; @@ -481,6 +563,13 @@ export interface UpdateIssueResponse { bodyHTML: string; title: string; titleHTML: string; + milestone?: { + title: string; + dueOn?: string; + id: string; + createdAt: string; + number: number; + }; }; }; } @@ -499,7 +588,7 @@ export interface GetBranchResponse { target: { oid: string; } - } + } | null; } | null; } @@ -552,6 +641,7 @@ export interface Issue { number: number; url: string; state: 'OPEN' | 'CLOSED' | 'MERGED'; // TODO: don't allow merged in an issue + stateReason?: 'REOPENED' | 'NOT_PLANNED' | 'COMPLETED' | 'DUPLICATE'; body: string; bodyHTML: string; title: string; @@ -599,6 +689,7 @@ export interface Issue { reactions: { totalCount: number; } + reactionGroups: ReactionGroup[]; } @@ -632,6 +723,8 @@ export interface PullRequest extends Issue { viewerCanDisableAutoMerge: boolean; isDraft?: boolean; suggestedReviewers: SuggestedReviewerResponse[]; + additions?: number; + deletions?: number; closingIssuesReferences?: { nodes: { id: number, diff --git a/src/github/interface.ts b/src/github/interface.ts index d388af9be7..3e3fa488cf 100644 --- a/src/github/interface.ts +++ b/src/github/interface.ts @@ -11,7 +11,7 @@ export enum PRType { LocalPullRequest, } -export enum ReviewEvent { +export enum ReviewEventEnum { Approve = 'APPROVE', RequestChanges = 'REQUEST_CHANGES', Comment = 'COMMENT', @@ -104,15 +104,16 @@ export interface MergeQueueEntry { export function reviewerId(reviewer: ITeam | IAccount): string { // We can literally get different login values for copilot depending on where it's coming from (already assignee vs suggested assingee) - return isTeam(reviewer) ? reviewer.id : (reviewer.specialDisplayName ?? reviewer.login); + return isITeam(reviewer) ? reviewer.id : (reviewer.specialDisplayName ?? reviewer.login); } export function reviewerLabel(reviewer: ITeam | IAccount | IActor | any): string { - return isTeam(reviewer) ? (reviewer.name ?? reviewer.slug ?? reviewer.id) : (reviewer.specialDisplayName ?? reviewer.login); + return isITeam(reviewer) ? (reviewer.name ?? reviewer.slug ?? reviewer.id) : (reviewer.specialDisplayName ?? reviewer.login); } -export function isTeam(reviewer: ITeam | IAccount | IActor | any): reviewer is ITeam { - return 'org' in reviewer; +export function isITeam(reviewer: ITeam | IAccount | IActor | any): reviewer is ITeam { + const asITeam = reviewer as Partial; + return !!asITeam.org; } export interface ISuggestedReviewer extends IAccount { @@ -120,10 +121,11 @@ export interface ISuggestedReviewer extends IAccount { isCommenter: boolean; } -export function isSuggestedReviewer( +export function isISuggestedReviewer( reviewer: IAccount | ISuggestedReviewer | ITeam ): reviewer is ISuggestedReviewer { - return 'isAuthor' in reviewer && 'isCommenter' in reviewer; + const asISuggestedReviewer = reviewer as Partial; + return !!asISuggestedReviewer.isAuthor && !!asISuggestedReviewer.isCommenter; } export interface IProject { @@ -179,12 +181,23 @@ export interface IIssueComment { createdAt: string; } +export interface Reaction { + label: string; + count: number; + icon?: string; + viewerHasReacted: boolean; + reactors: readonly string[]; +} + +export type StateReason = 'REOPENED' | 'NOT_PLANNED' | 'COMPLETED' | 'DUPLICATE'; + export interface Issue { id: number; graphNodeId: string; url: string; number: number; state: string; + stateReason?: StateReason; body: string; bodyHTML?: string; title: string; @@ -202,6 +215,7 @@ export interface Issue { comments?: IIssueComment[]; commentCount: number; reactionCount: number; + reactions: Reaction[]; } export interface IssueReference { @@ -232,6 +246,8 @@ export interface PullRequest extends Issue { suggestedReviewers?: ISuggestedReviewer[]; closingIssues?: IssueReference[] hasComments?: boolean; + additions?: number; + deletions?: number; } export enum NotificationSubjectType { @@ -252,7 +268,7 @@ export interface Notification { }; reason: string; unread: boolean; - updatedAd: Date; + updatedAt: Date; lastReadAt: Date | undefined; } diff --git a/src/github/issueModel.ts b/src/github/issueModel.ts index 8a6a58d172..aee459393a 100644 --- a/src/github/issueModel.ts +++ b/src/github/issueModel.ts @@ -4,23 +4,46 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { IComment } from '../common/comment'; -import Logger from '../common/logger'; -import { Remote } from '../common/remote'; -import { TimelineEvent } from '../common/timelineEvent'; -import { formatError } from '../common/utils'; -import { GitHubRepository } from './githubRepository'; +import { OctokitCommon } from './common'; +import { CopilotWorkingStatus, GitHubRepository } from './githubRepository'; import { AddIssueCommentResponse, AddPullRequestToProjectResponse, EditIssueCommentResponse, + LatestCommit, + LatestReviewThread, + LatestUpdatesResponse, TimelineEventsResponse, UpdateIssueResponse, } from './graphql'; -import { GithubItemStateEnum, IAccount, IIssueEditData, IMilestone, IProject, IProjectItem, Issue } from './interface'; -import { parseGraphQlIssueComment, parseGraphQLTimelineEvents, parsePullRequestState } from './utils'; +import { GithubItemStateEnum, IAccount, IIssueEditData, IMilestone, IProject, IProjectItem, Issue, StateReason } from './interface'; +import { convertRESTIssueToRawPullRequest, eventTime, parseCombinedTimelineEvents, parseGraphQlIssueComment, parseMilestone, parsePullRequestState, parseSelectRestTimelineEvents, restPaginate } from './utils'; +import { COPILOT_ACCOUNTS, IComment } from '../common/comment'; +import { Disposable } from '../common/lifecycle'; +import Logger from '../common/logger'; +import { Remote } from '../common/remote'; +import { ITelemetry } from '../common/telemetry'; +import { ClosedEvent, CrossReferencedEvent, EventType, TimelineEvent } from '../common/timelineEvent'; +import { compareIgnoreCase, formatError } from '../common/utils'; + +export interface IssueChangeEvent { + title?: true; + body?: true; + milestone?: true; + // updatedAt?: true; + state?: true; + labels?: true; + assignees?: true; + projects?: true; + comments?: true; + + timeline?: true; + + draft?: true; + reviewers?: true; +} -export class IssueModel { +export class IssueModel extends Disposable { static ID = 'IssueModel'; public id: number; public graphNodeId: string; @@ -29,20 +52,30 @@ export class IssueModel { public titleHTML: string; public html_url: string; public state: GithubItemStateEnum = GithubItemStateEnum.Open; + public stateReason?: StateReason; public author: IAccount; public assignees?: IAccount[]; public createdAt: string; public updatedAt: string; public milestone?: IMilestone; public readonly githubRepository: GitHubRepository; + protected readonly _telemetry: ITelemetry; public readonly remote: Remote; public item: TItem; + public body: string; public bodyHTML?: string; - private _onDidInvalidate = new vscode.EventEmitter(); - public onDidInvalidate = this._onDidInvalidate.event; + private _lastCheckedForUpdatesAt?: Date; + + private _timelineEvents: readonly TimelineEvent[] | undefined; + private _copilotTimelineEvents: TimelineEvent[] | undefined; + + protected _onDidChange = this._register(new vscode.EventEmitter()); + public onDidChange = this._onDidChange.event; - constructor(githubRepository: GitHubRepository, remote: Remote, item: TItem, skipUpdate: boolean = false) { + constructor(telemetry: ITelemetry, githubRepository: GitHubRepository, remote: Remote, item: TItem, skipUpdate: boolean = false) { + super(); + this._telemetry = telemetry; this.githubRepository = githubRepository; this.remote = remote; this.item = item; @@ -52,9 +85,19 @@ export class IssueModel { } } - public invalidate() { - // Something about the PR data is stale - this._onDidInvalidate.fire(); + get timelineEvents(): readonly TimelineEvent[] | undefined { + return this._timelineEvents; + } + + protected set timelineEvents(timelineEvents: readonly TimelineEvent[]) { + if (!this._timelineEvents || this._timelineEvents.length !== timelineEvents.length) { + this._timelineEvents = timelineEvents; + this._onDidChange.fire({ timeline: true }); + } + } + + public get lastCheckedForUpdatesAt(): Date | undefined { + return this._lastCheckedForUpdatesAt; } public get isOpen(): boolean { @@ -96,41 +139,59 @@ export class IssueModel { return undefined; } - public get body(): string { - if (this.item) { - return this.item.body; - } - return ''; - } - - protected updateState(state: string) { - this.state = parsePullRequestState(state); - } + protected doUpdate(issue: TItem): IssueChangeEvent { + const changes: IssueChangeEvent = {}; - update(issue: TItem): void { this.id = issue.id; this.graphNodeId = issue.graphNodeId; this.number = issue.number; - this.title = issue.title; - if (issue.titleHTML) { - this.titleHTML = issue.titleHTML; - } - if (!this.bodyHTML || (issue.body !== this.body)) { - this.bodyHTML = issue.bodyHTML; - } this.html_url = issue.url; this.author = issue.user; - this.milestone = issue.milestone; this.createdAt = issue.createdAt; - this.updatedAt = issue.updatedAt; - - this.updateState(issue.state); - if (issue.assignees) { + if (this.title !== issue.title) { + changes.title = true; + this.title = issue.title; + } + if (issue.titleHTML && this.titleHTML !== issue.titleHTML) { + this.titleHTML = issue.titleHTML; + } + if ((!this.bodyHTML || (issue.body !== this.body)) && this.bodyHTML !== issue.bodyHTML) { + this.bodyHTML = issue.bodyHTML; + } + if (this.body !== issue.body) { + changes.body = true; + this.body = issue.body; + } + if (this.milestone?.id !== issue.milestone?.id) { + changes.milestone = true; + this.milestone = issue.milestone; + } + if (this.updatedAt !== issue.updatedAt) { + this.updatedAt = issue.updatedAt; + } + const newState = parsePullRequestState(issue.state); + if (this.state !== newState) { + changes.state = true; + this.state = newState; + } + if ((this.stateReason !== issue.stateReason) && issue.stateReason) { + changes.state = true; + this.stateReason = issue.stateReason; + } + if (issue.assignees && (issue.assignees.length !== (this.assignees?.length ?? 0) || issue.assignees.some(assignee => this.assignees?.every(a => a.id !== assignee.id)))) { + changes.assignees = true; this.assignees = issue.assignees; } + return changes; + } + update(issue: TItem): void { + const changes = this.doUpdate(issue); this.item = issue; + if (Object.keys(changes).length > 0) { + this._onDidChange.fire(changes); + } } equals(other: IssueModel | undefined): boolean { @@ -174,11 +235,18 @@ export class IssueModel { }, }); if (data?.updateIssue.issue) { - this.item.body = data.updateIssue.issue.body; - this.bodyHTML = data.updateIssue.issue.bodyHTML; - this.title = data.updateIssue.issue.title; - this.titleHTML = data.updateIssue.issue.titleHTML; - this.invalidate(); + const changes: IssueChangeEvent = {}; + if (this.body !== data.updateIssue.issue.body) { + changes.body = true; + this.item.body = data.updateIssue.issue.body; + this.bodyHTML = data.updateIssue.issue.bodyHTML; + } + if (this.title !== data.updateIssue.issue.title) { + changes.title = true; + this.title = data.updateIssue.issue.title; + this.titleHTML = data.updateIssue.issue.titleHTML; + } + this._onDidChange.fire(changes); } return data!.updateIssue.issue; } catch (e) { @@ -188,7 +256,7 @@ export class IssueModel { canEdit(): Promise { const username = this.author && this.author.login; - return this.githubRepository.isCurrentUser(username); + return this.githubRepository.isCurrentUser(this.remote.authProviderId, username); } async createIssueComment(text: string): Promise { @@ -203,6 +271,7 @@ export class IssueModel { }, }); + this._onDidChange.fire({ timeline: true }); return parseGraphQlIssueComment(data!.addComment.commentEdge.node, this.githubRepository); } @@ -220,6 +289,7 @@ export class IssueModel { }, }); + this._onDidChange.fire({ timeline: true }); return parseGraphQlIssueComment(data!.updateIssueComment.issueComment, this.githubRepository); } catch (e) { throw new Error(formatError(e)); @@ -235,6 +305,7 @@ export class IssueModel { repo: remote.repositoryName, comment_id: Number(commentId), }); + this._onDidChange.fire({ timeline: true }); } catch (e) { throw new Error(formatError(e)); } @@ -243,12 +314,18 @@ export class IssueModel { async setLabels(labels: string[]): Promise { const { octokit, remote } = await this.githubRepository.ensure(); try { - await octokit.call(octokit.api.issues.setLabels, { + const result = await octokit.call(octokit.api.issues.setLabels, { owner: remote.owner, repo: remote.repositoryName, issue_number: this.number, labels, }); + this.item.labels = result.data.map(label => ({ + name: label.name, + color: label.color, + description: label.description ?? undefined + })); + this._onDidChange.fire({ labels: true }); } catch (e) { // We don't get a nice error message from the API when setting labels fails. // Since adding labels isn't a critical part of the PR creation path it's safe to catch all errors that come from setting labels. @@ -259,15 +336,30 @@ export class IssueModel { async removeLabel(label: string): Promise { const { octokit, remote } = await this.githubRepository.ensure(); - await octokit.call(octokit.api.issues.removeLabel, { + const result = await octokit.call(octokit.api.issues.removeLabel, { owner: remote.owner, repo: remote.repositoryName, issue_number: this.number, name: label, }); + this.item.labels = result.data.map(label => ({ + name: label.name, + color: label.color, + description: label.description ?? undefined + })); + this._onDidChange.fire({ labels: true }); } public async removeProjects(projectItems: IProjectItem[]): Promise { + const result = await this.doRemoveProjects(projectItems); + if (!result) { + // If we failed to remove the projects, we don't want to update the model. + return; + } + this._onDidChange.fire({ projects: true }); + } + + private async doRemoveProjects(projectItems: IProjectItem[]): Promise { const { mutate, schema } = await this.githubRepository.ensure(); try { @@ -282,8 +374,10 @@ export class IssueModel { }, }))); this.item.projectItems = this.item.projectItems?.filter(project => !projectItems.find(p => p.project.id === project.project.id)); + return true; } catch (err) { Logger.error(err, IssueModel.ID); + return false; } } @@ -315,13 +409,61 @@ export class IssueModel { const projectsToRemove: IProjectItem[] = this.item.projectItems?.filter(project => !projects.find(p => p.id === project.project.id)) ?? []; await this.removeProjects(projectsToRemove); await this.addProjects(projectsToAdd); + this._onDidChange.fire({ projects: true }); return this.item.projectItems; } - async getIssueTimelineEvents(): Promise { + protected getUpdatesQuery(schema: any): any { + return schema.LatestIssueUpdates; + } + + async getLastUpdateTime(time: Date): Promise { Logger.debug(`Fetch timeline events of issue #${this.number} - enter`, IssueModel.ID); + // Record when we initiated this check regardless of outcome so callers can know staleness. + this._lastCheckedForUpdatesAt = new Date(); const githubRepository = this.githubRepository; const { query, remote, schema } = await githubRepository.ensure(); + try { + const { data } = await query({ + query: this.getUpdatesQuery(schema), + variables: { + owner: remote.owner, + name: remote.repositoryName, + number: this.number, + since: new Date(time), + } + }); + + const times = [ + time, + new Date(data.repository.pullRequest.updatedAt), + ...(data.repository.pullRequest.reactions.nodes.map(node => new Date(node.createdAt))), + ...(data.repository.pullRequest.comments.nodes.map(node => new Date(node.updatedAt))), + ...(data.repository.pullRequest.comments.nodes.flatMap(node => node.reactions.nodes.map(reaction => new Date(reaction.createdAt)))), + ...(data.repository.pullRequest.timelineItems.nodes.map(node => { + const latestCommit = node as Partial; + if (latestCommit.commit?.committedDate) { + return new Date(latestCommit.commit.committedDate); + } + const latestReviewThread = node as Partial; + if ((latestReviewThread.comments?.nodes.length ?? 0) > 0) { + return new Date(latestReviewThread.comments!.nodes[0].createdAt); + } + return new Date((node as { createdAt: string }).createdAt); + })) + ]; + + // Sort times and return the most recent one + return new Date(Math.max(...times.map(t => t.getTime()))); + } catch (e) { + Logger.error(`Error fetching timeline events of issue #${this.number} - ${formatError(e)}`, IssueModel.ID); + return time; // Return the original time in case of an error + } + } + + async getIssueTimelineEvents(): Promise { + Logger.debug(`Fetch timeline events of issue #${this.number} - enter`, GitHubRepository.ID); + const { query, remote, schema } = await this.githubRepository.ensure(); try { const { data } = await query({ @@ -334,12 +476,27 @@ export class IssueModel { }); if (data.repository === null) { - Logger.error('Unexpected null repository when getting issue timeline events', IssueModel.ID); + Logger.error('Unexpected null repository when getting issue timeline events', GitHubRepository.ID); return []; } + const ret = data.repository.pullRequest.timelineItems.nodes; - const events = parseGraphQLTimelineEvents(ret, githubRepository); + const events = await parseCombinedTimelineEvents(ret, await this.getCopilotTimelineEvents(true), this.githubRepository); + + const crossRefs = events.filter((event): event is CrossReferencedEvent => { + if ((event.event === EventType.CrossReferenced) && !event.source.isIssue) { + return !this.githubRepository.getExistingPullRequestModel(event.source.number) && (compareIgnoreCase(event.source.owner, this.remote.owner) === 0 && compareIgnoreCase(event.source.repo, this.remote.repositoryName) === 0); + } + return false; + + }); + + for (const unseenPrs of crossRefs) { + // Kick off getting the new PRs so that the system knows about them (and refreshes the tree when they're found) + this.githubRepository.getPullRequest(unseenPrs.source.number); + } + this.timelineEvents = events; return events; } catch (e) { console.log(e); @@ -347,12 +504,77 @@ export class IssueModel { } } + /** + * TODO: @alexr00 we should delete this https://github.com/microsoft/vscode-pull-request-github/issues/6965 + */ + async getCopilotTimelineEvents(skipMerge: boolean = false, useCache: boolean = false): Promise { + if (!COPILOT_ACCOUNTS[this.author.login]) { + return []; + } + + Logger.debug(`Fetch Copilot timeline events of issue #${this.number} - enter`, GitHubRepository.ID); + + if (useCache && this._copilotTimelineEvents) { + Logger.debug(`Fetch Copilot timeline events of issue #${this.number} (used cache) - exit`, GitHubRepository.ID); + + return this._copilotTimelineEvents; + } + + const { octokit, remote } = await this.githubRepository.ensure(); + try { + const timeline = await restPaginate(octokit.api.issues.listEventsForTimeline, { + issue_number: this.number, + owner: remote.owner, + repo: remote.repositoryName, + per_page: 100 + }); + + const timelineEvents = parseSelectRestTimelineEvents(this, timeline); + this._copilotTimelineEvents = timelineEvents; + if (timelineEvents.length === 0) { + return []; + } + if (!skipMerge) { + const oldLastEvent = this.timelineEvents ? (this.timelineEvents.length > 0 ? this.timelineEvents[this.timelineEvents.length - 1] : undefined) : undefined; + let allEvents: TimelineEvent[]; + if (!oldLastEvent) { + allEvents = timelineEvents; + } else { + const oldEventTime = (eventTime(oldLastEvent) ?? 0); + const newEvents = timelineEvents.filter(event => (eventTime(event) ?? 0) > oldEventTime); + allEvents = [...(this.timelineEvents ?? []), ...newEvents]; + } + this.timelineEvents = allEvents; + } + Logger.debug(`Fetch Copilot timeline events of issue #${this.number} - exit`, GitHubRepository.ID); + return timelineEvents; + } catch (e) { + Logger.error(`Error fetching Copilot timeline events of issue #${this.number} - ${formatError(e)}`, GitHubRepository.ID); + return []; + } + } + + async copilotWorkingStatus(): Promise { + const copilotEvents = await this.getCopilotTimelineEvents(); + if (copilotEvents.length > 0) { + const lastEvent = copilotEvents[copilotEvents.length - 1]; + if (lastEvent.event === EventType.CopilotFinished) { + return CopilotWorkingStatus.Done; + } else if (lastEvent.event === EventType.CopilotStarted) { + return CopilotWorkingStatus.InProgress; + } else if (lastEvent.event === EventType.CopilotFinishedError) { + return CopilotWorkingStatus.Error; + } + } + return CopilotWorkingStatus.NotCopilotIssue; + } + async updateMilestone(id: string): Promise { const { mutate, schema } = await this.githubRepository.ensure(); const finalId = id === 'null' ? null : id; try { - await mutate({ + const result = await mutate({ mutation: this.updateIssueSchema(schema), variables: { input: { @@ -361,6 +583,8 @@ export class IssueModel { }, }, }); + this.milestone = parseMilestone(result.data!.updateIssue.issue.milestone); + this._onDidChange.fire({ milestone: true }); } catch (err) { Logger.error(err, IssueModel.ID); } @@ -372,6 +596,15 @@ export class IssueModel { try { if (schema.ReplaceActorsForAssignable) { + const assignToCopilot = allAssignees.find(assignee => COPILOT_ACCOUNTS[assignee.login]); + const alreadyHasCopilot = this.assignees?.find(assignee => COPILOT_ACCOUNTS[assignee.login]) !== undefined; + if (assignToCopilot && !alreadyHasCopilot) { + /* __GDPR__ + "pr.assignCopilot" : {} + */ + this._telemetry.sendTelemetryEvent('pr.assignCopilot'); + } + const assigneeIds = allAssignees.map(assignee => assignee.id); await mutate({ mutation: schema.ReplaceActorsForAssignable, @@ -389,13 +622,14 @@ export class IssueModel { await this.deleteAssignees(removeAssignees); } this.assignees = allAssignees; + this._onDidChange.fire({ assignees: true }); } catch (e) { Logger.error(e, IssueModel.ID); } Logger.debug(`Replace assignees of issue #${this.number} - done`, IssueModel.ID); } - async addAssignees(assigneesToAdd: string[]): Promise { + private async addAssignees(assigneesToAdd: string[]): Promise { const { octokit, remote } = await this.githubRepository.ensure(); await octokit.call(octokit.api.issues.addAssignees, { owner: remote.owner, @@ -414,4 +648,30 @@ export class IssueModel { assignees, }); } + + async close(): Promise<{ item: Issue, closedEvent: ClosedEvent }> { + const { octokit, remote } = await this.githubRepository.ensure(); + const ret = await octokit.call(octokit.api.issues.update, { + owner: remote.owner, + repo: remote.repositoryName, + issue_number: this.number, + state: 'closed' + }); + + this.state = GithubItemStateEnum.Closed; + this._onDidChange.fire({ state: true }); + return { + item: convertRESTIssueToRawPullRequest(ret.data, this.githubRepository), + closedEvent: { + createdAt: ret.data.closed_at ?? '', + event: EventType.Closed, + id: `${ret.data.id}`, + actor: { + login: ret.data.closed_by!.login, + avatarUrl: ret.data.closed_by!.avatar_url, + url: ret.data.closed_by!.url + } + } + }; + } } diff --git a/src/github/issueOverview.ts b/src/github/issueOverview.ts index 8dc84326b9..506ee37221 100644 --- a/src/github/issueOverview.ts +++ b/src/github/issueOverview.ts @@ -5,19 +5,23 @@ 'use strict'; import * as vscode from 'vscode'; +import { CloseResult } from '../../common/views'; import { openPullRequestOnGitHub } from '../commands'; -import { COPILOT_ACCOUNTS, IComment } from '../common/comment'; -import Logger from '../common/logger'; -import { ITelemetry } from '../common/telemetry'; -import { CommentEvent, EventType, TimelineEvent } from '../common/timelineEvent'; -import { asPromise, formatError } from '../common/utils'; -import { getNonce, IRequestMessage, WebviewBase } from '../common/webview'; import { FolderRepositoryManager } from './folderRepositoryManager'; -import { IAccount, ILabel, IMilestone, IProject, IProjectItem, RepoAccessAndMergeMethods } from './interface'; +import { GithubItemStateEnum, IAccount, IMilestone, IProject, IProjectItem, RepoAccessAndMergeMethods } from './interface'; import { IssueModel } from './issueModel'; import { getAssigneesQuickPickItems, getLabelOptions, getMilestoneFromQuickPick, getProjectFromQuickPick } from './quickPicks'; import { isInCodespaces, vscodeDevPrLink } from './utils'; -import { ChangeAssigneesReply, Issue, ProjectItemsReply } from './views'; +import { ChangeAssigneesReply, DisplayLabel, Issue, ProjectItemsReply, SubmitReviewReply } from './views'; +import { COPILOT_ACCOUNTS, IComment } from '../common/comment'; +import { emojify, ensureEmojis } from '../common/emoji'; +import Logger from '../common/logger'; +import { PR_SETTINGS_NAMESPACE, WEBVIEW_REFRESH_INTERVAL } from '../common/settingKeys'; +import { ITelemetry } from '../common/telemetry'; +import { CommentEvent, EventType, ReviewStateValue, TimelineEvent } from '../common/timelineEvent'; +import { asPromise, formatError } from '../common/utils'; +import { generateUuid } from '../common/uuid'; +import { IRequestMessage, WebviewBase } from '../common/webview'; export class IssueOverviewPanel extends WebviewBase { public static ID: string = 'IssueOverviewPanel'; @@ -26,7 +30,7 @@ export class IssueOverviewPanel extends W */ public static currentPanel?: IssueOverviewPanel; - private static readonly _viewType: string = 'IssueOverview'; + public static readonly viewType: string = 'IssueOverview'; protected readonly _panel: vscode.WebviewPanel; protected _item: TItem; @@ -39,7 +43,10 @@ export class IssueOverviewPanel extends W folderRepositoryManager: FolderRepositoryManager, issue: IssueModel, toTheSide: Boolean = false, + _preserveFocus: boolean = true, + existingPanel?: vscode.WebviewPanel ) { + await ensureEmojis(folderRepositoryManager.context); const activeColumn = toTheSide ? vscode.ViewColumn.Beside : vscode.window.activeTextEditor @@ -58,6 +65,9 @@ export class IssueOverviewPanel extends W activeColumn || vscode.ViewColumn.Active, title, folderRepositoryManager, + undefined, + existingPanel, + undefined ); } @@ -85,7 +95,8 @@ export class IssueOverviewPanel extends W column: vscode.ViewColumn, title: string, folderRepositoryManager: FolderRepositoryManager, - private readonly type: string = IssueOverviewPanel._viewType, + private readonly type: string = IssueOverviewPanel.viewType, + existingPanel?: vscode.WebviewPanel, iconSubpath?: { light: string, dark: string, @@ -95,7 +106,7 @@ export class IssueOverviewPanel extends W this._folderRepositoryManager = folderRepositoryManager; // Create and show a new webview panel - this._panel = this._register(vscode.window.createWebviewPanel(type, title, column, { + this._panel = existingPanel ?? this._register(vscode.window.createWebviewPanel(type, title, column, { // Enable javascript in the webview enableScripts: true, retainContextWhenHidden: true, @@ -129,21 +140,59 @@ export class IssueOverviewPanel extends W }); } })); + + this._register(folderRepositoryManager.credentialStore.onDidUpgradeSession(() => { + this.updateItem(this._item); + })); + + this._register(this._panel.onDidChangeViewState(e => this.onDidChangeViewState(e))); + this.lastRefreshTime = new Date(); this.pollForUpdates(true); + this._register(vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(`${PR_SETTINGS_NAMESPACE}.${WEBVIEW_REFRESH_INTERVAL}`)) { + this.pollForUpdates(this._panel.visible, true); + } + })); + this._register({ dispose: () => clearTimeout(this.timeout) }); + + } + + private getRefreshInterval(): number { + return vscode.workspace.getConfiguration().get(`${PR_SETTINGS_NAMESPACE}.${WEBVIEW_REFRESH_INTERVAL}`) || 60; + } + + protected onDidChangeViewState(e: vscode.WebviewPanelOnDidChangeViewStateEvent): void { + if (e.webviewPanel.visible) { + this.pollForUpdates(!!this._item, true); + } } - private pollForUpdates(shorterTimeout: boolean = false): void { - const webview = shorterTimeout || vscode.window.tabGroups.all.find(group => group.activeTab?.input instanceof vscode.TabInputWebview && group.activeTab.input.viewType.endsWith(this.type)); - const timeoutDuration = 1000 * 60 * (webview ? 1 : 5); - setTimeout(async () => { - await this.refreshPanel(); - this.pollForUpdates(); + private timeout: NodeJS.Timeout | undefined = undefined; + private lastRefreshTime: Date; + private pollForUpdates(isVisible: boolean, refreshImmediately: boolean = false): void { + clearTimeout(this.timeout); + const refresh = async () => { + const previousRefreshTime = this.lastRefreshTime; + this.lastRefreshTime = await this._item.getLastUpdateTime(previousRefreshTime); + if (this.lastRefreshTime.getTime() > previousRefreshTime.getTime()) { + return this.refreshPanel(); + } + }; + + if (refreshImmediately) { + refresh(); + } + const webview = isVisible || vscode.window.tabGroups.all.find(group => group.activeTab?.input instanceof vscode.TabInputWebview && group.activeTab.input.viewType.endsWith(this.type)); + const timeoutDuration = 1000 * (webview ? this.getRefreshInterval() : (5 * 60)); + this.timeout = setTimeout(async () => { + await refresh(); + this.pollForUpdates(this._panel.visible); }, timeoutDuration); } public async refreshPanel(): Promise { if (this._panel && this._panel.visible) { - this.update(this._folderRepositoryManager, this._item); + await this.update(this._folderRepositoryManager, this._item); } } @@ -151,10 +200,17 @@ export class IssueOverviewPanel extends W return isInCodespaces(); } - protected getInitializeContext(issue: IssueModel, timelineEvents: TimelineEvent[], repositoryAccess: RepoAccessAndMergeMethods, viewerCanEdit: boolean, assignableUsers: IAccount[]): Issue { - const hasWritePermission = repositoryAccess!.hasWritePermission; + protected getInitializeContext(currentUser: IAccount, issue: IssueModel, timelineEvents: TimelineEvent[], repositoryAccess: RepoAccessAndMergeMethods, viewerCanEdit: boolean, assignableUsers: IAccount[]): Issue { + const hasWritePermission = repositoryAccess.hasWritePermission; const canEdit = hasWritePermission || viewerCanEdit; + const labels = issue.item.labels.map(label => ({ + ...label, + displayName: emojify(label.name) + })); + const context: Issue = { + owner: issue.remote.owner, + repo: issue.remote.repositoryName, number: issue.number, title: issue.title, titleHTML: issue.titleHTML, @@ -162,9 +218,10 @@ export class IssueOverviewPanel extends W createdAt: issue.createdAt, body: issue.body, bodyHTML: issue.bodyHTML, - labels: issue.item.labels, + labels: labels, author: issue.author, state: issue.state, + stateReason: issue.stateReason, events: timelineEvents, continueOnGitHub: this.continueOnGitHub(), canEdit, @@ -175,20 +232,23 @@ export class IssueOverviewPanel extends W assignees: issue.assignees ?? [], isEnterprise: issue.githubRepository.remote.isEnterprise, isDarkTheme: vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark, - canAssignCopilot: assignableUsers.find(user => COPILOT_ACCOUNTS[user.login]) !== undefined + canAssignCopilot: assignableUsers.find(user => COPILOT_ACCOUNTS[user.login]) !== undefined, + reactions: issue.item.reactions, + isAuthor: issue.author.login === currentUser.login, }; return context; } - public async updateIssue(issueModel: IssueModel): Promise { + protected async updateItem(issueModel: TItem): Promise { try { const [ issue, timelineEvents, repositoryAccess, viewerCanEdit, - assignableUsers + assignableUsers, + currentUser ] = await Promise.all([ this._folderRepositoryManager.resolveIssue( issueModel.remote.owner, @@ -198,7 +258,8 @@ export class IssueOverviewPanel extends W issueModel.getIssueTimelineEvents(), this._folderRepositoryManager.getPullRequestRepositoryAccessAndMergeMethods(issueModel), issueModel.canEdit(), - this._folderRepositoryManager.getAssignableUsers() + this._folderRepositoryManager.getAssignableUsers(), + this._folderRepositoryManager.getCurrentUser(), ]); if (!issue) { @@ -213,7 +274,7 @@ export class IssueOverviewPanel extends W Logger.debug('pr.initialize', IssueOverviewPanel.ID); this._postMessage({ command: 'pr.initialize', - pullrequest: this.getInitializeContext(issue, timelineEvents, repositoryAccess, viewerCanEdit, assignableUsers[this._item.remote.remoteName]), + pullrequest: this.getInitializeContext(currentUser, issue, timelineEvents, repositoryAccess, viewerCanEdit, assignableUsers[this._item.remote.remoteName] ?? []), }); } catch (e) { @@ -221,15 +282,32 @@ export class IssueOverviewPanel extends W } } - public async update(foldersManager: FolderRepositoryManager, issueModel: IssueModel): Promise { - this._folderRepositoryManager = foldersManager; + protected registerPrListeners() { + // none for issues + } + + public async update(foldersManager: FolderRepositoryManager, issueModel: TItem, progressLocation?: string): Promise { + if (this._folderRepositoryManager !== foldersManager) { + this._folderRepositoryManager = foldersManager; + this.registerPrListeners(); + } + this._postMessage({ command: 'set-scroll', scrollPosition: this._scrollPosition, }); - this._panel.webview.html = this.getHtmlForWebview(); - return this.updateIssue(issueModel); + if (!this._item || (this._item.number !== issueModel.number) || !this._panel.webview.html) { + this._panel.webview.html = this.getHtmlForWebview(); + this._postMessage({ command: 'pr.clear' }); + + } + + if (progressLocation) { + return vscode.window.withProgress({ location: { viewId: progressLocation } }, () => this.updateItem(issueModel)); + } else { + return this.updateItem(issueModel); + } } protected override async _onDidReceiveMessage(message: IRequestMessage) { @@ -283,7 +361,7 @@ export class IssueOverviewPanel extends W case 'pr.copy-vscodedevlink': return this.copyVscodeDevLink(); case 'pr.openOnGitHub': - return openPullRequestOnGitHub(this._item, (this._item as any)._telemetry); + return openPullRequestOnGitHub(this._item, this._telemetry); case 'pr.debug': return this.webviewDebug(message); default: @@ -291,14 +369,74 @@ export class IssueOverviewPanel extends W } } - protected submitReviewMessage(message: IRequestMessage) { - return this.createComment(message); + protected async submitReviewMessage(message: IRequestMessage) { + const comment = await this._item.createIssueComment(message.args); + const commentedEvent: CommentEvent = { + ...comment, + event: EventType.Commented + }; + const allEvents = await this._getTimeline(); + const reply: SubmitReviewReply = { + events: allEvents, + reviewedEvent: commentedEvent, + }; + this.tryScheduleCopilotRefresh(comment.body); + return this._replyMessage(message, reply); + } + + private _scheduledRefresh: Promise | undefined; + protected async tryScheduleCopilotRefresh(commentBody: string, reviewType?: ReviewStateValue) { + if (!this._scheduledRefresh) { + this._scheduledRefresh = this.doScheduleCopilotRefresh(commentBody, reviewType) + .finally(() => { + this._scheduledRefresh = undefined; + }); + } + } + + private async doScheduleCopilotRefresh(commentBody: string, reviewType?: ReviewStateValue) { + if (!COPILOT_ACCOUNTS[this._item.author.login]) { + return; + } + + if (!commentBody.includes('@copilot') && !commentBody.includes('@Copilot') && reviewType !== 'CHANGES_REQUESTED') { + return; + } + + const initialTimeline = await this._getTimeline(); + const delays = [250, 500, 1000, 2000]; + + for (const delay of delays) { + await new Promise(resolve => setTimeout(resolve, delay)); + if (this._isDisposed) { + return; + } + + try { + const currentTimeline = await this._getTimeline(); + + // Check if we have any new CopilotStarted events + if (currentTimeline.length > initialTimeline.length) { + // Found a new CopilotStarted event, refresh and stop + this.refreshPanel(); + return; + } + } catch (error) { + // If timeline fetch fails, continue with the next retry + Logger.warn(`Failed to fetch timeline during Copilot refresh retry: ${error}`, IssueOverviewPanel.ID); + } + } + + // If no new CopilotStarted events were found after all retries, still refresh once + if (!this._isDisposed) { + this.refreshPanel(); + } } private async addLabels(message: IRequestMessage): Promise { - const quickPick = vscode.window.createQuickPick(); + const quickPick = vscode.window.createQuickPick<(vscode.QuickPickItem & { name: string })>(); try { - let newLabels: ILabel[] = []; + let newLabels: DisplayLabel[] = []; quickPick.busy = true; quickPick.canSelectMany = true; @@ -314,14 +452,13 @@ export class IssueOverviewPanel extends W return quickPick.selectedItems; }); const hidePromise = asPromise(quickPick.onDidHide); - const labelsToAdd = await Promise.race([acceptPromise, hidePromise]); + const labelsToAdd = await Promise.race([acceptPromise, hidePromise]); quickPick.busy = true; + quickPick.enabled = false; if (labelsToAdd) { - await this._item.setLabels(labelsToAdd.map(r => r.label)); - const addedLabels: ILabel[] = labelsToAdd.map(label => newLabels.find(l => l.name === label.label)!); - - this._item.item.labels = addedLabels; + await this._item.setLabels(labelsToAdd.map(r => r.name)); + const addedLabels: DisplayLabel[] = labelsToAdd.map(label => newLabels.find(l => l.name === label.name)!); await this._replyMessage(message, { added: addedLabels, @@ -338,10 +475,6 @@ export class IssueOverviewPanel extends W private async removeLabel(message: IRequestMessage): Promise { try { await this._item.removeLabel(message.args); - - const index = this._item.item.labels.findIndex(label => label.name === message.args); - this._item.item.labels.splice(index, 1); - this._replyMessage(message, {}); } catch (e) { vscode.window.showErrorMessage(formatError(e)); @@ -375,6 +508,10 @@ export class IssueOverviewPanel extends W }); } + protected _getTimeline(): Promise { + return this._item.getIssueTimelineEvents(); + } + private async changeAssignees(message: IRequestMessage): Promise { const quickPick = vscode.window.createQuickPick(); @@ -393,11 +530,12 @@ export class IssueOverviewPanel extends W const hidePromise = asPromise(quickPick.onDidHide); const allAssignees = await Promise.race<(vscode.QuickPickItem & { user: IAccount })[] | void>([acceptPromise, hidePromise]); quickPick.busy = true; + quickPick.enabled = false; if (allAssignees) { const newAssignees: IAccount[] = allAssignees.map(item => item.user); await this._item.replaceAssignees(newAssignees); - const events = await this._item.getIssueTimelineEvents(); + const events = await this._getTimeline(); const reply: ChangeAssigneesReply = { assignees: newAssignees, events @@ -464,7 +602,7 @@ export class IssueOverviewPanel extends W const newAssignees = (this._item.assignees ?? []).concat(currentUser); await this._item.replaceAssignees(newAssignees); } - const events = await this._item.getIssueTimelineEvents(); + const events = await this._getTimeline(); const reply: ChangeAssigneesReply = { assignees: this._item.assignees ?? [], events @@ -482,7 +620,7 @@ export class IssueOverviewPanel extends W const newAssignees = (this._item.assignees ?? []).concat(copilotUser); await this._item.replaceAssignees(newAssignees); } - const events = await this._item.getIssueTimelineEvents(); + const events = await this._getTimeline(); const reply: ChangeAssigneesReply = { assignees: this._item.assignees ?? [], events @@ -540,30 +678,21 @@ export class IssueOverviewPanel extends W }); } - private close(message: IRequestMessage) { - vscode.commands - .executeCommand('pr.close', this._item, message.args) - .then(comment => { - if (comment) { - this._replyMessage(message, { - value: comment, - }); - } else { - this._throwError(message, 'Close cancelled'); - } - }); - } - - private createComment(message: IRequestMessage) { - return this._item.createIssueComment(message.args).then(comment => { - const commentedEvent: CommentEvent = { + protected async close(message: IRequestMessage) { + let comment: IComment | undefined; + if (message.args) { + comment = await this._item.createIssueComment(message.args); + } + const closeUpdate = await this._item.close(); + const result: CloseResult = { + state: closeUpdate.item.state.toUpperCase() as GithubItemStateEnum, + commentEvent: comment ? { ...comment, event: EventType.Commented - }; - return this._replyMessage(message, { - event: commentedEvent, - }); - }); + } : undefined, + closeEvent: closeUpdate.closedEvent + }; + this._replyMessage(message, result); } protected set _currentPanel(panel: IssueOverviewPanel | undefined) { @@ -577,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'); diff --git a/src/github/loggingOctokit.ts b/src/github/loggingOctokit.ts index 075b556a35..09813b75ae 100644 --- a/src/github/loggingOctokit.ts +++ b/src/github/loggingOctokit.ts @@ -7,13 +7,13 @@ import { Octokit } from '@octokit/rest'; import { ApolloClient, ApolloQueryResult, FetchResult, MutationOptions, NormalizedCacheObject, OperationVariables, QueryOptions } from 'apollo-boost'; import { bulkhead, BulkheadPolicy } from 'cockatiel'; import * as vscode from 'vscode'; +import { RateLimit } from './graphql'; +import { IRawFileChange } from './interface'; +import { restPaginate } from './utils'; import { GitHubRef } from '../common/githubRef'; import Logger from '../common/logger'; import { GitHubRemote } from '../common/remote'; import { ITelemetry } from '../common/telemetry'; -import { RateLimit } from './graphql'; -import { IRawFileChange } from './interface'; -import { restPaginate } from './utils'; interface RestResponse { headers: { @@ -22,6 +22,12 @@ interface RestResponse { } } +interface RateLimitResult { + data: { + rateLimit: RateLimit | undefined + } | undefined; +} + export class RateLogger { private bulkhead: BulkheadPolicy = bulkhead(140); private static ID = 'RateLimit'; @@ -56,7 +62,7 @@ export class RateLogger { return this.bulkhead.execute(() => apiRequest()) as T; } - public async logRateLimit(info: string | undefined, result: Promise<{ data: { rateLimit: RateLimit | undefined } | undefined } | undefined>, isRest: boolean = false) { + public async logRateLimit(info: string | undefined, result: Promise, isRest: boolean = false) { let rateLimitInfo: { limit: number, remaining: number, cost: number } | undefined; try { const resolvedResult = await result; @@ -115,7 +121,7 @@ export class LoggingApolloClient { if (result === undefined) { throw new Error('API call count has exceeded a rate limit.'); } - this._rateLogger.logRateLimit(logInfo, result as any); + this._rateLogger.logRateLimit(logInfo, result as Promise); return result; } @@ -125,7 +131,7 @@ export class LoggingApolloClient { if (result === undefined) { throw new Error('API call count has exceeded a rate limit.'); } - this._rateLogger.logRateLimit(logInfo, result as any); + this._rateLogger.logRateLimit(logInfo, result as Promise); return result; } } diff --git a/src/github/markdownUtils.ts b/src/github/markdownUtils.ts index 9e8f44a521..cf4c766af7 100644 --- a/src/github/markdownUtils.ts +++ b/src/github/markdownUtils.ts @@ -6,14 +6,15 @@ import * as marked from 'marked'; import 'url-search-params-polyfill'; import * as vscode from 'vscode'; -import Logger from '../common/logger'; -import { CODE_PERMALINK, findCodeLinkLocally } from '../issues/issueLinkLookup'; import { PullRequestDefaults } from './folderRepositoryManager'; import { GithubItemStateEnum, User } from './interface'; import { IssueModel } from './issueModel'; import { PullRequestModel } from './pullRequestModel'; import { RepositoriesManager } from './repositoriesManager'; import { getIssueNumberLabelFromParsed, ISSUE_OR_URL_EXPRESSION, makeLabel, parseIssueExpressionOutput, UnsatisfiedChecks } from './utils'; +import { ensureEmojis } from '../common/emoji'; +import Logger from '../common/logger'; +import { CODE_PERMALINK, findCodeLinkLocally } from '../issues/issueLinkLookup'; function getIconString(issue: IssueModel) { switch (issue.state) { @@ -37,7 +38,9 @@ function getIconMarkdown(issue: IssueModel) { return `$(issues)`; } case GithubItemStateEnum.Closed: { - return `$(issue-closed)`; + // Use grey for issues closed as "not planned", purple for "completed" + const color = issue.stateReason !== 'COMPLETED' ? '#6a737d' : '#8957e5'; + return `$(issue-closed)`; } } } @@ -168,9 +171,11 @@ export async function issueMarkdown( year: 'numeric', })} \n`, ); + const titleWithDraft = (issue instanceof PullRequestModel && issue.isDraft) ? `\[DRAFT\] ${issue.title}` : issue.title; const title = marked - .parse(issue.title, { + .parse(titleWithDraft, { renderer: new PlainTextRenderer(), + smartypants: true, }) .trim(); markdown.appendMarkdown( @@ -178,6 +183,7 @@ export async function issueMarkdown( ); let body = marked.parse(issue.body, { renderer: new PlainTextRenderer(), + smartypants: true, }); markdown.appendMarkdown(' \n'); body = body.length > ISSUE_BODY_LENGTH ? body.substr(0, ISSUE_BODY_LENGTH) + '...' : body; @@ -187,6 +193,7 @@ export async function issueMarkdown( markdown.appendMarkdown(body + ' \n'); if (issue.item.labels.length > 0) { + await ensureEmojis(context); markdown.appendMarkdown('  \n'); issue.item.labels.forEach(label => { markdown.appendMarkdown( @@ -210,7 +217,7 @@ export async function issueMarkdown( comment.body.length > ISSUE_BODY_LENGTH ? comment.body.substr(0, ISSUE_BODY_LENGTH) + '...' : comment.body, - { renderer: new PlainTextRenderer() }, + { renderer: new PlainTextRenderer(), smartypants: true }, ); commentText = await findLinksInIssue(commentText, issue); markdown.appendMarkdown(commentText); @@ -230,6 +237,13 @@ export async function issueMarkdown( } export class PlainTextRenderer extends marked.Renderer { + private allowSimpleMarkdown: boolean; + + constructor(allowSimpleMarkdown: boolean = false) { + super(); + this.allowSimpleMarkdown = allowSimpleMarkdown; + } + override code(code: string, _infostring: string | undefined): string { return code; } @@ -279,6 +293,9 @@ export class PlainTextRenderer extends marked.Renderer { return text; } override codespan(code: string): string { + if (this.allowSimpleMarkdown) { + return `\`${code}\``; + } return `\\\`${code}\\\``; } override br(): string { diff --git a/src/github/notifications.ts b/src/github/notifications.ts deleted file mode 100644 index 3c729add4a..0000000000 --- a/src/github/notifications.ts +++ /dev/null @@ -1,384 +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 { OctokitResponse } from '@octokit/types'; -import * as vscode from 'vscode'; -import { AuthProvider } from '../common/authentication'; -import { Disposable } from '../common/lifecycle'; -import Logger from '../common/logger'; -import { NOTIFICATION_SETTING, PR_SETTINGS_NAMESPACE } from '../common/settingKeys'; -import { createPRNodeUri } from '../common/uri'; -import { PullRequestsTreeDataProvider } from '../view/prsTreeDataProvider'; -import { CategoryTreeNode } from '../view/treeNodes/categoryNode'; -import { PRNode } from '../view/treeNodes/pullRequestNode'; -import { TreeNode } from '../view/treeNodes/treeNode'; -import { CredentialStore, GitHub } from './credentials'; -import { GitHubRepository } from './githubRepository'; -import { PullRequestState } from './graphql'; -import { IssueModel } from './issueModel'; -import { PullRequestModel } from './pullRequestModel'; -import { RepositoriesManager } from './repositoriesManager'; -import { hasEnterpriseUri } from './utils'; - -const DEFAULT_POLLING_DURATION = 60; - -export class Notification { - public readonly identifier; - public readonly threadId: number; - public readonly repositoryName: string; - public readonly pullRequestNumber: number; - public pullRequestModel?: PullRequestModel; - - constructor(identifier: string, threadId: number, repositoryName: string, - pullRequestNumber: number, pullRequestModel?: PullRequestModel) { - - this.identifier = identifier; - this.threadId = threadId; - this.repositoryName = repositoryName; - this.pullRequestNumber = pullRequestNumber; - this.pullRequestModel = pullRequestModel; - } -} - -export class NotificationProvider extends Disposable { - private static ID = 'NotificationProvider'; - private readonly _gitHubPrsTree: PullRequestsTreeDataProvider; - private readonly _credentialStore: CredentialStore; - private _authProvider: AuthProvider | undefined; - // The key uniquely identifies a PR from a Repository. The key is created with `getPrIdentifier` - private _notifications: Map; - private readonly _reposManager: RepositoriesManager; - - private _pollingDuration: number; - private _lastModified: string; - private _pollingHandler: NodeJS.Timeout | null; - - private _onDidChangeNotifications: vscode.EventEmitter = this._register(new vscode.EventEmitter()); - public readonly onDidChangeNotifications = this._onDidChangeNotifications.event; - - constructor( - gitHubPrsTree: PullRequestsTreeDataProvider, - credentialStore: CredentialStore, - reposManager: RepositoriesManager - ) { - super(); - this._gitHubPrsTree = gitHubPrsTree; - this._credentialStore = credentialStore; - this._reposManager = reposManager; - this._notifications = new Map(); - - this._lastModified = ''; - this._pollingDuration = DEFAULT_POLLING_DURATION; - this._pollingHandler = null; - - this.registerAuthProvider(credentialStore); - - for (const manager of this._reposManager.folderManagers) { - this._register(manager.onDidChangeGithubRepositories(() => { - this.refreshOrLaunchPolling(); - })); - } - - this._register(gitHubPrsTree.onDidChangeTreeData((node) => { - if (NotificationProvider.isPRNotificationsOn()) { - this.adaptPRNotifications(node); - } - })); - this._register(gitHubPrsTree.onDidChange(() => { - if (NotificationProvider.isPRNotificationsOn()) { - this.adaptPRNotifications(); - } - })); - - this._register(vscode.workspace.onDidChangeConfiguration((e) => { - if (e.affectsConfiguration(`${PR_SETTINGS_NAMESPACE}.${NOTIFICATION_SETTING}`)) { - this.checkNotificationSetting(); - } - })); - } - - private static isPRNotificationsOn() { - return ( - vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(NOTIFICATION_SETTING) === - 'pullRequests' - ); - } - - private registerAuthProvider(credentialStore: CredentialStore) { - if (credentialStore.isAuthenticated(AuthProvider.githubEnterprise) && hasEnterpriseUri()) { - this._authProvider = AuthProvider.githubEnterprise; - } else if (credentialStore.isAuthenticated(AuthProvider.github)) { - this._authProvider = AuthProvider.github; - } - - this._register(vscode.authentication.onDidChangeSessions(_ => { - if (credentialStore.isAuthenticated(AuthProvider.githubEnterprise) && hasEnterpriseUri()) { - this._authProvider = AuthProvider.githubEnterprise; - } - - if (credentialStore.isAuthenticated(AuthProvider.github)) { - this._authProvider = AuthProvider.github; - } - })); - } - - private getPrIdentifier(pullRequest: IssueModel | OctokitResponse['data']): string { - if (pullRequest instanceof IssueModel) { - return `${pullRequest.remote.url}:${pullRequest.number}`; - } - const splitPrUrl = pullRequest.subject.url.split('/'); - const prNumber = splitPrUrl[splitPrUrl.length - 1]; - return `${pullRequest.repository.html_url}.git:${prNumber}`; - } - - /* Takes a PullRequestModel or a PRIdentifier and - returns true if there is a Notification for the corresponding PR */ - public hasNotification(pullRequest: IssueModel | string): boolean { - const identifier = pullRequest instanceof IssueModel ? - this.getPrIdentifier(pullRequest) : - pullRequest; - const prNotifications = this._notifications.get(identifier); - return prNotifications !== undefined && prNotifications.length > 0; - } - - private updateViewBadge() { - const treeView = this._gitHubPrsTree.view; - const singularMessage = vscode.l10n.t('1 notification'); - const pluralMessage = vscode.l10n.t('{0} notifications', this._notifications.size); - treeView.badge = this._notifications.size !== 0 ? { - tooltip: this._notifications.size === 1 ? singularMessage : pluralMessage, - value: this._notifications.size - } : undefined; - } - - private adaptPRNotifications(node: TreeNode | void) { - if (this._pollingHandler === undefined) { - this.startPolling(); - } - - if (node instanceof PRNode) { - const prNotifications = this._notifications.get(this.getPrIdentifier(node.pullRequestModel)); - if (prNotifications) { - for (const prNotification of prNotifications) { - if (prNotification) { - prNotification.pullRequestModel = node.pullRequestModel; - return; - } - } - } - } - - this._gitHubPrsTree.cachedChildren().then(async (catNodes: CategoryTreeNode[]) => { - let allPrs: PullRequestModel[] = []; - - for (const catNode of catNodes) { - if (catNode.id === 'All Open') { - if (catNode.prs.size === 0) { - for (const prNode of await catNode.cachedChildren()) { - if (prNode instanceof PRNode) { - allPrs.push(prNode.pullRequestModel); - } - } - } - else { - allPrs = Array.from(catNode.prs.values()); - } - - } - } - - allPrs.forEach((pr) => { - const prNotifications = this._notifications.get(this.getPrIdentifier(pr)); - if (prNotifications) { - for (const prNotification of prNotifications) { - prNotification.pullRequestModel = pr; - } - } - }); - }); - } - - public refreshOrLaunchPolling() { - this._lastModified = ''; - this.checkNotificationSetting(); - } - - private checkNotificationSetting() { - const notificationsTurnedOn = NotificationProvider.isPRNotificationsOn(); - if (notificationsTurnedOn && this._pollingHandler === null) { - this.startPolling(); - } - else if (!notificationsTurnedOn && this._pollingHandler !== null) { - clearInterval(this._pollingHandler); - this._lastModified = ''; - this._pollingHandler = null; - this._pollingDuration = DEFAULT_POLLING_DURATION; - - this._onDidChangeNotifications.fire(this.uriFromNotifications()); - this._notifications.clear(); - this.updateViewBadge(); - } - } - - private uriFromNotifications(): vscode.Uri[] { - const notificationUris: vscode.Uri[] = []; - for (const [identifier, prNotifications] of this._notifications.entries()) { - if (prNotifications.length) { - notificationUris.push(createPRNodeUri(identifier)); - } - } - return notificationUris; - } - - private getGitHub(): GitHub | undefined { - return (this._authProvider !== undefined) ? - this._credentialStore.getHub(this._authProvider) : - undefined; - } - - private async getNotifications() { - const gitHub = this.getGitHub(); - if (gitHub === undefined) - return undefined; - const { data, headers } = await gitHub.octokit.call(gitHub.octokit.api.activity.listNotificationsForAuthenticatedUser, {}); - return { data: data, headers: headers }; - } - - private async markNotificationThreadAsRead(thredId) { - const github = this.getGitHub(); - if (!github) { - return; - } - await github.octokit.call(github.octokit.api.activity.markThreadAsRead, { - thread_id: thredId - }); - } - - public async markPrNotificationsAsRead(pullRequestModel: IssueModel) { - const identifier = this.getPrIdentifier(pullRequestModel); - const prNotifications = this._notifications.get(identifier); - if (prNotifications && prNotifications.length) { - for (const notification of prNotifications) { - await this.markNotificationThreadAsRead(notification.threadId); - } - - const uris = this.uriFromNotifications(); - this._onDidChangeNotifications.fire(uris); - this._notifications.delete(identifier); - this.updateViewBadge(); - } - } - - private async pollForNewNotifications() { - const response = await this.getNotifications(); - if (response === undefined) { - return; - } - const { data, headers } = response; - const pollTimeSuggested = Number(headers['x-poll-interval']); - - // Adapt polling interval if it has changed. - if (pollTimeSuggested !== this._pollingDuration) { - this._pollingDuration = pollTimeSuggested; - if (this._pollingHandler && NotificationProvider.isPRNotificationsOn()) { - Logger.appendLine('Notifications: Clearing interval', NotificationProvider.ID); - clearInterval(this._pollingHandler); - Logger.appendLine(`Notifications: Starting new polling interval with ${this._pollingDuration}`, NotificationProvider.ID); - this.startPolling(); - } - } - - // Only update if the user has new notifications - if (this._lastModified === headers['last-modified']) { - return; - } - this._lastModified = headers['last-modified'] ?? ''; - - const prNodesToUpdate = this.uriFromNotifications(); - this._notifications.clear(); - - const currentRepos = new Map(); - - this._reposManager.folderManagers.forEach(manager => { - manager.gitHubRepositories.forEach(repo => { - currentRepos.set(repo.remote.url, repo); - }); - }); - - await Promise.all(data.map(async (notification) => { - - const repoUrl = `${notification.repository.html_url}.git`; - const githubRepo = currentRepos.get(repoUrl); - - if (githubRepo && notification.subject.type === 'PullRequest') { - const splitPrUrl = notification.subject.url.split('/'); - const prNumber = Number(splitPrUrl[splitPrUrl.length - 1]); - const identifier = this.getPrIdentifier(notification); - - const { remote, query, schema } = await githubRepo.ensure(); - - const { data } = await query({ - query: schema.PullRequestState, - variables: { - owner: remote.owner, - name: remote.repositoryName, - number: prNumber, - }, - }); - - if (data.repository === null) { - Logger.error('Unexpected null repository when getting notifications', NotificationProvider.ID); - } - - // We only consider open PullRequests as these are displayed in the AllOpen PR category. - // Other categories could have queries with closed PRs, but its hard to figure out if a PR - // belongs to a query without loading each PR of that query. - if (data.repository?.pullRequest.state === 'OPEN') { - - const newNotification = new Notification( - identifier, - Number(notification.id), - notification.repository.name, - Number(prNumber) - ); - - const currentPrNotifications = this._notifications.get(identifier); - if (currentPrNotifications === undefined) { - this._notifications.set( - identifier, [newNotification] - ); - } - else { - currentPrNotifications.push(newNotification); - } - } - - } - })); - - this.adaptPRNotifications(); - - this.updateViewBadge(); - for (const uri of this.uriFromNotifications()) { - if (prNodesToUpdate.find(u => u.fsPath === uri.fsPath) === undefined) { - prNodesToUpdate.push(uri); - } - } - - this._onDidChangeNotifications.fire(prNodesToUpdate); - } - - private startPolling() { - this.pollForNewNotifications(); - this._pollingHandler = setInterval( - function (notificationProvider: NotificationProvider) { - notificationProvider.pollForNewNotifications(); - }, - this._pollingDuration * 1000, - this - ); - this._register({ dispose: () => clearInterval(this._pollingHandler!) }); - } -} \ No newline at end of file diff --git a/src/github/overviewRestorer.ts b/src/github/overviewRestorer.ts new file mode 100644 index 0000000000..5092d0063f --- /dev/null +++ b/src/github/overviewRestorer.ts @@ -0,0 +1,95 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { CredentialStore } from './credentials'; +import { FolderRepositoryManager } from './folderRepositoryManager'; +import { GitHubRepository } from './githubRepository'; +import { IssueOverviewPanel } from './issueOverview'; +import { PullRequestOverviewPanel } from './pullRequestOverview'; +import { RepositoriesManager } from './repositoriesManager'; +import { PullRequest } from './views'; +import { Disposable } from '../common/lifecycle'; +import Logger from '../common/logger'; +import { ITelemetry } from '../common/telemetry'; + +export class OverviewRestorer extends Disposable implements vscode.WebviewPanelSerializer { + private static ID = 'OverviewRestorer'; + + constructor(private readonly _repositoriesManager: RepositoriesManager, + private readonly _telemetry: ITelemetry, + private readonly _extensionUri: vscode.Uri, + private readonly _credentialStore: CredentialStore + ) { + super(); + this._register(vscode.window.registerWebviewPanelSerializer(IssueOverviewPanel.viewType, this)); + this._register(vscode.window.registerWebviewPanelSerializer(PullRequestOverviewPanel.viewType, this)); + } + + async deserializeWebviewPanel(webviewPanel: vscode.WebviewPanel, state: PullRequest): Promise { + await this.waitForAuth(); + await this.waitForAnyGitHubRepos(this._repositoriesManager); + + if (!state || !state.number || this._repositoriesManager.folderManagers.length === 0) { + webviewPanel.dispose(); + return; + } + + let repo: GitHubRepository | undefined; + let folderManager: FolderRepositoryManager | undefined; + for (const manager of this._repositoriesManager.folderManagers) { + const githubRepository = manager.findExistingGitHubRepository({ owner: state.owner, repositoryName: state.repo }); + if (githubRepository) { + repo = githubRepository; + folderManager = manager; + break; + } + } + + if (!repo || !folderManager) { + folderManager = this._repositoriesManager.folderManagers[0]; + repo = await folderManager.createGitHubRepositoryFromOwnerName(state.owner, state.repo); + } + + if (state.isIssue) { + const issueModel = await repo.getIssue(state.number, true); + if (!issueModel) { + webviewPanel.dispose(); + return; + } + return IssueOverviewPanel.createOrShow(this._telemetry, this._extensionUri, folderManager, issueModel, undefined, true, webviewPanel); + } else { + const pullRequestModel = await repo.getPullRequest(state.number, true); + if (!pullRequestModel) { + webviewPanel.dispose(); + return; + } + return PullRequestOverviewPanel.createOrShow(this._telemetry, this._extensionUri, folderManager, pullRequestModel, undefined, true, webviewPanel); + } + } + + protected async waitForAuth(): Promise { + if (this._credentialStore.isAnyAuthenticated()) { + return; + } + return new Promise(resolve => this._credentialStore.onDidGetSession(() => resolve())); + } + + protected async waitForAnyGitHubRepos(reposManager: RepositoriesManager): Promise { + // Check if any folder manager already has GitHub repositories + if (reposManager.folderManagers.some(manager => manager.gitHubRepositories.length > 0)) { + return; + } + + Logger.appendLine('Waiting for GitHub repositories.', OverviewRestorer.ID); + return new Promise(resolve => { + const disposable = reposManager.onDidChangeAnyGitHubRepository(() => { + Logger.appendLine('Found GitHub repositories.', OverviewRestorer.ID); + disposable.dispose(); + resolve(); + }); + }); + } +} \ No newline at end of file diff --git a/src/github/prComment.ts b/src/github/prComment.ts index c31fce3ecf..12d74662f3 100644 --- a/src/github/prComment.ts +++ b/src/github/prComment.ts @@ -5,15 +5,15 @@ import * as path from 'path'; import * as vscode from 'vscode'; -import { IComment } from '../common/comment'; +import { GitHubRepository } from './githubRepository'; +import { IAccount } from './interface'; +import { updateCommentReactions } from './utils'; +import { COPILOT_ACCOUNTS, IComment } from '../common/comment'; import { emojify, ensureEmojis } from '../common/emoji'; import Logger from '../common/logger'; import { DataUri } from '../common/uri'; import { ALLOWED_USERS, JSDOC_NON_USERS, PHPDOC_NON_USERS } from '../common/user'; -import { stringReplaceAsync } from '../common/utils'; -import { GitHubRepository } from './githubRepository'; -import { IAccount } from './interface'; -import { updateCommentReactions } from './utils'; +import { escapeRegExp, stringReplaceAsync } from '../common/utils'; export interface GHPRCommentThread extends vscode.CommentThread2 { gitHubThreadId: string; @@ -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, ) { @@ -169,10 +174,11 @@ export class TemporaryComment extends CommentBase { super(parent); this.mode = vscode.CommentMode.Preview; this.originalAuthor = { - name: currentUser.login, + name: currentUser.specialDisplayName ?? currentUser.login, 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; @@ -190,7 +196,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 { @@ -208,6 +216,7 @@ export class TemporaryComment extends CommentBase { const SUGGESTION_EXPRESSION = /```suggestion(\u0020*(\r\n|\n))((?[\s\S]*?)(\r\n|\n))?```/; const IMG_EXPRESSION = /.+?)['"].*?>/g; +const UUID_EXPRESSION = /[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}/; export class GHPRComment extends CommentBase { private static ID = 'GHPRComment'; @@ -221,14 +230,17 @@ export class GHPRComment extends CommentBase { private _rawBody: string | vscode.MarkdownString; private replacedBody: string; + private githubRepository: GitHubRepository | undefined; - constructor(private readonly context: vscode.ExtensionContext, comment: IComment, parent: GHPRCommentThread, private readonly githubRepositories?: GitHubRepository[]) { + constructor(private readonly context: vscode.ExtensionContext, comment: IComment, parent: GHPRCommentThread, githubRepositories?: GitHubRepository[]) { super(parent); this.rawComment = comment; this.originalAuthor = { - name: comment.user!.login, + name: comment.user?.specialDisplayName ?? comment.user!.login, iconPath: comment.user && comment.user.avatarUrl ? vscode.Uri.parse(comment.user.avatarUrl) : undefined, }; + const url = vscode.Uri.parse(comment.url); + this.githubRepository = githubRepositories?.find(repo => repo.remote.host === url.authority); const avatarUrisPromise = comment.user ? DataUri.avatarCirclesAsImageDataUris(context, [comment.user], 28, 28) : Promise.resolve([]); this.doSetBody(comment.body, !comment.user).then(async () => { // only refresh if there's no user. If there's a user, we'll refresh in the then. @@ -243,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) { @@ -282,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; } @@ -328,6 +343,7 @@ export class GHPRComment extends CommentBase { if (match) { return suggestionBody ? suggestionBody : ''; } + return undefined; } public commentEditId() { @@ -364,20 +380,18 @@ ${args[3] ?? ''} } private async replacePermalink(body: string): Promise { - const githubRepositories = this.githubRepositories; - if (!githubRepositories || githubRepositories.length === 0) { + const githubRepository = this.githubRepository; + if (!githubRepository) { return body; } - const expression = new RegExp(`https://github.com/(.+)/${githubRepositories[0].remote.repositoryName}/blob/([0-9a-f]{40})/(.*)#L([0-9]+)(-L([0-9]+))?`, 'g'); + const repoName = escapeRegExp(githubRepository.remote.repositoryName); + const expression = new RegExp(`https://github.com/(.+)/${repoName}/blob/([0-9a-f]{40})/(.*)#L([0-9]+)(-L([0-9]+))?`, 'g'); return stringReplaceAsync(body, expression, async (match: string, owner: string, sha: string, file: string, start: string, _endGroup?: string, end?: string, index?: number) => { if (index && (index > 0) && (body.charAt(index - 1) === '(')) { return match; } - const githubRepository = githubRepositories.find(repository => repository.remote.owner.toLocaleLowerCase() === owner.toLocaleLowerCase()); - if (!githubRepository) { - return match; - } + const startLine = parseInt(start); const endLine = end ? parseInt(end) : startLine + 1; const lineContents = await githubRepository.getLines(sha, file, startLine, endLine); @@ -398,6 +412,15 @@ ${lineContents} }); } + private replaceImages(body: string): string { + const html = this.rawComment.bodyHTML; + if (!html) { + return body; + } + + return replaceImages(body, html, this.githubRepository?.remote.host); + } + private replaceNewlines(body: string) { return body.replace(/(?${UUID_EXPRESSION.source})`); + let originalMatch = markdownBody.match(originalExpression); + const htmlHost = escapeRegExp(host === 'github.com' ? 'githubusercontent.com' : host); + + while (originalMatch) { + if (originalMatch.groups?.uuid) { + const uuid = escapeRegExp(originalMatch.groups.uuid); + const htmlExpression = new RegExp(`https:\/\/([^"]*${htmlHost})\/[^?]+${uuid}[^"]+`); + const htmlMatch = htmlBody.match(htmlExpression); + if (htmlMatch && htmlMatch[0]) { + markdownBody = markdownBody.replace(originalMatch[0], htmlMatch[0]); + } else { + return markdownBody; + } + } + originalMatch = markdownBody.match(originalExpression); } + return markdownBody; } diff --git a/src/github/pullRequestGitHelper.ts b/src/github/pullRequestGitHelper.ts index 0c6ccd595a..74239cf974 100644 --- a/src/github/pullRequestGitHelper.ts +++ b/src/github/pullRequestGitHelper.ts @@ -7,12 +7,12 @@ * Inspired by and includes code from GitHub/VisualStudio project, obtained from https://github.com/github/VisualStudio/blob/165a97bdcab7559e0c4393a571b9ff2aed4ba8a7/src/GitHub.App/Services/PullRequestService.cs */ import * as vscode from 'vscode'; +import { IResolvedPullRequestModel, PullRequestModel } from './pullRequestModel'; import { Branch, Repository } from '../api/api'; import Logger from '../common/logger'; import { Protocol } from '../common/protocol'; import { parseRepositoryRemotes, Remote } from '../common/remote'; import { PR_SETTINGS_NAMESPACE, PULL_PR_BRANCH_BEFORE_CHECKOUT, PullPRBranchVariants } from '../common/settingKeys'; -import { IResolvedPullRequestModel, PullRequestModel } from './pullRequestModel'; const PullRequestRemoteMetadataKey = 'github-pr-remote'; export const PullRequestMetadataKey = 'github-pr-owner-number'; @@ -33,6 +33,13 @@ export interface BaseBranchMetadata { branch: string; } +export type BranchInfo = { + branch: string; + remote?: string; + createdForPullRequest?: boolean; + remoteInUse?: boolean; +}; + export class PullRequestGitHelper { static ID = 'PullRequestGitHelper'; static async checkoutFromFork( @@ -87,50 +94,67 @@ export class PullRequestGitHelper { return PullRequestGitHelper.checkoutFromFork(repository, pullRequest, remote && remote.remoteName, progress); } - const branchName = pullRequest.head.ref; + const originalBranchName = pullRequest.head.ref; const remoteName = remote.remoteName; let branch: Branch; + let localBranchName = originalBranchName; // This will be the branch we actually checkout + + // Always fetch the remote branch first to ensure we have the latest commits + const trackedBranchName = `refs/remotes/${remoteName}/${originalBranchName}`; + Logger.appendLine(`Fetch tracked branch ${trackedBranchName}`, PullRequestGitHelper.ID); + progress.report({ message: vscode.l10n.t('Fetching branch {0}', originalBranchName) }); + await repository.fetch(remoteName, originalBranchName); + const trackedBranch = await repository.getBranch(trackedBranchName); try { - branch = await repository.getBranch(branchName); + branch = await repository.getBranch(localBranchName); + // Check if local branch is pointing to the same commit as the remote + if (branch.commit !== trackedBranch.commit) { + Logger.appendLine(`Local branch ${localBranchName} commit ${branch.commit} differs from remote commit ${trackedBranch.commit}. Creating new branch to avoid overwriting user's work.`, PullRequestGitHelper.ID); + // Instead of deleting the user's branch, create a unique branch name to avoid conflicts + const uniqueBranchName = await PullRequestGitHelper.calculateUniqueBranchNameForPR(repository, pullRequest); + Logger.appendLine(`Creating branch ${uniqueBranchName} for PR checkout`, PullRequestGitHelper.ID); + progress.report({ message: vscode.l10n.t('Creating branch {0} for pull request', uniqueBranchName) }); + await repository.createBranch(uniqueBranchName, false, trackedBranch.commit); + await repository.setBranchUpstream(uniqueBranchName, trackedBranchName); + // Use the unique branch name for checkout + localBranchName = uniqueBranchName; + branch = await repository.getBranch(localBranchName); + } + // Make sure we aren't already on this branch if (repository.state.HEAD?.name === branch.name) { - Logger.appendLine(`Tried to checkout ${branchName}, but branch is already checked out.`, PullRequestGitHelper.ID); + Logger.appendLine(`Tried to checkout ${localBranchName}, but branch is already checked out.`, PullRequestGitHelper.ID); return; } - Logger.debug(`Checkout ${branchName}`, PullRequestGitHelper.ID); - progress.report({ message: vscode.l10n.t('Checking out {0}', branchName) }); - await repository.checkout(branchName); + + Logger.debug(`Checkout ${localBranchName}`, PullRequestGitHelper.ID); + progress.report({ message: vscode.l10n.t('Checking out {0}', localBranchName) }); + await repository.checkout(localBranchName); if (!branch.upstream) { // this branch is not associated with upstream yet - const trackedBranchName = `refs/remotes/${remoteName}/${branchName}`; - await repository.setBranchUpstream(branchName, trackedBranchName); + await repository.setBranchUpstream(localBranchName, trackedBranchName); } if (branch.behind !== undefined && branch.behind > 0 && branch.ahead === 0) { Logger.debug(`Pull from upstream`, PullRequestGitHelper.ID); - progress.report({ message: vscode.l10n.t('Pulling {0}', branchName) }); + progress.report({ message: vscode.l10n.t('Pulling {0}', localBranchName) }); await repository.pull(); } } catch (err) { - // there is no local branch with the same name, so we are good to fetch, create and checkout the remote branch. + // there is no local branch with the same name, so we are good to create and checkout the remote branch. Logger.appendLine( - `Branch ${remoteName}/${branchName} doesn't exist on local disk yet.`, + `Branch ${localBranchName} doesn't exist on local disk yet. Creating from remote.`, PullRequestGitHelper.ID, ); - const trackedBranchName = `refs/remotes/${remoteName}/${branchName}`; - Logger.appendLine(`Fetch tracked branch ${trackedBranchName}`, PullRequestGitHelper.ID); - progress.report({ message: vscode.l10n.t('Fetching branch {0}', branchName) }); - await repository.fetch(remoteName, branchName); - const trackedBranch = await repository.getBranch(trackedBranchName); // create branch - progress.report({ message: vscode.l10n.t('Creating and checking out branch {0}', branchName) }); - await repository.createBranch(branchName, true, trackedBranch.commit); - await repository.setBranchUpstream(branchName, trackedBranchName); + progress.report({ message: vscode.l10n.t('Creating and checking out branch {0}', localBranchName) }); + await repository.createBranch(localBranchName, true, trackedBranch.commit); + await repository.setBranchUpstream(localBranchName, trackedBranchName); } - await PullRequestGitHelper.associateBranchWithPullRequest(repository, pullRequest, branchName); + await PullRequestGitHelper.associateBranchWithPullRequest(repository, pullRequest, localBranchName); } static async checkoutExistingPullRequestBranch(repository: Repository, pullRequest: PullRequestModel, progress: vscode.Progress<{ message?: string; increment?: number }>) { @@ -185,12 +209,7 @@ export class PullRequestGitHelper { static async getBranchNRemoteForPullRequest( repository: Repository, pullRequest: PullRequestModel, - ): Promise<{ - branch: string; - remote?: string; - createdForPullRequest?: boolean; - remoteInUse?: boolean; - } | null> { + ): Promise { let branchName: string | null = null; try { const key = PullRequestGitHelper.buildPullRequestMetadata(pullRequest); @@ -312,8 +331,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; } @@ -340,10 +360,7 @@ export class PullRequestGitHelper { static async isRemoteCreatedForPullRequest(repository: Repository, remoteName: string) { try { - Logger.debug( - `Check if remote '${remoteName}' is created for pull request - start`, - PullRequestGitHelper.ID, - ); + Logger.debug(`Check if remote '${remoteName}' is created for pull request - start`, PullRequestGitHelper.ID); const isForPR = await repository.getConfig(`remote.${remoteName}.${PullRequestRemoteMetadataKey}`); Logger.debug(`Check if remote '${remoteName}' is created for pull request - end`, PullRequestGitHelper.ID); return isForPR === 'true'; diff --git a/src/github/pullRequestModel.ts b/src/github/pullRequestModel.ts index 19cb2ae19b..ad578ef47b 100644 --- a/src/github/pullRequestModel.ts +++ b/src/github/pullRequestModel.ts @@ -8,23 +8,12 @@ import * as path from 'path'; import equals from 'fast-deep-equal'; import gql from 'graphql-tag'; import * as vscode from 'vscode'; -import { Repository } from '../api/api'; -import { DiffSide, IComment, IReviewThread, SubjectType, ViewedState } from '../common/comment'; -import { getModifiedContentFromDiffHunk, parseDiff } from '../common/diffHunk'; -import { GitChangeType, InMemFileChange, SlimFileChange } from '../common/file'; -import { GitHubRef } from '../common/githubRef'; -import Logger from '../common/logger'; -import { Remote } from '../common/remote'; -import { ITelemetry } from '../common/telemetry'; -import { ReviewEvent as CommonReviewEvent, EventType, TimelineEvent } from '../common/timelineEvent'; -import { resolvePath, Schemes, toPRUri, toReviewUri } from '../common/uri'; -import { formatError, isDescendant } from '../common/utils'; -import { InMemFileChangeModel, RemoteFileChangeModel } from '../view/fileChangeModel'; import { OctokitCommon } from './common'; import { ConflictResolutionModel } from './conflictResolutionModel'; import { CredentialStore } from './credentials'; +import { showEmptyCommitWebview } from './emptyCommitWebview'; import { FolderRepositoryManager } from './folderRepositoryManager'; -import { GitHubRepository } from './githubRepository'; +import { GitHubRepository, GraphQLError, GraphQLErrorType } from './githubRepository'; import { AddCommentResponse, AddReactionResponse, @@ -36,8 +25,11 @@ import { EnqueuePullRequestResponse, FileContentResponse, GetReviewRequestsResponse, + MergeMethod as GraphQLMergeMethod, LatestReviewCommitResponse, MarkPullRequestReadyForReviewResponse, + MergePullRequestInput, + MergePullRequestResponse, PendingReviewIdResponse, PullRequestCommentsResponse, PullRequestFilesResponse, @@ -52,7 +44,6 @@ import { } from './graphql'; import { AccountType, - GithubItemStateEnum, IAccount, IGitTreeItem, IRawFileChange, @@ -67,26 +58,42 @@ import { PullRequestMergeability, PullRequestReviewRequirement, ReadyForReview, - ReviewEvent, + ReviewEventEnum, } from './interface'; -import { IssueModel } from './issueModel'; +import { IssueChangeEvent, IssueModel } from './issueModel'; import { compareCommits } from './loggingOctokit'; import { convertRESTPullRequestToRawPullRequest, convertRESTReviewEvent, getReactionGroup, insertNewCommitsSinceReview, + parseAccount, + parseCombinedTimelineEvents, parseGraphQLComment, parseGraphQLReaction, parseGraphQLReviewers, parseGraphQLReviewEvent, parseGraphQLReviewThread, - parseGraphQLTimelineEvents, parseMergeability, parseMergeQueueEntry, parsePullRequestState, + RestAccount, restPaginate, } from './utils'; +import { Repository } from '../api/api'; +import { COPILOT_ACCOUNTS, DiffSide, IComment, IReviewThread, SubjectType, ViewedState } from '../common/comment'; +import { getGitChangeType, getModifiedContentFromDiffHunk, parseDiff } from '../common/diffHunk'; +import { commands } from '../common/executeCommands'; +import { GitChangeType, InMemFileChange, SlimFileChange } from '../common/file'; +import { GitHubRef } from '../common/githubRef'; +import Logger from '../common/logger'; +import { Remote } from '../common/remote'; +import { DEFAULT_MERGE_METHOD, PR_SETTINGS_NAMESPACE } from '../common/settingKeys'; +import { ITelemetry } from '../common/telemetry'; +import { ClosedEvent, EventType, ReviewEvent, TimelineEvent } from '../common/timelineEvent'; +import { resolvePath, Schemes, toGitHubCommitUri, toPRUri, toReviewUri } from '../common/uri'; +import { formatError, isDescendant } from '../common/utils'; +import { InMemFileChangeModel, RemoteFileChangeModel } from '../view/fileChangeModel'; interface IPullRequestModel { head: GitHubRef | null; @@ -111,12 +118,15 @@ export interface FileViewedStateChangeEvent { export type FileViewedState = { [key: string]: ViewedState }; -const BATCH_SIZE = 100; +type TreeDataMode = '100644' | '100755' | '120000'; + +const BATCH_SIZE = 50; export class PullRequestModel extends IssueModel implements IPullRequestModel { static override ID = 'PullRequestModel'; public isDraft?: boolean; + public reviewers?: (IAccount | ITeam)[]; public localBranchName?: string; public mergeBase?: string; public mergeQueueEntry?: MergeQueueEntry; @@ -126,27 +136,25 @@ export class PullRequestModel extends IssueModel implements IPullRe public closingIssues: IssueReference[]; private _showChangesSinceReview: boolean; private _hasPendingReview: boolean = false; - private _onDidChangePendingReviewState: vscode.EventEmitter = new vscode.EventEmitter(); + private _onDidChangePendingReviewState: vscode.EventEmitter = this._register(new vscode.EventEmitter()); public onDidChangePendingReviewState = this._onDidChangePendingReviewState.event; - private _reviewThreadsCache: IReviewThread[] = []; + private _reviewThreadsCache: IReviewThread[] | undefined; private _reviewThreadsCacheInitialized = false; - private _onDidChangeReviewThreads = new vscode.EventEmitter(); + private _onDidChangeReviewThreads = this._register(new vscode.EventEmitter()); public onDidChangeReviewThreads = this._onDidChangeReviewThreads.event; private _fileChangeViewedState: FileViewedState = {}; private _viewedFiles: Set = new Set(); private _unviewedFiles: Set = new Set(); - private _onDidChangeFileViewedState = new vscode.EventEmitter(); + private _onDidChangeFileViewedState = this._register(new vscode.EventEmitter()); public onDidChangeFileViewedState = this._onDidChangeFileViewedState.event; - private _onDidChangeChangesSinceReview = new vscode.EventEmitter(); + private _onDidChangeChangesSinceReview = this._register(new vscode.EventEmitter()); public onDidChangeChangesSinceReview = this._onDidChangeChangesSinceReview.event; private _hasComments: boolean; private _comments: readonly IComment[] | undefined; - private _onDidChangeComments: vscode.EventEmitter = new vscode.EventEmitter(); - public readonly onDidChangeComments: vscode.Event = this._onDidChangeComments.event; // Whether the pull request is currently checked out locally private _isActive: boolean; @@ -157,7 +165,6 @@ export class PullRequestModel extends IssueModel implements IPullRe this._isActive = isActive; } - _telemetry: ITelemetry; constructor( private readonly credentialStore: CredentialStore, @@ -167,9 +174,8 @@ export class PullRequestModel extends IssueModel implements IPullRe item: PullRequest, isActive?: boolean, ) { - super(githubRepository, remote, item, true); + super(telemetry, githubRepository, remote, item, true); - this._telemetry = telemetry; this.isActive = !!isActive; this._showChangesSinceReview = false; @@ -180,7 +186,7 @@ export class PullRequestModel extends IssueModel implements IPullRe public clear() { this.comments = []; this._reviewThreadsCacheInitialized = false; - this._reviewThreadsCache = []; + this._reviewThreadsCache = undefined; } public async initializeReviewThreadCache(): Promise { @@ -189,7 +195,7 @@ export class PullRequestModel extends IssueModel implements IPullRe } public get reviewThreadsCache(): IReviewThread[] { - return this._reviewThreadsCache; + return this._reviewThreadsCache ?? []; } public get reviewThreadsCacheReady(): boolean { @@ -225,7 +231,7 @@ export class PullRequestModel extends IssueModel implements IPullRe set comments(comments: readonly IComment[]) { this._comments = comments; - this._onDidChangeComments.fire(); + this._onDidChange.fire({ comments: true }); } get fileChangeViewedState(): FileViewedState { @@ -237,14 +243,13 @@ export class PullRequestModel extends IssueModel implements IPullRe public isRemoteBaseDeleted?: boolean; public base: GitHubRef; - protected override updateState(state: string) { - const newState = parsePullRequestState(state); - this.state = this.item.merged ? GithubItemStateEnum.Merged : newState; - } + protected override doUpdate(item: PullRequest): IssueChangeEvent { + const changes = super.doUpdate(item) as IssueChangeEvent; + if (this.isDraft !== item.isDraft) { + changes.draft = true; + this.isDraft = item.isDraft; + } - override update(item: PullRequest): void { - super.update(item); - this.isDraft = item.isDraft; this.suggestedReviewers = item.suggestedReviewers; this.closingIssues = item.closingIssues ?? []; if (item.isRemoteHeadDeleted != null) { @@ -266,6 +271,7 @@ export class PullRequestModel extends IssueModel implements IPullRe if (item.hasComments !== undefined) { this._hasComments = item.hasComments; } + return changes; } /** @@ -317,7 +323,7 @@ export class PullRequestModel extends IssueModel implements IPullRe * Approve the pull request. * @param message Optional approval comment text. */ - async approve(repository: Repository, message?: string): Promise { + async approve(repository: Repository, message?: string): Promise { // Check that the remote head of the PR branch matches the local head of the PR branch let remoteHead: string | undefined; let localHead: string | undefined; @@ -325,27 +331,27 @@ export class PullRequestModel extends IssueModel implements IPullRe if (this.isActive) { localHead = repository.state.HEAD?.commit; remoteHead = (await this.githubRepository.getPullRequest(this.number))?.head?.sha; - rejectMessage = vscode.l10n.t('The remote head of the PR branch has changed. Please pull the latest changes from the remote branch before approving.'); + rejectMessage = vscode.l10n.t('The remote head of the pull request branch has changed. Please pull the latest changes from the remote branch before approving.'); } else { localHead = this.head?.sha; remoteHead = (await this.githubRepository.getPullRequest(this.number))?.head?.sha; - rejectMessage = vscode.l10n.t('The remote head of the PR branch has changed. Please refresh the pull request before approving.'); + rejectMessage = vscode.l10n.t('The remote head of the pull request branch has changed. Please refresh the pull request before approving.'); } if (!remoteHead || remoteHead !== localHead) { return Promise.reject(rejectMessage); } - const action: Promise = (await this.getPendingReviewId()) - ? this.submitReview(ReviewEvent.Approve, message) - : this.createReview(ReviewEvent.Approve, message); + const action: Promise = (await this.getPendingReviewId()) + ? this.submitReview(ReviewEventEnum.Approve, message) + : this.createReview(ReviewEventEnum.Approve, message); return action.then(x => { /* __GDPR__ "pr.approve" : {} */ this._telemetry.sendTelemetryEvent('pr.approve'); - this._onDidChangeComments.fire(); + this._onDidChange.fire({ comments: true, timeline: true }); return x; }); } @@ -354,25 +360,117 @@ export class PullRequestModel extends IssueModel implements IPullRe * Request changes on the pull request. * @param message Optional comment text to leave with the review. */ - async requestChanges(message?: string): Promise { - const action: Promise = (await this.getPendingReviewId()) - ? this.submitReview(ReviewEvent.RequestChanges, message) - : this.createReview(ReviewEvent.RequestChanges, message); + async requestChanges(message?: string): Promise { + const action: ReviewEvent = (await this.getPendingReviewId()) + ? await this.submitReview(ReviewEventEnum.RequestChanges, message) + : await this.createReview(ReviewEventEnum.RequestChanges, message); - return action.then(x => { - /* __GDPR__ - "pr.requestChanges" : {} + /* __GDPR__ + "pr.requestChanges" : {} + */ + this._telemetry.sendTelemetryEvent('pr.requestChanges'); + this._onDidChange.fire({ timeline: true, comments: true }); + return action; + } + + async merge( + repository: Repository, + title?: string, + description?: string, + method?: 'merge' | 'squash' | 'rebase', + email?: string + ): Promise<{ merged: boolean, message: string, timeline?: TimelineEvent[] }> { + Logger.debug(`Merging PR: ${this.number} method: ${method} for user: "${email}" - enter`, PullRequestModel.ID); + const { mutate, schema } = await this.githubRepository.ensure(); + + const workingDirectorySHA = repository.state.HEAD?.commit; + const mergingPRSHA = this.head?.sha; + const workingDirectoryIsDirty = repository.state.workingTreeChanges.length > 0; + let expectedHeadOid: string | undefined = this.head?.sha; + + if (this.isActive) { + // We're on the branch of the pr being merged. + expectedHeadOid = workingDirectorySHA; + if (workingDirectorySHA !== mergingPRSHA) { + // We are looking at different commit than what will be merged + const { ahead } = repository.state.HEAD!; + const pluralMessage = vscode.l10n.t('You have {0} unpushed commits on this pull request branch.\n\nWould you like to proceed anyway?', ahead ?? 'unknown'); + const singularMessage = vscode.l10n.t('You have 1 unpushed commit on this pull request branch.\n\nWould you like to proceed anyway?'); + if (ahead && + (await vscode.window.showWarningMessage( + ahead > 1 ? pluralMessage : singularMessage, + { modal: true }, + vscode.l10n.t('Yes'), + )) === undefined) { + + return { + merged: false, + message: vscode.l10n.t('unpushed changes'), + }; + } + } + + if (workingDirectoryIsDirty) { + // We have made changes to the PR that are not committed + if ( + (await vscode.window.showWarningMessage( + vscode.l10n.t('You have uncommitted changes on this pull request branch.\n\n Would you like to proceed anyway?'), + { modal: true }, + vscode.l10n.t('Yes'), + )) === undefined + ) { + return { + merged: false, + message: vscode.l10n.t('uncommitted changes'), + }; + } + } + } + const input: MergePullRequestInput = { + pullRequestId: this.graphNodeId, + commitHeadline: title, + commitBody: description, + expectedHeadOid, + authorEmail: email, + mergeMethod: + (method?.toUpperCase() ?? + vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<'merge' | 'squash' | 'rebase'>(DEFAULT_MERGE_METHOD, 'merge')?.toUpperCase()) as GraphQLMergeMethod, + }; + + return mutate({ + mutation: schema.MergePullRequest, + variables: { + input + } + }) + .then(async (result) => { + Logger.debug(`Merging PR: ${this.number} - done`, PullRequestModel.ID); + + /* __GDPR__ + "pr.merge.success" : {} */ - this._telemetry.sendTelemetryEvent('pr.requestChanges'); - this._onDidChangeComments.fire(); - return x; - }); + this._telemetry.sendTelemetryEvent('pr.merge.success'); + this._onDidChange.fire({ state: true }); + return { merged: true, message: '', timeline: await parseCombinedTimelineEvents(result.data?.mergePullRequest.pullRequest.timelineItems.nodes ?? [], await this.getCopilotTimelineEvents(), this.githubRepository) }; + }) + .catch(e => { + /* __GDPR__ + "pr.merge.failure" : {} + */ + this._telemetry.sendTelemetryErrorEvent('pr.merge.failure'); + const graphQLErrors = e.graphQLErrors as GraphQLError[] | undefined; + if (graphQLErrors?.length && graphQLErrors.find(error => error.type === GraphQLErrorType.Unprocessable && error.message?.includes('Head branch was modified'))) { + return { merged: false, message: vscode.l10n.t('Head branch was modified. Pull, review, then try again.') }; + } else { + throw e; + } + }); } /** * Close the pull request. */ - async close(): Promise { + override async close(): Promise<{ item: PullRequest; closedEvent: ClosedEvent }> { const { octokit, remote } = await this.githubRepository.ensure(); const ret = await octokit.call(octokit.api.pulls.update, { owner: remote.owner, @@ -385,8 +483,25 @@ export class PullRequestModel extends IssueModel implements IPullRe "pr.close" : {} */ this._telemetry.sendTelemetryEvent('pr.close'); + const user = await this.githubRepository.getAuthenticatedUser(); + this.state = parsePullRequestState(ret.data.state); - return convertRESTPullRequestToRawPullRequest(ret.data, this.githubRepository); + // Fire the event with a delay as GitHub needs some time to propagate the changes, we want to make sure any listeners of the event will get the right info when they query + setTimeout(() => this._onDidChange.fire({ state: true }), 1500); + + return { + item: convertRESTPullRequestToRawPullRequest(ret.data, this.githubRepository), + closedEvent: { + createdAt: ret.data.closed_at ?? '', + event: EventType.Closed, + id: `${ret.data.id}`, + actor: { + login: user.login, + avatarUrl: user.avatarUrl, + url: user.url + } + } + }; } /** @@ -394,7 +509,7 @@ export class PullRequestModel extends IssueModel implements IPullRe * @param event The type of review to create, an approval, request for changes, or comment. * @param message The summary comment text. */ - private async createReview(event: ReviewEvent, message?: string): Promise { + private async createReview(event: ReviewEventEnum, message?: string): Promise { const { octokit, remote } = await this.githubRepository.ensure(); const { data } = await octokit.call(octokit.api.pulls.createReview, { @@ -405,7 +520,8 @@ export class PullRequestModel extends IssueModel implements IPullRe body: message, }); - return convertRESTReviewEvent(data, this.githubRepository); + this._onDidChange.fire({ timeline: true }); + return convertRESTReviewEvent(data as OctokitCommon.PullsCreateReviewResponseData, this.githubRepository); } /** @@ -413,11 +529,11 @@ export class PullRequestModel extends IssueModel implements IPullRe * @param event The type of review to create, an approval, request for changes, or comment. * @param body The summary comment text. */ - async submitReview(event?: ReviewEvent, body?: string): Promise { + async submitReview(event?: ReviewEventEnum, body?: string): Promise { let pendingReviewId = await this.getPendingReviewId(); const { mutate, schema } = await this.githubRepository.ensure(); - if (!pendingReviewId && (event === ReviewEvent.Comment)) { + if (!pendingReviewId && (event === ReviewEventEnum.Comment)) { // Create a new review so that we can comment on it. pendingReviewId = await this.startReview(); } @@ -427,7 +543,7 @@ export class PullRequestModel extends IssueModel implements IPullRe mutation: schema.SubmitReview, variables: { id: pendingReviewId, - event: event || ReviewEvent.Comment, + event: event || ReviewEventEnum.Comment, body, }, }); @@ -436,7 +552,7 @@ export class PullRequestModel extends IssueModel implements IPullRe await this.updateDraftModeContext(); const reviewEvent = parseGraphQLReviewEvent(data!.submitPullRequestReview.pullRequestReview, this.githubRepository); - const threadWithComment = this._reviewThreadsCache.find(thread => + const threadWithComment = (this._reviewThreadsCache ?? []).find(thread => thread.comments.length ? (thread.comments[0].pullRequestReviewId === reviewEvent.id) : undefined, ); if (threadWithComment) { @@ -444,6 +560,7 @@ export class PullRequestModel extends IssueModel implements IPullRe threadWithComment.viewerCanResolve = true; this._onDidChangeReviewThreads.fire({ added: [], changed: [threadWithComment], removed: [] }); } + this._onDidChange.fire({ timeline: true, comments: true }); return reviewEvent; } else { throw new Error(`Submitting review failed, no pending review for current pull request: ${this.number}.`); @@ -455,7 +572,7 @@ export class PullRequestModel extends IssueModel implements IPullRe */ async getPendingReviewId(): Promise { const { query, schema } = await this.githubRepository.ensure(); - const currentUser = await this.githubRepository.getAuthenticatedUser(); + const currentUser = (await this.githubRepository.getAuthenticatedUser()).login; try { const { data } = await query({ query: schema.GetPendingReviewId, @@ -502,6 +619,10 @@ export class PullRequestModel extends IssueModel implements IPullRe */ async deleteReview(): Promise<{ deletedReviewId: number; deletedReviewComments: IComment[] }> { const pendingReviewId = await this.getPendingReviewId(); + if (!pendingReviewId) { + throw new Error(`No pending review found for pull request #${this.number}.`); + } + const { mutate, schema } = await this.githubRepository.ensure(); const { data } = await mutate({ mutation: schema.DeleteReview, @@ -511,15 +632,44 @@ export class PullRequestModel extends IssueModel implements IPullRe }); const { comments, databaseId } = data!.deletePullRequestReview.pullRequestReview; + const deletedReviewComments = comments.nodes.map(comment => parseGraphQLComment(comment, false, this.githubRepository)); + + // Update local state: remove all draft comments (and their threads if emptied) that belonged to the deleted review + const deletedCommentIds = new Set(deletedReviewComments.map(c => c.id)); + const changedThreads: IReviewThread[] = []; + const removedThreads: IReviewThread[] = []; + if (!this._reviewThreadsCache) { + this._reviewThreadsCache = []; + } + for (let i = this._reviewThreadsCache.length - 1; i >= 0; i--) { + const thread = this._reviewThreadsCache[i]; + const originalLength = thread.comments.length; + thread.comments = thread.comments.filter(c => !deletedCommentIds.has(c.id)); + if (thread.comments.length === 0 && originalLength > 0) { + // Entire thread was composed only of comments from the deleted review; remove it. + this._reviewThreadsCache.splice(i, 1); + removedThreads.push(thread); + } else if (thread.comments.length !== originalLength) { + changedThreads.push(thread); + } + } + if (changedThreads.length > 0 || removedThreads.length > 0) { + this._onDidChangeReviewThreads.fire({ added: [], changed: changedThreads, removed: removedThreads }); + } + + // Remove from flat comments collection + if (this._comments) { + this.comments = this._comments.filter(c => !deletedCommentIds.has(c.id)); + } this.hasPendingReview = false; await this.updateDraftModeContext(); - this.getReviewThreads(); - + // Fire change event to update timeline & comment views + this._onDidChange.fire({ timeline: true, comments: true }); return { deletedReviewId: databaseId, - deletedReviewComments: comments.nodes.map(comment => parseGraphQLComment(comment, false, this.githubRepository)), + deletedReviewComments, }; } @@ -545,7 +695,6 @@ export class PullRequestModel extends IssueModel implements IPullRe throw new Error('Failed to start review'); } this.hasPendingReview = true; - this._onDidChangeComments.fire(); return data.addPullRequestReview.pullRequestReview.id; } @@ -607,8 +756,12 @@ export class PullRequestModel extends IssueModel implements IPullRe const thread = data.addPullRequestReviewThread.thread; const newThread = parseGraphQLReviewThread(thread, this.githubRepository); + if (!this._reviewThreadsCache) { + this._reviewThreadsCache = []; + } this._reviewThreadsCache.push(newThread); this._onDidChangeReviewThreads.fire({ added: [newThread], changed: [], removed: [] }); + this._onDidChange.fire({ timeline: true }); return newThread; } @@ -660,7 +813,7 @@ export class PullRequestModel extends IssueModel implements IPullRe newComment.isDraft = false; } - const threadWithComment = this._reviewThreadsCache.find(thread => + const threadWithComment = this._reviewThreadsCache?.find(thread => thread.comments.some(comment => comment.graphNodeId === inReplyTo), ); if (threadWithComment) { @@ -668,6 +821,7 @@ export class PullRequestModel extends IssueModel implements IPullRe this._onDidChangeReviewThreads.fire({ added: [], changed: [threadWithComment], removed: [] }); } + this._onDidChange.fire({ timeline: true, comments: true }); return newComment; } @@ -691,6 +845,113 @@ export class PullRequestModel extends IssueModel implements IPullRe } } + /** + * Get the timeline events of a pull request, including comments, reviews, commits, merges, deletes, and assigns. + */ + async getTimelineEvents(): Promise { + const getTimelineEvents = async () => { + Logger.debug(`Fetch timeline events of PR #${this.number} - enter`, PullRequestModel.ID); + const { query, remote, schema } = await this.githubRepository.ensure(); + try { + const { data } = await query({ + query: schema.TimelineEvents, + variables: { + owner: remote.owner, + name: remote.repositoryName, + number: this.number, + }, + }); + + if (data.repository === null) { + Logger.error('Unexpected null repository when fetching timeline', PullRequestModel.ID); + } + return data; + } catch (e) { + Logger.error(`Failed to get pull request timeline events: ${e}`, PullRequestModel.ID); + console.log(e); + return undefined; + } + }; + + const [data, latestReviewCommitInfo, currentUser, reviewThreads] = await Promise.all([ + getTimelineEvents(), + this.getViewerLatestReviewCommit(), + // eslint-disable-next-line @typescript-eslint/await-thenable + (await this.githubRepository.getAuthenticatedUser()).login, + this.getReviewThreads() + ]); + + + const ret = data?.repository?.pullRequest.timelineItems.nodes ?? []; + const events = await parseCombinedTimelineEvents(ret, await this.getCopilotTimelineEvents(true), this.githubRepository); + + this.addReviewTimelineEventComments(events, reviewThreads); + insertNewCommitsSinceReview(events, latestReviewCommitInfo?.sha, currentUser, this.head); + Logger.debug(`Fetch timeline events of PR #${this.number} - done`, PullRequestModel.ID); + this.timelineEvents = events; + return events; + } + + private addReviewTimelineEventComments(events: TimelineEvent[], reviewThreads: IReviewThread[]): void { + interface CommentNode extends IComment { + childComments?: CommentNode[]; + } + + const reviewEvents = events.filter((e): e is ReviewEvent => e.event === EventType.Reviewed); + const reviewComments = reviewThreads.reduce((previous, current) => (previous as IComment[]).concat(current.comments), []); + + const reviewEventsById = reviewEvents.reduce((index, evt) => { + index[evt.id] = evt; + evt.comments = []; + return index; + }, {} as { [key: number]: ReviewEvent }); + + const commentsById = reviewComments.reduce((index, evt) => { + index[evt.id] = evt; + return index; + }, {} as { [key: number]: CommentNode }); + + const roots: CommentNode[] = []; + let i = reviewComments.length; + while (i-- > 0) { + const c: CommentNode = reviewComments[i]; + if (!c.inReplyToId) { + roots.unshift(c); + continue; + } + const parent = commentsById[c.inReplyToId]; + parent.childComments = parent.childComments || []; + parent.childComments = [c, ...(c.childComments || []), ...parent.childComments]; + } + + roots.forEach(c => { + const review = reviewEventsById[c.pullRequestReviewId!]; + if (review) { + review.comments = review.comments.concat(c).concat(c.childComments || []); + } + }); + + reviewThreads.forEach(thread => { + if (!thread.prReviewDatabaseId || !reviewEventsById[thread.prReviewDatabaseId]) { + return; + } + const prReviewThreadEvent = reviewEventsById[thread.prReviewDatabaseId]; + prReviewThreadEvent.reviewThread = { + threadId: thread.id, + canResolve: thread.viewerCanResolve, + canUnresolve: thread.viewerCanUnresolve, + isResolved: thread.isResolved + }; + + }); + + const pendingReview = reviewEvents.filter(r => r.state?.toLowerCase() === 'pending')[0]; + if (pendingReview) { + // Ensures that pending comments made in reply to other reviews are included for the pending review + pendingReview.comments = reviewComments.filter(c => c.isDraft); + } + } + /** * Edit an existing review comment. * @param comment The comment to edit @@ -698,7 +959,7 @@ export class PullRequestModel extends IssueModel implements IPullRe */ async editReviewComment(comment: IComment, text: string): Promise { const { mutate, schema } = await this.githubRepository.ensure(); - let threadWithComment = this._reviewThreadsCache.find(thread => + let threadWithComment = this._reviewThreadsCache?.find(thread => thread.comments.some(c => c.graphNodeId === comment.graphNodeId), ); @@ -729,6 +990,7 @@ export class PullRequestModel extends IssueModel implements IPullRe const index = threadWithComment.comments.findIndex(c => c.graphNodeId === comment.graphNodeId); threadWithComment.comments.splice(index, 1, newComment); this._onDidChangeReviewThreads.fire({ added: [], changed: [threadWithComment], removed: [] }); + this._onDidChange.fire({ timeline: true }); } return newComment; @@ -742,7 +1004,7 @@ export class PullRequestModel extends IssueModel implements IPullRe try { const { octokit, remote } = await this.githubRepository.ensure(); const id = Number(commentId); - const threadIndex = this._reviewThreadsCache.findIndex(thread => thread.comments.some(c => c.id === id)); + const threadIndex = this._reviewThreadsCache?.findIndex(thread => thread.comments.some(c => c.id === id)) ?? -1; if (threadIndex === -1) { this.deleteIssueComment(commentId); @@ -754,15 +1016,16 @@ export class PullRequestModel extends IssueModel implements IPullRe }); if (threadIndex > -1) { - const threadWithComment = this._reviewThreadsCache[threadIndex]; + const threadWithComment = this._reviewThreadsCache![threadIndex]; const index = threadWithComment.comments.findIndex(c => c.id === id); threadWithComment.comments.splice(index, 1); if (threadWithComment.comments.length === 0) { - this._reviewThreadsCache.splice(threadIndex, 1); + this._reviewThreadsCache?.splice(threadIndex, 1); this._onDidChangeReviewThreads.fire({ added: [], changed: [], removed: [threadWithComment] }); } else { this._onDidChangeReviewThreads.fire({ added: [], changed: [threadWithComment], removed: [] }); } + this._onDidChange.fire({ timeline: true }); } } } catch (e) { @@ -822,11 +1085,11 @@ export class PullRequestModel extends IssueModel implements IPullRe } const baseTreeData = baseTree.data.tree.find(f => f.path === file.filename); - const baseMode: '100644' | '100755' | '120000' = baseTreeData?.mode as any ?? '100644'; + const baseMode: TreeDataMode = (baseTreeData?.mode as TreeDataMode | undefined) ?? '100644'; const headTree = await octokit.call(octokit.api.git.getTree, { owner: model.prHeadOwner, repo: model.repositoryName, tree_sha: headTreeSha, recursive: 'true' }); const headTreeData = headTree.data.tree.find(f => f.path === file.filename); - const headMode: '100644' | '100755' | '120000' = headTreeData?.mode as any ?? '100644'; + const headMode: TreeDataMode = (headTreeData?.mode as TreeDataMode | undefined) ?? '100644'; if (file.status === 'removed') { // The file was removed so we use a null sha to indicate that (per GitHub's API). @@ -952,6 +1215,10 @@ export class PullRequestModel extends IssueModel implements IPullRe } const reviewers: (IAccount | ITeam)[] = parseGraphQLReviewers(data, githubRepository); + if (this.reviewers?.length !== reviewers.length || (this.reviewers.some(r => !reviewers.some(rr => rr.id === r.id)))) { + this.reviewers = reviewers; + this._onDidChange.fire({ reviewers: true }); + } Logger.debug('Get Review Requests - done', PullRequestModel.ID); return reviewers; } @@ -962,17 +1229,33 @@ export class PullRequestModel extends IssueModel implements IPullRe */ async requestReview(reviewers: IAccount[], teamReviewers: ITeam[], union: boolean = false): Promise { const { mutate, schema } = await this.githubRepository.ensure(); - await mutate({ + const input: { pullRequestId: string, teamIds: string[], userIds: string[], botIds?: string[], union: boolean } = { + pullRequestId: this.graphNodeId, + teamIds: teamReviewers.map(t => t.id), + userIds: reviewers.filter(r => r.accountType !== AccountType.Bot).map(r => r.id), + union + }; + if (!this.githubRepository.areQueriesLimited) { + input.botIds = reviewers.filter(r => r.accountType === AccountType.Bot).map(r => r.id); + } + + const { data } = await mutate({ mutation: schema.AddReviewers, variables: { - input: { - pullRequestId: this.graphNodeId, - teamIds: teamReviewers.map(t => t.id), - userIds: reviewers.filter(r => r.accountType !== AccountType.Bot).map(r => r.id), - union - }, + input }, }); + + if (!data?.repository) { + Logger.error('Unexpected null repository while getting review requests', PullRequestModel.ID); + return; + } + + const newReviewers: (IAccount | ITeam)[] = parseGraphQLReviewers(data, this.githubRepository); + if (this.reviewers?.length !== newReviewers.length || (this.reviewers.some(r => !newReviewers.some(rr => rr.id === r.id)))) { + this.reviewers = newReviewers; + this._onDidChange.fire({ reviewers: true }); + } } /** @@ -980,14 +1263,20 @@ 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, repo: remote.repositoryName, pull_number: this.number, - reviewers: reviewers.filter(r => r.accountType !== AccountType.Bot).map(r => r.id), - team_reviewers: teamReviewers.map(t => t.id) + reviewers: reviewers.map(r => r.id), + team_reviewers: teamReviewers.map(t => t.id), }); + + this.reviewers = this.reviewers?.filter(r => !reviewers.some(rr => rr.id === r.id) && !teamReviewers.some(t => t.id === r.id)) || []; + this._onDidChange.fire({ reviewers: true }); } private diffThreads(oldReviewThreads: IReviewThread[], newReviewThreads: IReviewThread[]): void { @@ -1012,11 +1301,13 @@ export class PullRequestModel extends IssueModel implements IPullRe } }); - this._onDidChangeReviewThreads.fire({ - added, - changed, - removed, - }); + if (added.length > 0 || changed.length > 0 || removed.length > 0) { + this._onDidChangeReviewThreads.fire({ + added, + changed, + removed, + }); + } } async initializeReviewThreadCacheAndReviewComments(): Promise { @@ -1034,13 +1325,15 @@ export class PullRequestModel extends IssueModel implements IPullRe private setReviewThreadCacheFromRaw(raw: ReviewThread[]): IReviewThread[] { const reviewThreads: IReviewThread[] = raw.map(thread => parseGraphQLReviewThread(thread, this.githubRepository)); - const oldReviewThreads = this._reviewThreadsCache; + const oldReviewThreads = this._reviewThreadsCache ?? []; this._reviewThreadsCache = reviewThreads; this.diffThreads(oldReviewThreads, reviewThreads); return reviewThreads; } private async getRawReviewComments(): Promise { + Logger.debug(`Fetching review comments for PR #${this.number} - enter`, PullRequestModel.ID); + const { remote, query, schema } = await this.githubRepository.ensure(); let after: string | null = null; let hasNextPage = false; @@ -1062,6 +1355,7 @@ export class PullRequestModel extends IssueModel implements IPullRe hasNextPage = data.repository.pullRequest.reviewThreads.pageInfo.hasNextPage; after = data.repository.pullRequest.reviewThreads.pageInfo.endCursor; } while (hasNextPage && reviewThreads.length < 1000); + Logger.debug(`Fetching review comments for PR #${this.number} - exit`, PullRequestModel.ID); return reviewThreads; } catch (e) { @@ -1104,20 +1398,14 @@ export class PullRequestModel extends IssueModel implements IPullRe commit: OctokitCommon.PullsListCommitsResponseData[0], ): Promise { try { - Logger.debug( - `Fetch file changes of commit ${commit.sha} in PR #${this.number} - enter`, - PullRequestModel.ID, - ); + Logger.debug(`Fetch file changes of commit ${commit.sha} in PR #${this.number} - enter`, PullRequestModel.ID,); const { octokit, remote } = await this.githubRepository.ensure(); const fullCommit = await octokit.call(octokit.api.repos.getCommit, { owner: remote.owner, repo: remote.repositoryName, ref: commit.sha, }); - Logger.debug( - `Fetch file changes of commit ${commit.sha} in PR #${this.number} - done`, - PullRequestModel.ID, - ); + Logger.debug(`Fetch file changes of commit ${commit.sha} in PR #${this.number} - done`, PullRequestModel.ID,); return fullCommit.data.files ?? []; } catch (e) { @@ -1126,104 +1414,24 @@ export class PullRequestModel extends IssueModel implements IPullRe } } - /** - * Get the timeline events of a pull request, including comments, reviews, commits, merges, deletes, and assigns. - */ - async getTimelineEvents(): Promise { - Logger.debug(`Fetch timeline events of PR #${this.number} - enter`, PullRequestModel.ID); - const { query, remote, schema } = await this.githubRepository.ensure(); - - try { - const [{ data }, latestReviewCommitInfo, currentUser, reviewThreads] = await Promise.all([ - query({ - query: schema.TimelineEvents, - variables: { - owner: remote.owner, - name: remote.repositoryName, - number: this.number, - }, - }), - this.getViewerLatestReviewCommit(), - this.githubRepository.getAuthenticatedUser(), - this.getReviewThreads() - ]); - - if (data.repository === null) { - Logger.error('Unexpected null repository when fetching timeline', PullRequestModel.ID); - } - - const ret = data.repository?.pullRequest.timelineItems.nodes; - const events = ret ? parseGraphQLTimelineEvents(ret, this.githubRepository) : []; - - this.addReviewTimelineEventComments(events, reviewThreads); - insertNewCommitsSinceReview(events, latestReviewCommitInfo?.sha, currentUser, this.head); - Logger.debug(`Fetch timeline events of PR #${this.number} - done`, PullRequestModel.ID); - return events; - } catch (e) { - Logger.error(`Failed to get pull request timeline events: ${e}`, PullRequestModel.ID); - console.log(e); + async getCoAuthors(): Promise { + // To save time, we only do for Copilot now as that's where we need it + if (!COPILOT_ACCOUNTS[this.item.user.login]) { return []; } - } - - private addReviewTimelineEventComments(events: TimelineEvent[], reviewThreads: IReviewThread[]): void { - interface CommentNode extends IComment { - childComments?: CommentNode[]; - } - - const reviewEvents = events.filter((e): e is CommonReviewEvent => e.event === EventType.Reviewed); - const reviewComments = reviewThreads.reduce((previous, current) => (previous as IComment[]).concat(current.comments), []); - - const reviewEventsById = reviewEvents.reduce((index, evt) => { - index[evt.id] = evt; - evt.comments = []; - return index; - }, {} as { [key: number]: CommonReviewEvent }); - - const commentsById = reviewComments.reduce((index, evt) => { - index[evt.id] = evt; - return index; - }, {} as { [key: number]: CommentNode }); - - const roots: CommentNode[] = []; - let i = reviewComments.length; - while (i-- > 0) { - const c: CommentNode = reviewComments[i]; - if (!c.inReplyToId) { - roots.unshift(c); - continue; - } - const parent = commentsById[c.inReplyToId]; - parent.childComments = parent.childComments || []; - parent.childComments = [c, ...(c.childComments || []), ...parent.childComments]; - } - - roots.forEach(c => { - const review = reviewEventsById[c.pullRequestReviewId!]; - if (review) { - review.comments = review.comments.concat(c).concat(c.childComments || []); - } - }); - - reviewThreads.forEach(thread => { - if (!thread.prReviewDatabaseId || !reviewEventsById[thread.prReviewDatabaseId]) { - return; - } - const prReviewThreadEvent = reviewEventsById[thread.prReviewDatabaseId]; - prReviewThreadEvent.reviewThread = { - threadId: thread.id, - canResolve: thread.viewerCanResolve, - canUnresolve: thread.viewerCanUnresolve, - isResolved: thread.isResolved - }; - + const { octokit, remote } = await this.githubRepository.ensure(); + const timeline = await octokit.call(octokit.api.issues.listEventsForTimeline, { + issue_number: this.number, + owner: remote.owner, + repo: remote.repositoryName, + per_page: 100 }); + const workStartedInitiator = (timeline.data.find(event => event.event === 'copilot_work_started') as { actor: RestAccount } | undefined)?.actor; + return workStartedInitiator ? [parseAccount(workStartedInitiator, this.githubRepository)] : []; + } - const pendingReview = reviewEvents.filter(r => r.state?.toLowerCase() === 'pending')[0]; - if (pendingReview) { - // Ensures that pending comments made in reply to other reviews are included for the pending review - pendingReview.comments = reviewComments.filter(c => c.isDraft); - } + protected override getUpdatesQuery(schema: any): any { + return schema.LatestUpdates; } /** @@ -1233,7 +1441,7 @@ export class PullRequestModel extends IssueModel implements IPullRe return this.githubRepository.getStatusChecks(this.number); } - static async openChanges(folderManager: FolderRepositoryManager, pullRequestModel: PullRequestModel) { + static async openChanges(folderManager: FolderRepositoryManager, pullRequestModel: PullRequestModel, openToTheSide?: boolean): Promise { const changeModels = await PullRequestModel.getChangeModels(folderManager, pullRequestModel); const args: [vscode.Uri, vscode.Uri | undefined, vscode.Uri | undefined][] = []; for (const changeModel of changeModels) { @@ -1244,9 +1452,62 @@ export class PullRequestModel extends IssueModel implements IPullRe "pr.openChanges" : {} */ folderManager.telemetry.sendTelemetryEvent('pr.openChanges'); + + if (openToTheSide) { + if (vscode.window.tabGroups.all.length < 2) { + await vscode.commands.executeCommand('workbench.action.splitEditor'); + } + + await vscode.commands.executeCommand('workbench.action.focusSecondEditorGroup'); + } + return vscode.commands.executeCommand('vscode.changes', vscode.l10n.t('Changes in Pull Request #{0}', pullRequestModel.number), args); } + static async openCommitChanges(extensionUri: vscode.Uri, githubRepository: GitHubRepository, commitSha: string) { + try { + const parentCommit = await githubRepository.getCommitParent(commitSha); + if (!parentCommit) { + vscode.window.showErrorMessage(vscode.l10n.t('Commit {0} has no parent', commitSha.substring(0, 7))); + return; + } + + const changes = await githubRepository.compareCommits(parentCommit, commitSha); + if (!changes?.files || changes.files.length === 0) { + // Show a webview with the empty commit message instead of a notification + showEmptyCommitWebview(extensionUri, commitSha); + return; + } + + // Create URI pairs for the multi diff editor using review scheme + const args: [vscode.Uri, vscode.Uri | undefined, vscode.Uri | undefined][] = []; + for (const change of changes.files) { + const rightUri = toGitHubCommitUri(change.filename, { commit: commitSha, owner: githubRepository.remote.owner, repo: githubRepository.remote.repositoryName }); + const leftUri = toGitHubCommitUri(change.previous_filename ?? change.filename, { commit: parentCommit, owner: githubRepository.remote.owner, repo: githubRepository.remote.repositoryName }); + const changeType = getGitChangeType(change.status); + if (changeType === GitChangeType.ADD) { + // For added files, show against empty + args.push([rightUri, undefined, rightUri]); + } else if (changeType === GitChangeType.DELETE) { + // For deleted files, show old version against empty + args.push([rightUri, leftUri, undefined]); + } else { + args.push([rightUri, leftUri, rightUri]); + } + } + + /* __GDPR__ + "pr.openCommitChanges" : {} + */ + githubRepository.telemetry.sendTelemetryEvent('pr.openCommitChanges'); + + return commands.executeCommand('vscode.changes', vscode.l10n.t('Changes in Commit {0}', commitSha.substring(0, 7)), args); + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + vscode.window.showErrorMessage(vscode.l10n.t('Failed to open commit changes: {0}', errorMessage)); + } + } + static async openDiffFromComment( folderManager: FolderRepositoryManager, pullRequestModel: PullRequestModel, @@ -1351,6 +1612,11 @@ export class PullRequestModel extends IssueModel implements IPullRe return this._fileChanges; } + private _rawFileChangesCache: IRawFileChange[] | undefined; + get rawFileChanges(): IRawFileChange[] | undefined { + return this._rawFileChangesCache; + } + async getFileChangesInfo() { this._fileChanges.clear(); const data = await this.getRawFileChangesInfo(); @@ -1362,6 +1628,26 @@ export class PullRequestModel extends IssueModel implements IPullRe return parsed; } + async getPatch(): Promise { + const githubRepository = this.githubRepository; + const { octokit, remote } = await githubRepository.ensure(); + + const { data } = await octokit.call(octokit.api.pulls.get, { + owner: remote.owner, + repo: remote.repositoryName, + pull_number: this.number, + mediaType: { + format: 'diff' + } + }); + + if (typeof data === 'string') { + return data; + } else { + throw new Error('Expected diff data to be a string'); + } + } + public static async getChangeModels(folderManager: FolderRepositoryManager, pullRequestModel: PullRequestModel): Promise<(RemoteFileChangeModel | InMemFileChangeModel)[]> { const isCurrentPR = folderManager.activePullRequest?.number === pullRequestModel.number; const changes = pullRequestModel.fileChanges.size > 0 ? pullRequestModel.fileChanges.values() : await pullRequestModel.getFileChangesInfo(); @@ -1381,7 +1667,7 @@ export class PullRequestModel extends IssueModel implements IPullRe /** * List the changed files in a pull request. */ - private async getRawFileChangesInfo(): Promise { + public async getRawFileChangesInfo(): Promise { Logger.debug(`Fetch file changes, base, head and merge base of PR #${this.number} - enter`, PullRequestModel.ID); const githubRepository = this.githubRepository; @@ -1389,6 +1675,7 @@ export class PullRequestModel extends IssueModel implements IPullRe if (!this.base) { Logger.appendLine('No base branch found for PR, fetching it now', PullRequestModel.ID); + Logger.trace(`Fetching from ${remote.owner}/${remote.repositoryName}. PR #${this.number}`, PullRequestModel.ID); const info = await octokit.call(octokit.api.pulls.get, { owner: remote.owner, repo: remote.repositoryName, @@ -1416,7 +1703,7 @@ export class PullRequestModel extends IssueModel implements IPullRe // Use the original base to compare against for merged PRs this.mergeBase = this.base.sha; - + this._rawFileChangesCache = response; return response; } @@ -1428,10 +1715,8 @@ export class PullRequestModel extends IssueModel implements IPullRe this._onDidChangeChangesSinceReview.fire(); } - Logger.debug( - `Fetch file changes and merge base of PR #${this.number} - done, total files ${files.length} `, - PullRequestModel.ID, - ); + Logger.debug(`Fetch file changes and merge base of PR #${this.number} - done, total files ${files.length} `, PullRequestModel.ID,); + this._rawFileChangesCache = files; return files; } @@ -1468,7 +1753,7 @@ export class PullRequestModel extends IssueModel implements IPullRe const { query, remote, schema } = await this.githubRepository.ensure(); // hard code the users for selfhost purposes - const { data } = (schema.PullRequestMergeabilityMergeRequirements && ((await this.credentialStore.getCurrentUser(this.remote.authProviderId))?.login === 'alexr00')) ? await query({ + const { data } = /*(schema.PullRequestMergeabilityMergeRequirements && ((await this.credentialStore.getCurrentUser(this.remote.authProviderId))?.login === 'alexr00')) ? await query({ query: schema.PullRequestMergeabilityMergeRequirements, variables: { owner: remote.owner, @@ -1480,7 +1765,7 @@ export class PullRequestModel extends IssueModel implements IPullRe 'GraphQL-Features': 'pull_request_merge_requirements_api' // This flag allows specific users to test a private field. } } - }) : await query({ + }) : */await query({ query: schema.PullRequestMergeability, variables: { owner: remote.owner, @@ -1533,6 +1818,7 @@ export class PullRequestModel extends IssueModel implements IPullRe this.item.isDraft = result.isDraft; this.item.mergeable = result.mergeable; this.item.allowAutoMerge = result.allowAutoMerge; + this._onDidChange.fire({ draft: true }); return result; } catch (e) { /* __GDPR__ @@ -1544,7 +1830,7 @@ export class PullRequestModel extends IssueModel implements IPullRe } private updateCommentReactions(graphNodeId: string, reactionGroups: ReactionGroup[]) { - const reviewThread = this._reviewThreadsCache.find(thread => + const reviewThread = this._reviewThreadsCache?.find(thread => thread.comments.some(c => c.graphNodeId === graphNodeId), ); if (reviewThread) { @@ -1621,7 +1907,7 @@ export class PullRequestModel extends IssueModel implements IPullRe } async resolveReviewThread(threadId: string): Promise { - const oldThread = this._reviewThreadsCache.find(thread => thread.id === threadId); + const oldThread = this._reviewThreadsCache?.find(thread => thread.id === threadId); try { Logger.debug(`Resolve review thread - enter`, PullRequestModel.ID); @@ -1650,10 +1936,10 @@ export class PullRequestModel extends IssueModel implements IPullRe throw new Error('Resolve review thread failed.'); } - const index = this._reviewThreadsCache.findIndex(thread => thread.id === threadId); + const index = this._reviewThreadsCache?.findIndex(thread => thread.id === threadId) ?? -1; if (index > -1) { const thread = parseGraphQLReviewThread(data.resolveReviewThread.thread, this.githubRepository); - this._reviewThreadsCache.splice(index, 1, thread); + this._reviewThreadsCache?.splice(index, 1, thread); this._onDidChangeReviewThreads.fire({ added: [], changed: [thread], removed: [] }); } Logger.debug(`Resolve review thread - done`, PullRequestModel.ID); @@ -1664,7 +1950,7 @@ export class PullRequestModel extends IssueModel implements IPullRe } async unresolveReviewThread(threadId: string): Promise { - const oldThread = this._reviewThreadsCache.find(thread => thread.id === threadId); + const oldThread = this._reviewThreadsCache?.find(thread => thread.id === threadId); try { Logger.debug(`Unresolve review thread - enter`, PullRequestModel.ID); @@ -1693,10 +1979,10 @@ export class PullRequestModel extends IssueModel implements IPullRe throw new Error('Unresolve review thread failed.'); } - const index = this._reviewThreadsCache.findIndex(thread => thread.id === threadId); + const index = this._reviewThreadsCache?.findIndex(thread => thread.id === threadId) ?? -1; if (index > -1) { const thread = parseGraphQLReviewThread(data.unresolveReviewThread.thread, this.githubRepository); - this._reviewThreadsCache.splice(index, 1, thread); + this._reviewThreadsCache?.splice(index, 1, thread); this._onDidChangeReviewThreads.fire({ added: [], changed: [thread], removed: [] }); } Logger.debug(`Unresolve review thread - done`, PullRequestModel.ID); @@ -1866,7 +2152,7 @@ export class PullRequestModel extends IssueModel implements IPullRe async markFiles(filePathOrSubpaths: string[], event: boolean, state: 'viewed' | 'unviewed'): Promise { const allFilenames = filePathOrSubpaths .map((f) => - isDescendant(this.githubRepository.rootUri.path, f, '/') + isDescendant(this.githubRepository.rootUri.path, f, false, '/') ? f.substring(this.githubRepository.rootUri.path.length + 1) : f ); @@ -1883,18 +2169,22 @@ export class PullRequestModel extends IssueModel implements IPullRe // We only ever send 100 mutations at once. Any more than this and // we risk a timeout from GitHub. for (let i = 0; i < allFilenames.length; i += BATCH_SIZE) { - const batch = allFilenames.slice(i, i + BATCH_SIZE); - // See below for an example of what a mutation produced by this - // will look like - const mutation = gql`mutation Batch${mutationName}{ - ${batch.map((filename, i) => - `alias${i}: ${mutationName}( - input: {path: "${filename}", pullRequestId: "${pullRequestId}"} - ) { clientMutationId } - ` - )} - }`; - await mutate({ mutation }); + try { + const batch = allFilenames.slice(i, i + BATCH_SIZE); + // See below for an example of what a mutation produced by this + // will look like + const mutation = gql`mutation Batch${mutationName}{ + ${batch.map((filename, i) => + `alias${i}: ${mutationName}( + input: {path: "${filename}", pullRequestId: "${pullRequestId}"} + ) { clientMutationId } + ` + )} + }`; + await mutate({ mutation }); + } catch (e) { + Logger.error(`Error marking files as ${state}: ${e}`, PullRequestModel.ID); + } } // mutation BatchUnmarkFileAsViewedInline { @@ -1951,3 +2241,18 @@ export class PullRequestModel extends IssueModel implements IPullRe }; } } + +export async function isCopilotOnMyBehalf(pullRequestModel: PullRequestModel, currentUser: IAccount, coAuthors?: IAccount[]): Promise { + if (!COPILOT_ACCOUNTS[pullRequestModel.author.login]) { + return false; + } + + if (!coAuthors) { + coAuthors = await pullRequestModel.getCoAuthors(); + } + if (!coAuthors || coAuthors.length === 0) { + return false; + } + + return coAuthors.some(c => c.login === currentUser.login); +} diff --git a/src/github/pullRequestOverview.ts b/src/github/pullRequestOverview.ts index 961fbb6f75..8a7afa68b7 100644 --- a/src/github/pullRequestOverview.ts +++ b/src/github/pullRequestOverview.ts @@ -5,43 +5,52 @@ 'use strict'; import * as vscode from 'vscode'; -import { onDidUpdatePR, openPullRequestOnGitHub } from '../commands'; -import { IComment } from '../common/comment'; -import { commands, contexts } from '../common/executeCommands'; -import { disposeAll } from '../common/lifecycle'; -import Logger from '../common/logger'; -import { DEFAULT_MERGE_METHOD, PR_SETTINGS_NAMESPACE } from '../common/settingKeys'; -import { ITelemetry } from '../common/telemetry'; -import { ReviewEvent as CommonReviewEvent } from '../common/timelineEvent'; -import { asPromise, formatError } from '../common/utils'; -import { IRequestMessage, PULL_REQUEST_OVERVIEW_VIEW_TYPE } from '../common/webview'; +import { OpenCommitChangesArgs } from '../../common/views'; +import { openPullRequestOnGitHub } from '../commands'; +import { getCopilotApi } from './copilotApi'; +import { SessionIdForPr } from './copilotRemoteAgent'; import { FolderRepositoryManager } from './folderRepositoryManager'; import { GithubItemStateEnum, IAccount, - isTeam, + isITeam, ITeam, MergeMethod, MergeMethodsAvailability, - PullRequestMergeability, - reviewerId, - ReviewEvent, + ReviewEventEnum, ReviewState, } from './interface'; import { IssueOverviewPanel } from './issueOverview'; -import { PullRequestModel } from './pullRequestModel'; -import { PullRequestView } from './pullRequestOverviewCommon'; +import { isCopilotOnMyBehalf, PullRequestModel } from './pullRequestModel'; +import { PullRequestReviewCommon, ReviewContext } from './pullRequestReviewCommon'; import { pickEmail, reviewersQuickPick } from './quickPicks'; import { parseReviewers } from './utils'; -import { MergeArguments, MergeResult, PullRequest, ReviewType } 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'; +import { disposeAll } from '../common/lifecycle'; +import Logger from '../common/logger'; +import { DEFAULT_MERGE_METHOD, DELETE_BRANCH_AFTER_MERGE, PR_SETTINGS_NAMESPACE } from '../common/settingKeys'; +import { ITelemetry } from '../common/telemetry'; +import { EventType, ReviewEvent, SessionLinkInfo, TimelineEvent } from '../common/timelineEvent'; +import { asPromise, formatError } from '../common/utils'; +import { IRequestMessage, PULL_REQUEST_OVERVIEW_VIEW_TYPE } from '../common/webview'; export class PullRequestOverviewPanel extends IssueOverviewPanel { public static override ID: string = 'PullRequestOverviewPanel'; + public static override readonly viewType = PULL_REQUEST_OVERVIEW_VIEW_TYPE; /** * Track the currently panel. Only allow a single panel to exist at a time. */ public static override currentPanel?: PullRequestOverviewPanel; + /** + * Event emitter for when a PR overview becomes active + */ + private static _onVisible = new vscode.EventEmitter(); + public static readonly onVisible = PullRequestOverviewPanel._onVisible.event; + private _repositoryDefaultBranch: string; private _existingReviewers: ReviewState[] = []; private _teamsCount = 0; @@ -55,8 +64,17 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel { - if (pr) { - this._item.update(pr); - } - - this._postMessage({ - command: 'update-state', - state: this._item.state, - }); - } - )); this.setVisibilityContext(); - this._register(this._panel.onDidChangeViewState(() => this.setVisibilityContext())); - this._register(folderRepositoryManager.onDidMergePullRequest(_ => { - this._postMessage({ - command: 'update-state', - state: GithubItemStateEnum.Merged, - }); + + this._register(vscode.commands.registerCommand('pr.readyForReviewDescription', async () => { + return this.readyForReviewCommand(); })); - this._register(folderRepositoryManager.credentialStore.onDidUpgradeSession(() => { - this.updatePullRequest(this._item); + 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))); this._register(vscode.commands.registerCommand('review.approveOnDotComDescription', () => { - return openPullRequestOnGitHub(this._item, (this._item as any)._telemetry); + return openPullRequestOnGitHub(this._item, this._telemetry); })); this._register(vscode.commands.registerCommand('review.requestChangesOnDotComDescription', () => { - return openPullRequestOnGitHub(this._item, (this._item as any)._telemetry); + return openPullRequestOnGitHub(this._item, this._telemetry); })); } - registerPrListeners() { + protected override registerPrListeners() { disposeAll(this._prListeners); this._prListeners.push(this._folderRepositoryManager.onDidChangeActivePullRequest(_ => { if (this._folderRepositoryManager && this._item) { @@ -159,14 +170,24 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel { - if (!this._isUpdating) { + this._prListeners.push(this._item.onDidChange(e => { + if ((e.state || e.comments) && !this._isUpdating) { this.refreshPanel(); } })); } } + protected override onDidChangeViewState(e: vscode.WebviewPanelOnDidChangeViewStateEvent): void { + super.onDidChangeViewState(e); + this.setVisibilityContext(); + + // If the panel becomes visible and we have an item, notify that this PR is active + if (this._panel.visible && this._item) { + PullRequestOverviewPanel._onVisible.fire(this._item); + } + } + private setVisibilityContext() { return commands.setContext(contexts.PULL_REQUEST_DESCRIPTION_VISIBLE, this._panel.visible); } @@ -177,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 { @@ -194,7 +228,18 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel { + private preLoadInfoNotRequiredForOverview(pullRequest: PullRequestModel): void { + // Load some more info in the background, don't await. + pullRequest.getFileChangesInfo(); + } + + protected override async updateItem(pullRequestModel: PullRequestModel): Promise { + this._item = pullRequestModel; + + if (this._isUpdating) { + throw new Error('Already updating pull request webview'); + } + this._isUpdating = true; try { const [ pullRequest, @@ -211,6 +256,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel = { ...baseContext, @@ -272,6 +323,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel { + const result = super.update(folderRepositoryManager, pullRequestModel, 'pr:github'); if (this._folderRepositoryManager !== folderRepositoryManager) { - this._folderRepositoryManager = folderRepositoryManager; this.registerPrListeners(); } - this._postMessage({ - command: 'set-scroll', - scrollPosition: this._scrollPosition, - }); + await result; + // Notify that this PR overview is now active + PullRequestOverviewPanel._onVisible.fire(pullRequestModel); - if (!this._item || (this._item.number !== pullRequestModel.number) || !this._panel.webview.html) { - this._panel.webview.html = this.getHtmlForWebview(); - } - - return vscode.window.withProgress({ location: { viewId: 'pr:github' } }, () => this.updatePullRequest(pullRequestModel)); + return result; } protected override async _onDidReceiveMessage(message: IRequestMessage) { @@ -335,6 +384,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel): Promise { + if (!this._item.showChangesSinceReview) { + this._item.showChangesSinceReview = true; + } else { + PullRequestModel.openChanges(this._folderRepositoryManager, this._item); + } + return this._replyMessage(message, {}); } private async changeReviewers(message: IRequestMessage): Promise { @@ -382,26 +448,26 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel = asPromise(quickPick.onDidAccept).then(() => { const pickedReviewers: (IAccount | ITeam)[] | undefined = quickPick?.selectedItems.filter(item => item.user).map(item => item.user) as (IAccount | ITeam)[]; - const botReviewers = this._existingReviewers.filter(reviewer => !isTeam(reviewer.reviewer) && reviewer.reviewer.accountType === 'Bot').map(reviewer => reviewer.reviewer); - return pickedReviewers.concat(botReviewers); + return pickedReviewers; }); const hidePromise = asPromise(quickPick.onDidHide); const allReviewers = await Promise.race<(IAccount | ITeam)[] | void>([acceptPromise, hidePromise]); quickPick.busy = true; + quickPick.enabled = false; if (allReviewers) { const newUserReviewers: IAccount[] = []; const newTeamReviewers: ITeam[] = []; allReviewers.forEach(reviewer => { - const newReviewers: (IAccount | ITeam)[] = isTeam(reviewer) ? newTeamReviewers : newUserReviewers; + const newReviewers: (IAccount | ITeam)[] = isITeam(reviewer) ? newTeamReviewers : newUserReviewers; newReviewers.push(reviewer); }); const removedUserReviewers: IAccount[] = []; const removedTeamReviewers: ITeam[] = []; this._existingReviewers.forEach(existing => { - let newReviewers: (IAccount | ITeam)[] = isTeam(existing.reviewer) ? newTeamReviewers : newUserReviewers; - let removedReviewers: (IAccount | ITeam)[] = isTeam(existing.reviewer) ? removedTeamReviewers : removedUserReviewers; + let newReviewers: (IAccount | ITeam)[] = isITeam(existing.reviewer) ? newTeamReviewers : newUserReviewers; + let removedReviewers: (IAccount | ITeam)[] = isITeam(existing.reviewer) ? removedTeamReviewers : removedUserReviewers; if (!newReviewers.find(newTeamReviewer => newTeamReviewer.id === existing.reviewer.id)) { removedReviewers.push(existing.reviewer); } @@ -450,6 +516,10 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel { + return this._item.getTimelineEvents(); + } + private async openDiff(message: IRequestMessage<{ comment: IComment }>): Promise { try { const comment = message.args.comment; @@ -459,6 +529,66 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel): Promise { + try { + 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); + } + } + + private async cancelCodingAgent(message: IRequestMessage): Promise { + try { + let result = false; + if (message.args.event !== EventType.CopilotStarted) { + return this._replyMessage(message, { success: false, error: 'Invalid event type' }); + } else { + const copilotApi = await getCopilotApi(this._folderRepositoryManager.credentialStore, this._telemetry, this._item.remote.authProviderId); + if (copilotApi) { + const session = (await copilotApi.getAllSessions(this._item.id))[0]; + if (session.state !== 'completed') { + result = await this._item.githubRepository.cancelWorkflow(session.workflow_run_id); + } + } + } + // need to wait until we get the updated timeline events + let events: TimelineEvent[] = []; + if (result) { + do { + events = await this._getTimeline(); + } while (copilotEventToStatus(mostRecentCopilotEvent(events)) !== CopilotPRStatus.Completed && await new Promise(c => setTimeout(() => c(true), 2000))); + } + const reply: CancelCodingAgentReply = { + events + }; + this._replyMessage(message, reply); + } catch (e) { + Logger.error(`Cancelling coding agent failed: ${formatError(e)}`, PullRequestOverviewPanel.ID); + vscode.window.showErrorMessage(vscode.l10n.t('Cannot cancel coding agent')); + const reply: CancelCodingAgentReply = { + events: [], + }; + this._replyMessage(message, reply); + } + } + + private async openCommitChanges(message: IRequestMessage): Promise { + try { + const { commitSha } = message.args; + 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); + vscode.window.showErrorMessage(vscode.l10n.t('Failed to open commit changes: {0}', formatError(error))); + } + } + + private async openChanges(message?: IRequestMessage<{ openToTheSide?: boolean }>): Promise { + const openToTheSide = message?.args?.openToTheSide || false; + return PullRequestModel.openChanges(this._folderRepositoryManager, this._item, openToTheSide); + } + private async resolveCommentThread(message: IRequestMessage<{ threadId: string, toResolve: boolean, thread: IComment[] }>) { try { if (message.args.toResolve) { @@ -467,7 +597,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel, - ): void { + ): Promise { const { title, description, method, email } = message.args; - this._folderRepositoryManager - .mergePullRequest(this._item, title, description, method, email) - .then(result => { - vscode.commands.executeCommand('pr.refreshList'); + try { + const result = await this._item.merge(this._folderRepositoryManager.repository, title, description, method, email); - if (!result.merged) { - vscode.window.showErrorMessage(`Merging PR failed: ${result.message}`); + if (!result.merged) { + vscode.window.showErrorMessage(`Merging pull request failed: ${result.message}`); + } else { + // Check if auto-delete branch setting is enabled + const deleteBranchAfterMerge = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(DELETE_BRANCH_AFTER_MERGE, false); + if (deleteBranchAfterMerge) { + // Automatically delete the branch after successful merge + await PullRequestReviewCommon.autoDeleteBranchesAfterMerge(this._folderRepositoryManager, this._item); } + } - const mergeResult: MergeResult = { - state: result.merged ? GithubItemStateEnum.Merged : GithubItemStateEnum.Open, - revertable: result.merged, - events: result.timeline - }; - this._replyMessage(message, mergeResult); - }) - .catch(e => { - vscode.window.showErrorMessage(`Unable to merge pull request. ${formatError(e)}`); - this._throwError(message, {}); - }); + const mergeResult: MergeResult = { + state: result.merged ? GithubItemStateEnum.Merged : GithubItemStateEnum.Open, + revertable: result.merged, + events: result.timeline + }; + this._replyMessage(message, mergeResult); + } catch (e) { + vscode.window.showErrorMessage(`Unable to merge pull request. ${formatError(e)}`); + this._throwError(message, ''); + } } private async changeEmail(message: IRequestMessage): Promise { @@ -523,7 +657,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 { @@ -532,87 +666,52 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel): void { - this._item - .setReadyForReview() - .then(result => { - vscode.commands.executeCommand('pr.refreshList'); + private async setReadyForReview(message: IRequestMessage<{}>): Promise { + return PullRequestReviewCommon.setReadyForReview(this.getReviewContext(), message); + } - this._replyMessage(message, result); - }) - .catch(e => { - vscode.window.showErrorMessage(`Unable to set PR ready for review. ${formatError(e)}`); - this._throwError(message, {}); - }); + private async setReadyForReviewAndMerge(message: IRequestMessage<{ mergeMethod: MergeMethod }>): Promise { + return PullRequestReviewCommon.setReadyForReviewAndMerge(this.getReviewContext(), message); } - 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 async readyForReviewCommand(): Promise { + return PullRequestReviewCommon.readyForReviewCommand(this.getReviewContext()); } - private updateReviewers(review?: CommonReviewEvent): 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, - }); - } - } + private async readyForReviewAndMergeCommand(context: { mergeMethod: MergeMethod }): Promise { + return PullRequestReviewCommon.readyForReviewAndMergeCommand(this.getReviewContext(), context); } - 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 = { - command: 'pr.append-review', - event: review, - reviewers: this._existingReviewers - }; - 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' }); + private async checkoutDefaultBranch(message: IRequestMessage): Promise { + return PullRequestReviewCommon.checkoutDefaultBranch(this.getReviewContext(), message); + } + + private async doReviewCommand(context: { body: string }, reviewType: ReviewType, action: (body: string) => Promise) { + 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); - this._replyMessage(message, { - review: review, - reviewers: this._existingReviewers, - }); - } catch (e) { - vscode.window.showErrorMessage(vscode.l10n.t('Submitting review failed. {0}', formatError(e))); - this._throwError(message, `${formatError(e)}`); + private async doReviewMessage(message: IRequestMessage, action: (body) => Promise) { + const result = await PullRequestReviewCommon.doReviewMessage( + this.getReviewContext(), + message, + true, + action, + ); + if (result) { + this.tryScheduleCopilotRefresh(result.body, result.state); } } - private approvePullRequest(body: string): Promise { + private approvePullRequest(body: string): Promise { return this._item.approve(this._folderRepositoryManager.repository, body); } @@ -624,7 +723,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel this.approvePullRequest(body)); } - private requestChanges(body: string): Promise { + private requestChanges(body: string): Promise { return this._item.requestChanges(body); } @@ -636,8 +735,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel this.requestChanges(body)); } - private submitReview(body: string): Promise { - return this._item.submitReview(ReviewEvent.Comment, body); + private submitReview(body: string): Promise { + return this._item.submitReview(ReviewEventEnum.Comment, body); } private submitReviewCommand(context: { body: string }) { @@ -649,33 +748,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 && isTeam(targetReviewer.reviewer)) { - teamReviewers.push(targetReviewer.reviewer); - } else if (targetReviewer && !isTeam(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 { @@ -713,35 +786,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._item.getTimelineEvents(), - 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 { @@ -752,10 +802,28 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel) { + try { + const result: DeleteReviewResult = await this._item.deleteReview(); + await this._replyMessage(message, result); + } catch (e) { + Logger.error(formatError(e), PullRequestOverviewPanel.ID); + vscode.window.showErrorMessage(vscode.l10n.t('Deleting review failed. {0}', formatError(e))); + this._throwError(message, `${formatError(e)}`); + } + } + override dispose() { super.dispose(); disposeAll(this._prListeners); } + + /** + * Static dispose method to clean up static resources + */ + public static dispose() { + PullRequestOverviewPanel._onVisible.dispose(); + } } export function getDefaultMergeMethod( diff --git a/src/github/pullRequestOverviewCommon.ts b/src/github/pullRequestOverviewCommon.ts deleted file mode 100644 index 612fe17d4c..0000000000 --- a/src/github/pullRequestOverviewCommon.ts +++ /dev/null @@ -1,150 +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 { - DEFAULT_DELETION_METHOD, - PR_SETTINGS_NAMESPACE, - SELECT_LOCAL_BRANCH, - SELECT_REMOTE, -} from '../common/settingKeys'; -import { Schemes } from '../common/uri'; -import { FolderRepositoryManager } from './folderRepositoryManager'; -import { PullRequestModel } from './pullRequestModel'; - -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.repository.checkout(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); - - vscode.commands.executeCommand('pr.refreshList'); - - 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..1f1d565bde --- /dev/null +++ b/src/github/pullRequestReviewCommon.ts @@ -0,0 +1,457 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { BranchInfo } from './pullRequestGitHelper'; +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); + } + } + + interface SelectedAction { + type: 'remoteHead' | 'local' | 'remote' | 'suspend' + }; + + export async function deleteBranch(folderRepositoryManager: FolderRepositoryManager, item: PullRequestModel): Promise<{ isReply: boolean, message: any }> { + const branchInfo = await folderRepositoryManager.getBranchNameForPullRequest(item); + const actions: (vscode.QuickPickItem & SelectedAction)[] = []; + 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, + }); + + + if (selectedActions) { + const deletedBranchTypes: string[] = await performBranchDeletion(folderRepositoryManager, item, defaultBranch, branchInfo!, selectedActions); + + return { + isReply: false, + message: { + command: 'pr.deleteBranch', + branchTypes: deletedBranchTypes + } + }; + } else { + return { + isReply: true, + message: { + cancelled: true + } + }; + } + } + + async function performBranchDeletion(folderRepositoryManager: FolderRepositoryManager, item: PullRequestModel, defaultBranch: string, branchInfo: BranchInfo, selectedActions: SelectedAction[]): Promise { + const isBranchActive = item.equals(folderRepositoryManager.activePullRequest) || (folderRepositoryManager.repository.state.HEAD?.name && folderRepositoryManager.repository.state.HEAD.name === branchInfo?.branch); + const deletedBranchTypes: string[] = []; + + 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 deletedBranchTypes; + } + + /** + * Automatically delete branches after merge based on user preferences. + * This function does not show any prompts - it uses the default deletion method preferences. + */ + export async function autoDeleteBranchesAfterMerge(folderRepositoryManager: FolderRepositoryManager, item: PullRequestModel): Promise { + const branchInfo = await folderRepositoryManager.getBranchNameForPullRequest(item); + const defaultBranch = await folderRepositoryManager.getPullRequestRepositoryDefaultBranch(item); + + // Get user preferences for automatic deletion + const deleteLocalBranch = vscode.workspace + .getConfiguration(PR_SETTINGS_NAMESPACE) + .get(`${DEFAULT_DELETION_METHOD}.${SELECT_LOCAL_BRANCH}`, true); + + const deleteRemote = vscode.workspace + .getConfiguration(PR_SETTINGS_NAMESPACE) + .get(`${DEFAULT_DELETION_METHOD}.${SELECT_REMOTE}`, true); + + const selectedActions: SelectedAction[] = []; + + // Delete remote head branch if it's not the default branch + if (item.isResolved()) { + const isDefaultBranch = defaultBranch === item.head.ref; + if (!isDefaultBranch && !item.isRemoteHeadDeleted) { + selectedActions.push({ type: 'remoteHead' }); + } + } + + // Delete local branch if preference is set + if (branchInfo && deleteLocalBranch) { + selectedActions.push({ type: 'local' }); + } + + // Delete remote if it's no longer used and preference is set + if (branchInfo && branchInfo.remote && branchInfo.createdForPullRequest && !branchInfo.remoteInUse && deleteRemote) { + selectedActions.push({ type: 'remote' }); + } + + // Execute all deletions in parallel + await performBranchDeletion(folderRepositoryManager, item, defaultBranch, branchInfo!, selectedActions); + } +} diff --git a/src/github/queries.gql b/src/github/queries.gql index 357d33a1a2..e5762a019c 100644 --- a/src/github/queries.gql +++ b/src/github/queries.gql @@ -42,10 +42,29 @@ fragment Team on Team { # Team is not an Actor ...Node } +fragment Reactable on Reactable { + reactionGroups { + content + viewerHasReacted + reactors(first: 10) { + nodes { + ... on User { + login + } + ... on Actor { + login + } + } + totalCount + } + } +} + fragment IssueBase on Issue { number url state + stateReason body bodyHTML title @@ -65,7 +84,7 @@ fragment IssueBase on Issue { id number } - assignees(first: 10) { + assignees: assignedActors(first: 10) { nodes { ...Node ...Actor @@ -83,6 +102,7 @@ fragment IssueBase on Issue { reactions(first: 100) { totalCount } + ...Reactable repository { name owner { @@ -142,7 +162,7 @@ fragment PullRequestFragment on PullRequest { id number } - assignees(first: 10) { + assignees: assignedActors(first: 10) { nodes { ...Node ...Actor @@ -160,7 +180,7 @@ fragment PullRequestFragment on PullRequest { reactions(first: 100) { totalCount } - + ...Reactable comments(first: 1) { totalCount } @@ -237,6 +257,8 @@ fragment PullRequestFragment on PullRequest { ...Node } } + additions + deletions } query Issue($owner: String!, $name: String!, $number: Int!) { @@ -327,6 +349,359 @@ query PullRequestMergeabilityMergeRequirements($owner: String!, $name: String!, } } +query LatestUpdates($owner: String!, $name: String!, $number: Int!, $since: DateTime!) { + repository(owner: $owner, name: $name) { + pullRequest(number: $number) { + reactions(orderBy:{direction:DESC, field: CREATED_AT}, first: 1) { + nodes { + createdAt + } + } + updatedAt + comments(orderBy: {direction:DESC, field: UPDATED_AT}, first: 1) { + nodes { + updatedAt + reactions(orderBy:{direction:DESC, field: CREATED_AT}, first: 1) { + nodes { + createdAt + } + } + } + } + timelineItems(since: $since, first: 1) { + nodes { + ... on AddedToMergeQueueEvent { + createdAt + } + ... on AddedToProjectEvent { + createdAt + } + ... on AssignedEvent { + createdAt + } + ... on AutoMergeDisabledEvent { + createdAt + } + ... on AutoMergeEnabledEvent { + createdAt + } + ... on AutoRebaseEnabledEvent { + createdAt + } + ... on AutoSquashEnabledEvent { + createdAt + } + ... on AutomaticBaseChangeFailedEvent { + createdAt + } + ... on AutomaticBaseChangeSucceededEvent { + createdAt + } + ... on BaseRefChangedEvent { + createdAt + } + ... on BaseRefDeletedEvent { + createdAt + } + ... on BaseRefForcePushedEvent { + createdAt + } + ... on ClosedEvent { + createdAt + } + ... on CommentDeletedEvent { + createdAt + } + ... on ConnectedEvent { + createdAt + } + ... on ConvertToDraftEvent { + createdAt + } + ... on ConvertedNoteToIssueEvent { + createdAt + } + ... on ConvertedToDiscussionEvent { + createdAt + } + ... on CrossReferencedEvent { + createdAt + } + ... on DemilestonedEvent { + createdAt + } + ... on DeployedEvent { + createdAt + } + ... on DeploymentEnvironmentChangedEvent { + createdAt + } + ... on DisconnectedEvent { + createdAt + } + ... on HeadRefDeletedEvent { + createdAt + } + ... on HeadRefForcePushedEvent { + createdAt + } + ... on HeadRefRestoredEvent { + createdAt + } + ... on IssueComment { + createdAt + } + ... on IssueTypeAddedEvent { + createdAt + } + ... on LabeledEvent { + createdAt + } + ... on LockedEvent { + createdAt + } + ... on MarkedAsDuplicateEvent { + createdAt + } + ... on MentionedEvent { + createdAt + } + ... on MergedEvent { + createdAt + } + ... on MilestonedEvent { + createdAt + } + ... on MovedColumnsInProjectEvent { + createdAt + } + ... on PinnedEvent { + createdAt + } + ... on PullRequestCommit { + commit { + committedDate + } + } + ... on PullRequestReview { + createdAt + } + ... on PullRequestReviewThread { + comments(last: 1) { + nodes { + createdAt + } + } + } + ... on PullRequestRevisionMarker { + createdAt + } + ... on ReadyForReviewEvent { + createdAt + } + ... on ReferencedEvent { + createdAt + } + ... on RemovedFromMergeQueueEvent { + createdAt + } + ... on RemovedFromProjectEvent { + createdAt + } + ... on RenamedTitleEvent { + createdAt + } + ... on ReopenedEvent { + createdAt + } + ... on ReviewDismissedEvent { + createdAt + } + ... on ReviewRequestRemovedEvent { + createdAt + } + ... on ReviewRequestedEvent { + createdAt + } + ... on SubscribedEvent { + createdAt + } + ... on TransferredEvent { + createdAt + } + ... on UnassignedEvent { + createdAt + } + ... on UnlabeledEvent { + createdAt + } + ... on UnlockedEvent { + createdAt + } + ... on UnmarkedAsDuplicateEvent { + createdAt + } + ... on UnpinnedEvent { + createdAt + } + ... on UnsubscribedEvent { + createdAt + } + ... on UserBlockedEvent { + createdAt + } + } + } + } + } + rateLimit { + ...RateLimit + } +} + +query LatestIssueUpdates($owner: String!, $name: String!, $number: Int!, $since: DateTime!) { + repository(owner: $owner, name: $name) { + pullRequest: issue(number: $number) { + reactions(orderBy:{direction:DESC, field: CREATED_AT}, first: 1) { + nodes { + createdAt + } + } + updatedAt + comments(orderBy: {direction:DESC, field: UPDATED_AT}, first: 1) { + nodes { + updatedAt + reactions(orderBy:{direction:DESC, field: CREATED_AT}, first: 1) { + nodes { + createdAt + } + } + } + } + timelineItems(since: $since, first: 1) { + nodes { + ... on AddedToProjectEvent { + createdAt + } + ... on AssignedEvent { + createdAt + } + ... on ClosedEvent { + createdAt + } + ... on CommentDeletedEvent { + createdAt + } + ... on ConnectedEvent { + createdAt + } + ... on ConvertedNoteToIssueEvent { + createdAt + } + ... on ConvertedToDiscussionEvent { + createdAt + } + ... on CrossReferencedEvent { + createdAt + } + ... on DemilestonedEvent { + createdAt + } + ... on DisconnectedEvent { + createdAt + } + ... on IssueComment { + createdAt + } + ... on IssueTypeAddedEvent { + createdAt + } + ... on LabeledEvent { + createdAt + } + ... on LockedEvent { + createdAt + } + ... on MarkedAsDuplicateEvent { + createdAt + } + ... on MentionedEvent { + createdAt + } + ... on MilestonedEvent { + createdAt + } + ... on MovedColumnsInProjectEvent { + createdAt + } + ... on PinnedEvent { + createdAt + } + ... on ReferencedEvent { + createdAt + } + ... on RemovedFromProjectEvent { + createdAt + } + ... on RenamedTitleEvent { + createdAt + } + ... on ReopenedEvent { + createdAt + } + ... on SubscribedEvent { + createdAt + } + ... on TransferredEvent { + createdAt + } + ... on UnassignedEvent { + createdAt + } + ... on UnlabeledEvent { + createdAt + } + ... on UnlockedEvent { + createdAt + } + ... on UnmarkedAsDuplicateEvent { + createdAt + } + ... on UnpinnedEvent { + createdAt + } + ... on UnsubscribedEvent { + createdAt + } + ... on UserBlockedEvent { + createdAt + } + } + } + } + } + rateLimit { + ...RateLimit + } +} + +query GetAssignableUsers($owner: String!, $name: String!, $first: Int!, $after: String) { + repository(owner: $owner, name: $name) { + assignableUsers(first: $first, after: $after) { + nodes { + ...User + } + pageInfo { + hasNextPage + endCursor + } + } + } + rateLimit { + ...RateLimit + } +} + mutation CreatePullRequest($input: CreatePullRequestInput!) { createPullRequest(input: $input) { pullRequest { @@ -383,7 +758,7 @@ mutation EnqueuePullRequest($input: EnqueuePullRequestInput!) { mutation ReplaceActorsForAssignable($input: ReplaceActorsForAssignableInput!) { replaceActorsForAssignable(input: $input) { assignable { - assignees(first: 100) { + assignees: assignedActors(first: 100) { nodes { ...Node ...Actor diff --git a/src/github/queriesExtra.gql b/src/github/queriesExtra.gql index 513d7bf4df..45967950d4 100644 --- a/src/github/queriesExtra.gql +++ b/src/github/queriesExtra.gql @@ -42,11 +42,29 @@ fragment Team on Team { # Team is not an Actor ...Node } +fragment Reactable on Reactable { + reactionGroups { + content + viewerHasReacted + reactors(first: 10) { + nodes { + ... on User { + login + } + ... on Actor { + login + } + } + totalCount + } + } +} fragment IssueBase on Issue { number url state + stateReason body bodyHTML title @@ -66,7 +84,7 @@ fragment IssueBase on Issue { id number } - assignees(first: 10) { + assignees: assignedActors(first: 10) { nodes { ...Node ...Actor @@ -84,6 +102,7 @@ fragment IssueBase on Issue { reactions(first: 100) { totalCount } + ...Reactable repository { name owner { @@ -152,7 +171,7 @@ fragment PullRequestFragment on PullRequest { id number } - assignees(first: 10) { + assignees: assignedActors(first: 10) { nodes { ...Node ...Actor @@ -170,6 +189,7 @@ fragment PullRequestFragment on PullRequest { reactions(first: 100) { totalCount } + ...Reactable projectItems(first: 100) { nodes { id @@ -179,15 +199,9 @@ fragment PullRequestFragment on PullRequest { } } } - - comments(first: 1) { - totalCount - } - comments(first: 1) { totalCount } - commits(first: 50) { nodes { commit { @@ -357,6 +371,359 @@ query GetSuggestedActors($owner: String!, $name: String!, $capabilities: [Reposi } } +query LatestUpdates($owner: String!, $name: String!, $number: Int!, $since: DateTime!) { + repository(owner: $owner, name: $name) { + pullRequest(number: $number) { + reactions(orderBy:{direction:DESC, field: CREATED_AT}, first: 1) { + nodes { + createdAt + } + } + updatedAt + comments(orderBy: {direction:DESC, field: UPDATED_AT}, first: 1) { + nodes { + updatedAt + reactions(orderBy:{direction:DESC, field: CREATED_AT}, first: 1) { + nodes { + createdAt + } + } + } + } + timelineItems(since: $since, first: 1) { + nodes { + ... on AddedToMergeQueueEvent { + createdAt + } + ... on AddedToProjectEvent { + createdAt + } + ... on AssignedEvent { + createdAt + } + ... on AutoMergeDisabledEvent { + createdAt + } + ... on AutoMergeEnabledEvent { + createdAt + } + ... on AutoRebaseEnabledEvent { + createdAt + } + ... on AutoSquashEnabledEvent { + createdAt + } + ... on AutomaticBaseChangeFailedEvent { + createdAt + } + ... on AutomaticBaseChangeSucceededEvent { + createdAt + } + ... on BaseRefChangedEvent { + createdAt + } + ... on BaseRefDeletedEvent { + createdAt + } + ... on BaseRefForcePushedEvent { + createdAt + } + ... on ClosedEvent { + createdAt + } + ... on CommentDeletedEvent { + createdAt + } + ... on ConnectedEvent { + createdAt + } + ... on ConvertToDraftEvent { + createdAt + } + ... on ConvertedNoteToIssueEvent { + createdAt + } + ... on ConvertedToDiscussionEvent { + createdAt + } + ... on CrossReferencedEvent { + createdAt + } + ... on DemilestonedEvent { + createdAt + } + ... on DeployedEvent { + createdAt + } + ... on DeploymentEnvironmentChangedEvent { + createdAt + } + ... on DisconnectedEvent { + createdAt + } + ... on HeadRefDeletedEvent { + createdAt + } + ... on HeadRefForcePushedEvent { + createdAt + } + ... on HeadRefRestoredEvent { + createdAt + } + ... on IssueComment { + createdAt + } + ... on IssueTypeAddedEvent { + createdAt + } + ... on LabeledEvent { + createdAt + } + ... on LockedEvent { + createdAt + } + ... on MarkedAsDuplicateEvent { + createdAt + } + ... on MentionedEvent { + createdAt + } + ... on MergedEvent { + createdAt + } + ... on MilestonedEvent { + createdAt + } + ... on MovedColumnsInProjectEvent { + createdAt + } + ... on PinnedEvent { + createdAt + } + ... on PullRequestCommit { + commit { + committedDate + } + } + ... on PullRequestReview { + createdAt + } + ... on PullRequestReviewThread { + comments(last: 1) { + nodes { + createdAt + } + } + } + ... on PullRequestRevisionMarker { + createdAt + } + ... on ReadyForReviewEvent { + createdAt + } + ... on ReferencedEvent { + createdAt + } + ... on RemovedFromMergeQueueEvent { + createdAt + } + ... on RemovedFromProjectEvent { + createdAt + } + ... on RenamedTitleEvent { + createdAt + } + ... on ReopenedEvent { + createdAt + } + ... on ReviewDismissedEvent { + createdAt + } + ... on ReviewRequestRemovedEvent { + createdAt + } + ... on ReviewRequestedEvent { + createdAt + } + ... on SubscribedEvent { + createdAt + } + ... on TransferredEvent { + createdAt + } + ... on UnassignedEvent { + createdAt + } + ... on UnlabeledEvent { + createdAt + } + ... on UnlockedEvent { + createdAt + } + ... on UnmarkedAsDuplicateEvent { + createdAt + } + ... on UnpinnedEvent { + createdAt + } + ... on UnsubscribedEvent { + createdAt + } + ... on UserBlockedEvent { + createdAt + } + } + } + } + } + rateLimit { + ...RateLimit + } +} + +query LatestIssueUpdates($owner: String!, $name: String!, $number: Int!, $since: DateTime!) { + repository(owner: $owner, name: $name) { + pullRequest: issue(number: $number) { + reactions(orderBy:{direction:DESC, field: CREATED_AT}, first: 1) { + nodes { + createdAt + } + } + updatedAt + comments(orderBy: {direction:DESC, field: UPDATED_AT}, first: 1) { + nodes { + updatedAt + reactions(orderBy:{direction:DESC, field: CREATED_AT}, first: 1) { + nodes { + createdAt + } + } + } + } + timelineItems(since: $since, first: 1) { + nodes { + ... on AddedToProjectEvent { + createdAt + } + ... on AssignedEvent { + createdAt + } + ... on ClosedEvent { + createdAt + } + ... on CommentDeletedEvent { + createdAt + } + ... on ConnectedEvent { + createdAt + } + ... on ConvertedNoteToIssueEvent { + createdAt + } + ... on ConvertedToDiscussionEvent { + createdAt + } + ... on CrossReferencedEvent { + createdAt + } + ... on DemilestonedEvent { + createdAt + } + ... on DisconnectedEvent { + createdAt + } + ... on IssueComment { + createdAt + } + ... on IssueTypeAddedEvent { + createdAt + } + ... on LabeledEvent { + createdAt + } + ... on LockedEvent { + createdAt + } + ... on MarkedAsDuplicateEvent { + createdAt + } + ... on MentionedEvent { + createdAt + } + ... on MilestonedEvent { + createdAt + } + ... on MovedColumnsInProjectEvent { + createdAt + } + ... on PinnedEvent { + createdAt + } + ... on ReferencedEvent { + createdAt + } + ... on RemovedFromProjectEvent { + createdAt + } + ... on RenamedTitleEvent { + createdAt + } + ... on ReopenedEvent { + createdAt + } + ... on SubscribedEvent { + createdAt + } + ... on TransferredEvent { + createdAt + } + ... on UnassignedEvent { + createdAt + } + ... on UnlabeledEvent { + createdAt + } + ... on UnlockedEvent { + createdAt + } + ... on UnmarkedAsDuplicateEvent { + createdAt + } + ... on UnpinnedEvent { + createdAt + } + ... on UnsubscribedEvent { + createdAt + } + ... on UserBlockedEvent { + createdAt + } + } + } + } + } + rateLimit { + ...RateLimit + } +} + +query GetAssignableUsers($owner: String!, $name: String!, $first: Int!, $after: String) { + repository(owner: $owner, name: $name) { + assignableUsers(first: $first, after: $after) { + nodes { + ...User + } + pageInfo { + hasNextPage + endCursor + } + } + } + rateLimit { + ...RateLimit + } +} + mutation CreatePullRequest($input: CreatePullRequestInput!) { createPullRequest(input: $input) { pullRequest { @@ -368,7 +735,7 @@ mutation CreatePullRequest($input: CreatePullRequestInput!) { mutation ReplaceActorsForAssignable($input: ReplaceActorsForAssignableInput!) { replaceActorsForAssignable(input: $input) { assignable { - assignees(first: 100) { + assignees: assignedActors(first: 100) { nodes { ...Node ...Actor diff --git a/src/github/queriesLimited.gql b/src/github/queriesLimited.gql index ae3e0002b9..9ce0dffa9d 100644 --- a/src/github/queriesLimited.gql +++ b/src/github/queriesLimited.gql @@ -32,10 +32,29 @@ fragment Organization on Organization { ...Node } +fragment Reactable on Reactable { + reactionGroups { + content + viewerHasReacted + reactors(first: 10) { + nodes { + ... on User { + login + } + ... on Actor { + login + } + } + totalCount + } + } +} + fragment IssueBase on Issue { number url state + stateReason body bodyHTML title @@ -73,6 +92,7 @@ fragment IssueBase on Issue { reactions(first: 100) { totalCount } + ...Reactable repository { name owner { @@ -150,7 +170,7 @@ fragment PullRequestFragment on PullRequest { reactions(first: 100) { totalCount } - + ...Reactable comments(first: 1) { totalCount } @@ -291,6 +311,336 @@ query GetAssignableUsers($owner: String!, $name: String!, $first: Int!, $after: } } +query LatestUpdates($owner: String!, $name: String!, $number: Int!, $since: DateTime!) { + repository(owner: $owner, name: $name) { + pullRequest(number: $number) { + reactions(orderBy:{direction:DESC, field: CREATED_AT}, first: 1) { + nodes { + createdAt + } + } + updatedAt + comments(orderBy: {direction:DESC, field: UPDATED_AT}, first: 1) { + nodes { + updatedAt + reactions(orderBy:{direction:DESC, field: CREATED_AT}, first: 1) { + nodes { + createdAt + } + } + } + } + timelineItems(since: $since, first: 1) { + nodes { + ... on AddedToMergeQueueEvent { + createdAt + } + ... on AddedToProjectEvent { + createdAt + } + ... on AssignedEvent { + createdAt + } + ... on AutoMergeDisabledEvent { + createdAt + } + ... on AutoMergeEnabledEvent { + createdAt + } + ... on AutoRebaseEnabledEvent { + createdAt + } + ... on AutoSquashEnabledEvent { + createdAt + } + ... on AutomaticBaseChangeFailedEvent { + createdAt + } + ... on AutomaticBaseChangeSucceededEvent { + createdAt + } + ... on BaseRefChangedEvent { + createdAt + } + ... on BaseRefDeletedEvent { + createdAt + } + ... on BaseRefForcePushedEvent { + createdAt + } + ... on ClosedEvent { + createdAt + } + ... on CommentDeletedEvent { + createdAt + } + ... on ConnectedEvent { + createdAt + } + ... on ConvertToDraftEvent { + createdAt + } + ... on ConvertedNoteToIssueEvent { + createdAt + } + ... on ConvertedToDiscussionEvent { + createdAt + } + ... on CrossReferencedEvent { + createdAt + } + ... on DemilestonedEvent { + createdAt + } + ... on DeployedEvent { + createdAt + } + ... on DeploymentEnvironmentChangedEvent { + createdAt + } + ... on DisconnectedEvent { + createdAt + } + ... on HeadRefDeletedEvent { + createdAt + } + ... on HeadRefForcePushedEvent { + createdAt + } + ... on HeadRefRestoredEvent { + createdAt + } + ... on IssueComment { + createdAt + } + ... on LabeledEvent { + createdAt + } + ... on LockedEvent { + createdAt + } + ... on MarkedAsDuplicateEvent { + createdAt + } + ... on MentionedEvent { + createdAt + } + ... on MergedEvent { + createdAt + } + ... on MilestonedEvent { + createdAt + } + ... on MovedColumnsInProjectEvent { + createdAt + } + ... on PinnedEvent { + createdAt + } + ... on PullRequestCommit { + commit { + committedDate + } + } + ... on PullRequestReview { + createdAt + } + ... on PullRequestReviewThread { + comments(last: 1) { + nodes { + createdAt + } + } + } + ... on PullRequestRevisionMarker { + createdAt + } + ... on ReadyForReviewEvent { + createdAt + } + ... on ReferencedEvent { + createdAt + } + ... on RemovedFromMergeQueueEvent { + createdAt + } + ... on RemovedFromProjectEvent { + createdAt + } + ... on RenamedTitleEvent { + createdAt + } + ... on ReopenedEvent { + createdAt + } + ... on ReviewDismissedEvent { + createdAt + } + ... on ReviewRequestRemovedEvent { + createdAt + } + ... on ReviewRequestedEvent { + createdAt + } + ... on SubscribedEvent { + createdAt + } + ... on TransferredEvent { + createdAt + } + ... on UnassignedEvent { + createdAt + } + ... on UnlabeledEvent { + createdAt + } + ... on UnlockedEvent { + createdAt + } + ... on UnmarkedAsDuplicateEvent { + createdAt + } + ... on UnpinnedEvent { + createdAt + } + ... on UnsubscribedEvent { + createdAt + } + ... on UserBlockedEvent { + createdAt + } + } + } + } + } + rateLimit { + ...RateLimit + } +} + +query LatestIssueUpdates($owner: String!, $name: String!, $number: Int!, $since: DateTime!) { + repository(owner: $owner, name: $name) { + pullRequest: issue(number: $number) { + reactions(orderBy:{direction:DESC, field: CREATED_AT}, first: 1) { + nodes { + createdAt + } + } + updatedAt + comments(orderBy: {direction:DESC, field: UPDATED_AT}, first: 1) { + nodes { + updatedAt + reactions(orderBy:{direction:DESC, field: CREATED_AT}, first: 1) { + nodes { + createdAt + } + } + } + } + timelineItems(since: $since, first: 1) { + nodes { + ... on AddedToProjectEvent { + createdAt + } + ... on AssignedEvent { + createdAt + } + ... on ClosedEvent { + createdAt + } + ... on CommentDeletedEvent { + createdAt + } + ... on ConnectedEvent { + createdAt + } + ... on ConvertedNoteToIssueEvent { + createdAt + } + ... on ConvertedToDiscussionEvent { + createdAt + } + ... on CrossReferencedEvent { + createdAt + } + ... on DemilestonedEvent { + createdAt + } + ... on DisconnectedEvent { + createdAt + } + ... on IssueComment { + createdAt + } + ... on LabeledEvent { + createdAt + } + ... on LockedEvent { + createdAt + } + ... on MarkedAsDuplicateEvent { + createdAt + } + ... on MentionedEvent { + createdAt + } + ... on MilestonedEvent { + createdAt + } + ... on MovedColumnsInProjectEvent { + createdAt + } + ... on PinnedEvent { + createdAt + } + ... on ReferencedEvent { + createdAt + } + ... on RemovedFromProjectEvent { + createdAt + } + ... on RenamedTitleEvent { + createdAt + } + ... on ReopenedEvent { + createdAt + } + ... on SubscribedEvent { + createdAt + } + ... on TransferredEvent { + createdAt + } + ... on UnassignedEvent { + createdAt + } + ... on UnlabeledEvent { + createdAt + } + ... on UnlockedEvent { + createdAt + } + ... on UnmarkedAsDuplicateEvent { + createdAt + } + ... on UnpinnedEvent { + createdAt + } + ... on UnsubscribedEvent { + createdAt + } + ... on UserBlockedEvent { + createdAt + } + } + } + } + } + rateLimit { + ...RateLimit + } +} + mutation CreatePullRequest($input: CreatePullRequestInput!) { createPullRequest(input: $input) { pullRequest { diff --git a/src/github/queriesShared.gql b/src/github/queriesShared.gql index 088c98e451..473841f0a1 100644 --- a/src/github/queriesShared.gql +++ b/src/github/queriesShared.gql @@ -103,6 +103,7 @@ fragment Comment on IssueComment { viewerCanUpdate viewerCanReact viewerCanDelete + ...Reactable } fragment Commit on PullRequestCommit { @@ -121,7 +122,10 @@ fragment Commit on PullRequestCommit { } oid message - authoredDate + committedDate + statusCheckRollup { + state + } } url } @@ -140,6 +144,20 @@ fragment AssignedEvent on AssignedEvent { createdAt } +fragment UnassignedEvent on UnassignedEvent { + id + actor { + ...Node + ...Actor + } + user { + ...Node + ...Actor + ...User + } + createdAt +} + fragment CrossReferencedEvent on CrossReferencedEvent { id actor { @@ -152,16 +170,46 @@ fragment CrossReferencedEvent on CrossReferencedEvent { number url title + repository: baseRepository { + owner { + login + } + name + } } ... on Issue { number url title + repository { + owner { + login + } + name + } } } willCloseTarget } +fragment ClosedEvent on ClosedEvent { + id + actor { + ...Node + ...Actor + } + createdAt +} + +fragment ReopenedEvent on ReopenedEvent { + id + actor { + ...Node + ...Actor + } + createdAt +} + fragment Review on PullRequestReview { id databaseId @@ -179,6 +227,7 @@ fragment Review on PullRequestReview { submittedAt updatedAt createdAt + ...Reactable } fragment Reactable on Reactable { @@ -190,6 +239,9 @@ fragment Reactable on Reactable { ... on User { login } + ... on Actor { + login + } } totalCount } @@ -255,8 +307,11 @@ query TimelineEvents($owner: String!, $name: String!, $number: Int!, $last: Int ...Review ...Commit ...AssignedEvent + ...UnassignedEvent ...HeadRefDeleted ...CrossReferencedEvent + ...ClosedEvent + ...ReopenedEvent } } } @@ -274,7 +329,10 @@ query IssueTimelineEvents($owner: String!, $name: String!, $number: Int!, $last: __typename ...Comment ...AssignedEvent + ...UnassignedEvent ...CrossReferencedEvent + ...ClosedEvent + ...ReopenedEvent } } } @@ -743,6 +801,13 @@ mutation UpdateIssue($input: UpdateIssueInput!) { bodyHTML title titleHTML + milestone { + title + dueOn + createdAt + id + number + } } } } @@ -754,6 +819,13 @@ mutation UpdatePullRequest($input: UpdatePullRequestInput!) { bodyHTML title titleHTML + milestone { + title + dueOn + createdAt + id + number + } } } } @@ -842,6 +914,23 @@ query MaxIssue($owner: String!, $name: String!) { } } +query MaxPullRequest($owner: String!, $name: String!) { + repository(owner: $owner, name: $name) { + issues: pullRequests(first: 1, orderBy: { direction: DESC, field: CREATED_AT }) { + edges { + node { + ... on PullRequest { + number + } + } + } + } + } + rateLimit { + ...RateLimit + } +} + query GetMilestones($owner: String!, $name: String!, $states: [MilestoneState!]!) { repository(owner: $owner, name: $name) { milestones(first: 100, orderBy: { direction: DESC, field: DUE_DATE }, states: $states) { @@ -1136,7 +1225,11 @@ mutation MergePullRequest($input: MergePullRequestInput!, $last: Int = 150) { ...Review ...Commit ...AssignedEvent + ...UnassignedEvent ...HeadRefDeleted + ...CrossReferencedEvent + ...ClosedEvent + ...ReopenedEvent } } } diff --git a/src/github/quickPicks.ts b/src/github/quickPicks.ts index 1ef5b4ae58..78e723db45 100644 --- a/src/github/quickPicks.ts +++ b/src/github/quickPicks.ts @@ -6,14 +6,40 @@ import { Buffer } from 'buffer'; import * as vscode from 'vscode'; +import { FolderRepositoryManager } from './folderRepositoryManager'; +import { GitHubRepository, TeamReviewerRefreshKind } from './githubRepository'; +import { AccountType, IAccount, ILabel, IMilestone, IProject, isISuggestedReviewer, isITeam, ISuggestedReviewer, ITeam, reviewerId, ReviewState } from './interface'; +import { IssueModel } from './issueModel'; +import { DisplayLabel } from './views'; import { COPILOT_ACCOUNTS } from '../common/comment'; +import { COPILOT_REVIEWER, COPILOT_REVIEWER_ID, COPILOT_SWE_AGENT } from '../common/copilot'; +import { emojify, ensureEmojis } from '../common/emoji'; import Logger from '../common/logger'; import { DataUri } from '../common/uri'; import { formatError } from '../common/utils'; -import { FolderRepositoryManager } from './folderRepositoryManager'; -import { GitHubRepository, TeamReviewerRefreshKind } from './githubRepository'; -import { AccountType, IAccount, ILabel, IMilestone, IProject, isSuggestedReviewer, isTeam, ISuggestedReviewer, ITeam, reviewerId, ReviewState } from './interface'; -import { IssueModel } from './issueModel'; + +export async function chooseItem( + itemsToChooseFrom: T[], + propertyGetter: (itemValue: T) => { label: string; description?: string; }, + options?: vscode.QuickPickOptions, +): Promise { + if (itemsToChooseFrom.length === 0) { + return undefined; + } + if (itemsToChooseFrom.length === 1) { + return itemsToChooseFrom[0]; + } + interface Item extends vscode.QuickPickItem { + itemValue: T; + } + const items: Item[] = itemsToChooseFrom.map(currentItem => { + return { + ...propertyGetter(currentItem), + itemValue: currentItem, + }; + }); + return (await vscode.window.showQuickPick(items, options))?.itemValue; +} async function getItems(context: vscode.ExtensionContext, skipList: Set, users: T[], picked: boolean, tooManyAssignable: boolean = false): Promise<(vscode.QuickPickItem & { user?: T })[]> { const alreadyAssignedItems: (vscode.QuickPickItem & { user?: T })[] = []; @@ -32,7 +58,7 @@ async function getItems(context const user = filteredUsers[i]; let detail: string | undefined; - if (isSuggestedReviewer(user)) { + if (isISuggestedReviewer(user)) { detail = user.isAuthor && user.isCommenter ? vscode.l10n.t('Recently edited and reviewed changes to these files') : user.isAuthor @@ -43,7 +69,7 @@ async function getItems(context } alreadyAssignedItems.push({ - label: isTeam(user) ? `${user.org}/${user.slug}` : COPILOT_ACCOUNTS[user.login] ? COPILOT_ACCOUNTS[user.login].name : user.login, + label: isITeam(user) ? `${user.org}/${user.slug}` : COPILOT_ACCOUNTS[user.login] ? COPILOT_ACCOUNTS[user.login].name : user.login, description: user.name, user, picked, @@ -120,13 +146,12 @@ export async function getAssigneesQuickPickItems(folderRepositoryManager: Folder } function userThemeIcon(user: IAccount | ITeam) { - return (isTeam(user) ? new vscode.ThemeIcon('organization') : new vscode.ThemeIcon('account')); + return (isITeam(user) ? new vscode.ThemeIcon('organization') : new vscode.ThemeIcon('account')); } async function getReviewersQuickPickItems(folderRepositoryManager: FolderRepositoryManager, remoteName: string, isInOrganization: boolean, author: IAccount, existingReviewers: ReviewState[], suggestedReviewers: ISuggestedReviewer[] | undefined, refreshKind: TeamReviewerRefreshKind, ): Promise<(vscode.QuickPickItem & { user?: IAccount | ITeam })[]> { - existingReviewers = existingReviewers.filter(reviewer => isTeam(reviewer.reviewer) || (reviewer.reviewer.accountType !== AccountType.Bot)); if (!suggestedReviewers) { return []; } @@ -135,8 +160,20 @@ async function getReviewersQuickPickItems(folderRepositoryManager: FolderReposit const allTeamReviewers = isInOrganization ? await folderRepositoryManager.getTeamReviewers(refreshKind) : []; const teamReviewers: ITeam[] = allTeamReviewers[remoteName] ?? []; const assignableUsers: (IAccount | ITeam)[] = [...teamReviewers]; - if (allAssignableUsers[remoteName]) { - assignableUsers.push(...allAssignableUsers[remoteName]); + + // Remove the swe agent as it can't do reviews, but add the reviewer instead + const originalAssignableUsers = allAssignableUsers[remoteName] ?? []; + let hasCopilotSweAgent: boolean = false; + const assignableUsersForRemote = originalAssignableUsers.filter(user => { + if (user.login === COPILOT_SWE_AGENT) { + hasCopilotSweAgent = true; + return false; + } + return true; + }); + + if (assignableUsersForRemote) { + assignableUsers.push(...assignableUsersForRemote); } // used to track logins that shouldn't be added to pick list @@ -152,6 +189,19 @@ async function getReviewersQuickPickItems(folderRepositoryManager: FolderReposit reviewersPromises.push(getItems(folderRepositoryManager.context, skipList, existingReviewers.map(reviewer => reviewer.reviewer), true)); } + // If we removed the coding agent, add the Copilot reviewer instead + if (hasCopilotSweAgent && !existingReviewers.find(user => (user.reviewer as IAccount).login === COPILOT_REVIEWER)) { + const copilotReviewer: IAccount = { + login: COPILOT_REVIEWER, + id: COPILOT_REVIEWER_ID, + url: '', + avatarUrl: '', + name: COPILOT_ACCOUNTS[COPILOT_REVIEWER]?.name ?? 'Copilot', + accountType: AccountType.Bot + }; + assignableUsers.push(copilotReviewer); + } + // Suggested reviewers reviewersPromises.push(getItems(folderRepositoryManager.context, skipList, suggestedReviewers, false)); @@ -284,7 +334,18 @@ export async function getMilestoneFromQuickPick(folderRepositoryManager: FolderR }; let selectedItem: vscode.QuickPickItem | undefined; async function getMilestoneOptions(): Promise<(MilestoneQuickPickItem | vscode.QuickPickItem)[]> { - const milestones = await githubRepository.getMilestones(); + const milestones = (await githubRepository.getMilestones())?.sort((a, b) => { + // Milestones with a date should be first, and sorted by due date + if (a.dueOn && b.dueOn) { + return new Date(a.dueOn).getTime() - new Date(b.dueOn).getTime(); + } else if (a.dueOn) { + return -1; + } else if (b.dueOn) { + return 1; + } else { + return a.title.localeCompare(b.title); + } + }); if (!milestones || !milestones.length) { return [ { @@ -381,18 +442,26 @@ export async function getLabelOptions( labels: ILabel[], baseOwner: string, repositoryName: string -): Promise<{ newLabels: ILabel[], labelPicks: vscode.QuickPickItem[] }> { - const newLabels = await folderRepoManager.getLabels(undefined, { owner: baseOwner, repo: repositoryName }); +): Promise<{ newLabels: DisplayLabel[], labelPicks: (vscode.QuickPickItem & { name: string })[] }> { + await ensureEmojis(folderRepoManager.context); + const newLabels = (await folderRepoManager.getLabels(undefined, { owner: baseOwner, repo: repositoryName })).map(label => ({ ...label, displayName: emojify(label.name) })); const labelPicks = newLabels.map(label => { return { - label: label.name, + label: label.displayName, + name: label.name, description: label.description ?? undefined, picked: labels.some(existingLabel => existingLabel.name === label.name), iconPath: DataUri.asImageDataURI(Buffer.from(` `, 'utf8')) }; + }).sort((a, b) => { + // Sort so that already picked labels are at the top + if (a.picked === b.picked) { + return a.label.localeCompare(b.label); + } + return a.picked ? -1 : 1; }); return { newLabels, labelPicks }; } diff --git a/src/github/repositoriesManager.ts b/src/github/repositoriesManager.ts index 096ba5ddbd..e2fce7a325 100644 --- a/src/github/repositoriesManager.ts +++ b/src/github/repositoriesManager.ts @@ -4,6 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { CredentialStore } from './credentials'; +import { FolderRepositoryManager, ReposManagerState, ReposManagerStateContext } from './folderRepositoryManager'; +import { PullRequestChangeEvent } from './githubRepository'; +import { IssueModel } from './issueModel'; +import { findDotComAndEnterpriseRemotes, getEnterpriseUri, hasEnterpriseUri, setEnterpriseUri } from './utils'; import { Repository } from '../api/api'; import { AuthProvider } from '../common/authentication'; import { commands, contexts } from '../common/executeCommands'; @@ -13,10 +18,6 @@ import { ITelemetry } from '../common/telemetry'; import { EventType } from '../common/timelineEvent'; import { fromPRUri, fromRepoUri, Schemes } from '../common/uri'; import { compareIgnoreCase, isDescendant } from '../common/utils'; -import { CredentialStore } from './credentials'; -import { FolderRepositoryManager, ReposManagerState, ReposManagerStateContext } from './folderRepositoryManager'; -import { IssueModel } from './issueModel'; -import { findDotComAndEnterpriseRemotes, getEnterpriseUri, hasEnterpriseUri, setEnterpriseUri } from './utils'; export interface ItemsResponseResult { items: T[]; @@ -45,6 +46,15 @@ export class RepositoriesManager extends Disposable { private _onDidLoadAnyRepositories = new vscode.EventEmitter(); readonly onDidLoadAnyRepositories = this._onDidLoadAnyRepositories.event; + private _onDidChangeAnyPullRequests = new vscode.EventEmitter(); + readonly onDidChangeAnyPullRequests = this._onDidChangeAnyPullRequests.event; + + private _onDidAddPullRequest = new vscode.EventEmitter(); + readonly onDidAddPullRequest = this._onDidAddPullRequest.event; + + private _onDidAddAnyGitHubRepository = new vscode.EventEmitter(); + readonly onDidChangeAnyGitHubRepository = this._onDidAddAnyGitHubRepository.event; + private _state: ReposManagerState = ReposManagerState.Initializing; constructor( @@ -72,12 +82,15 @@ export class RepositoriesManager extends Disposable { private registerFolderListeners(folderManager: FolderRepositoryManager) { const disposables = [ - folderManager.onDidLoadRepositories(state => { - this.state = state; + folderManager.onDidLoadRepositories(() => { + this.updateState(); this._onDidLoadAnyRepositories.fire(); }), folderManager.onDidChangeActivePullRequest(() => this.updateActiveReviewCount()), - folderManager.onDidDispose(() => this.removeRepo(folderManager.repository)) + folderManager.onDidDispose(() => this.removeRepo(folderManager.repository)), + folderManager.onDidChangeAnyPullRequests(e => this._onDidChangeAnyPullRequests.fire(e)), + folderManager.onDidAddPullRequest(e => this._onDidAddPullRequest.fire(e)), + folderManager.onDidChangeGithubRepositories(() => this._onDidAddAnyGitHubRepository.fire(folderManager)), ]; this._subs.set(folderManager, disposables); } @@ -179,11 +192,29 @@ export class RepositoriesManager extends Disposable { return this._state; } - set state(state: ReposManagerState) { - const stateChange = state !== this._state; - this._state = state; + private updateState(state?: ReposManagerState) { + let maxState = ReposManagerState.Initializing; + if (state) { + maxState = state; + } else { + // Get the most advanced state from all folder managers + const stateValue = (testState: ReposManagerState) => { + switch (testState) { + case ReposManagerState.Initializing: return 0; + case ReposManagerState.NeedsAuthentication: return 1; + case ReposManagerState.RepositoriesLoaded: return 2; + } + }; + for (const folderManager of this._folderManagers) { + if (stateValue(folderManager.state) > stateValue(maxState)) { + maxState = folderManager.state; + } + } + } + const stateChange = maxState !== this._state; + this._state = maxState; if (stateChange) { - vscode.commands.executeCommand('setContext', ReposManagerStateContext, state); + vscode.commands.executeCommand('setContext', ReposManagerStateContext, maxState); this._onDidChangeState.fire(); } } @@ -194,7 +225,7 @@ export class RepositoriesManager extends Disposable { async clearCredentialCache(): Promise { await this._credentialStore.reset(); - this.state = ReposManagerState.Initializing; + this.updateState(ReposManagerState.NeedsAuthentication); } async authenticate(enterprise?: boolean): Promise { diff --git a/src/github/revertPRViewProvider.ts b/src/github/revertPRViewProvider.ts index 31dd9b7440..c50ad7eaa1 100644 --- a/src/github/revertPRViewProvider.ts +++ b/src/github/revertPRViewProvider.ts @@ -6,9 +6,6 @@ import * as vscode from 'vscode'; import { CreateParamsNew, CreatePullRequestNew } from '../../common/views'; import { openDescription } from '../commands'; -import { commands, contexts } from '../common/executeCommands'; -import { ITelemetry } from '../common/telemetry'; -import { IRequestMessage } from '../common/webview'; import { BaseCreatePullRequestViewProvider, BasePullRequestDataModel } from './createPRViewProvider'; import { FolderRepositoryManager, @@ -16,6 +13,9 @@ import { } from './folderRepositoryManager'; import { BaseBranchMetadata } from './pullRequestGitHelper'; import { PullRequestModel } from './pullRequestModel'; +import { commands, contexts } from '../common/executeCommands'; +import { ITelemetry } from '../common/telemetry'; +import { IRequestMessage } from '../common/webview'; export class RevertPullRequestViewProvider extends BaseCreatePullRequestViewProvider implements vscode.WebviewViewProvider { constructor( diff --git a/src/github/utils.ts b/src/github/utils.ts index 3b65723440..6a978948a2 100644 --- a/src/github/utils.ts +++ b/src/github/utils.ts @@ -7,19 +7,6 @@ import * as crypto from 'crypto'; import * as OctokitTypes from '@octokit/types'; import * as vscode from 'vscode'; -import { Repository } from '../api/api'; -import { GitApiImpl } from '../api/api1'; -import { AuthProvider, GitHubServerType } from '../common/authentication'; -import { COPILOT_ACCOUNTS, IComment, IReviewThread, Reaction, SubjectType } from '../common/comment'; -import { DiffHunk, parseDiffHunk } from '../common/diffHunk'; -import { GitHubRef } from '../common/githubRef'; -import Logger from '../common/logger'; -import { Remote } from '../common/remote'; -import { Resource } from '../common/resources'; -import { GITHUB_ENTERPRISE, OVERRIDE_DEFAULT_BRANCH, PR_SETTINGS_NAMESPACE, URI } from '../common/settingKeys'; -import * as Common from '../common/timelineEvent'; -import { DataUri } from '../common/uri'; -import { gitHubLabelColor, uniqBy } from '../common/utils'; import { OctokitCommon } from './common'; import { FolderRepositoryManager, PullRequestDefaults } from './folderRepositoryManager'; import { GitHubRepository, ViewerPermission } from './githubRepository'; @@ -44,6 +31,7 @@ import { NotificationSubjectType, PullRequest, PullRequestMergeability, + Reaction, reviewerId, reviewerLabel, ReviewState, @@ -52,6 +40,21 @@ import { } from './interface'; import { IssueModel } from './issueModel'; import { GHPRComment, GHPRCommentThread } from './prComment'; +import { RemoteInfo } from '../../common/types'; +import { Repository } from '../api/api'; +import { GitApiImpl } from '../api/api1'; +import { AuthProvider, GitHubServerType } from '../common/authentication'; +import { COPILOT_ACCOUNTS, IComment, IReviewThread, SubjectType } from '../common/comment'; +import { COPILOT_SWE_AGENT } from '../common/copilot'; +import { DiffHunk, parseDiffHunk } from '../common/diffHunk'; +import { emojify } from '../common/emoji'; +import { GitHubRef } from '../common/githubRef'; +import Logger from '../common/logger'; +import { Remote } from '../common/remote'; +import { GITHUB_ENTERPRISE, OVERRIDE_DEFAULT_BRANCH, PR_SETTINGS_NAMESPACE, URI } from '../common/settingKeys'; +import * as Common from '../common/timelineEvent'; +import { DataUri, toOpenIssueWebviewUri, toOpenPullRequestWebviewUri } from '../common/uri'; +import { escapeRegExp, gitHubLabelColor, stringReplaceAsync, uniqBy } from '../common/utils'; export const ISSUE_EXPRESSION = /(([A-Za-z0-9_.\-]+)\/([A-Za-z0-9_.\-]+))?(#|GH-)([1-9][0-9]*)($|\b)/; export const ISSUE_OR_URL_EXPRESSION = /(https?:\/\/github\.com\/(([^\s]+)\/([^\s]+))\/([^\s]+\/)?(issues|pull)\/([0-9]+)(#issuecomment\-([0-9]+))?)|(([A-Za-z0-9_.\-]+)\/([A-Za-z0-9_.\-]+))?(#|GH-)([1-9][0-9]*)($|\b)/; @@ -97,6 +100,17 @@ export function threadRange(startLine: number, endLine: number, endCharacter?: n return new vscode.Range(startLine, 0, endLine, endCharacter); } +export async function setReplyAuthor(thread: vscode.CommentThread | vscode.CommentThread2, currentUser: IAccount, context: vscode.ExtensionContext) { + if (currentUser.avatarUrl) { + const thread2 = thread as vscode.CommentThread2; + thread2.canReply = { name: currentUser.name ?? currentUser.login, iconPath: vscode.Uri.parse(currentUser.avatarUrl) }; + const uri = await DataUri.avatarCirclesAsImageDataUris(context, [currentUser], 28, 28); + thread2.canReply = { name: currentUser.name ?? currentUser.login, iconPath: uri[0] }; + } else { + thread.canReply = true; + } +} + export function createVSCodeCommentThreadForReviewThread( context: vscode.ExtensionContext, uri: vscode.Uri, @@ -128,12 +142,7 @@ export function createVSCodeCommentThreadForReviewThread( updateCommentThreadLabel(vscodeThread as GHPRCommentThread); vscodeThread.collapsibleState = getCommentCollapsibleState(thread, undefined, currentUser.login); - if (currentUser.avatarUrl) { - vscodeThread.canReply = { name: currentUser.name ?? currentUser.login, iconPath: vscode.Uri.parse(currentUser.avatarUrl) }; - DataUri.avatarCirclesAsImageDataUris(context, [currentUser], 28, 28).then(uri => { - vscodeThread.canReply = { name: currentUser.name ?? currentUser.login, iconPath: uri[0] }; - }); - } + setReplyAuthor(vscodeThread, currentUser, context); return vscodeThread as GHPRCommentThread; } @@ -301,6 +310,18 @@ export function convertRESTHeadToIGitHubRef(head: OctokitCommon.PullsListRespons }; } +async function transformHtmlUrlsToExtensionUrls(body: string, githubRepository: GitHubRepository): Promise { + const issueRegex = new RegExp( + `href="https?:\/\/${escapeRegExp(githubRepository.remote.gitProtocol.url.authority)}\\/${escapeRegExp(githubRepository.remote.owner)}\\/${escapeRegExp(githubRepository.remote.repositoryName)}\\/(issues|pull)\\/([0-9]+)"`, 'g'); + return stringReplaceAsync(body, issueRegex, async (match: string, issuesOrPull: string, number: string) => { + if (issuesOrPull === 'issues') { + return `href="${(await toOpenIssueWebviewUri({ owner: githubRepository.remote.owner, repo: githubRepository.remote.repositoryName, issueNumber: Number(number) })).toString()}""`; + } else { + return `href="${(await toOpenPullRequestWebviewUri({ owner: githubRepository.remote.owner, repo: githubRepository.remote.repositoryName, pullRequestNumber: Number(number) })).toString()}"`; + } + }); +} + export function convertRESTPullRequestToRawPullRequest( pullRequest: | OctokitCommon.PullsGetResponseData @@ -350,12 +371,13 @@ export function convertRESTPullRequestToRawPullRequest( projectItems: [], // projects only available through GraphQL API commits: [], // commits only available through GraphQL API reactionCount: 0, // reaction count only available through GraphQL API + reactions: [], // reactions only available through GraphQL API commentCount: 0 // comment count only available through GraphQL API }; // mergeable is not included in the list response, will need to fetch later - if ('mergeable' in pullRequest) { - item.mergeable = pullRequest.mergeable + if ((pullRequest as OctokitCommon.PullsGetResponseData).mergeable !== undefined) { + item.mergeable = (pullRequest as OctokitCommon.PullsGetResponseData).mergeable ? PullRequestMergeability.Mergeable : PullRequestMergeability.NotMergeable; } @@ -403,6 +425,7 @@ export function convertRESTIssueToRawPullRequest( ), projectItems: [], // projects only available through GraphQL API reactionCount: 0, // reaction count only available through GraphQL API + reactions: [], // reactions only available through GraphQL API commentCount: comments }; @@ -416,7 +439,7 @@ export function convertRESTReviewEvent( return { event: Common.EventType.Reviewed, comments: [], - submittedAt: (review as any).submitted_at, // TODO fix typings upstream + submittedAt: review.submitted_at, body: review.body, bodyHTML: review.body, htmlUrl: review.html_url, @@ -424,6 +447,7 @@ export function convertRESTReviewEvent( authorAssociation: review.user!.type, state: review.state as 'COMMENTED' | 'APPROVED' | 'CHANGES_REQUESTED' | 'PENDING', id: review.id, + reactions: undefined // reactions only available through GraphQL API }; } @@ -451,6 +475,8 @@ export function convertGraphQLEventType(text: string) { return Common.EventType.Milestoned; case 'AssignedEvent': return Common.EventType.Assigned; + case 'UnassignedEvent': + return Common.EventType.Unassigned; case 'HeadRefDeletedEvent': return Common.EventType.HeadRefDeleted; case 'IssueComment': @@ -461,6 +487,10 @@ export function convertGraphQLEventType(text: string) { return Common.EventType.Merged; case 'CrossReferencedEvent': return Common.EventType.CrossReferenced; + case 'ClosedEvent': + return Common.EventType.Closed; + case 'ReopenedEvent': + return Common.EventType.Reopened; default: return Common.EventType.Other; } @@ -534,6 +564,7 @@ export function parseGraphQlIssueComment(comment: GraphQL.IssueComment, githubRe htmlUrl: comment.url, graphNodeId: comment.id, diffHunk: '', + reactions: parseGraphQLReaction(comment.reactionGroups), }; } @@ -541,7 +572,7 @@ export function parseGraphQLReaction(reactionGroups: GraphQL.ReactionGroup[]): R const reactionContentEmojiMapping = getReactionGroup().reduce((prev, curr) => { prev[curr.title] = curr; return prev; - }, {} as { [key: string]: { title: string; label: string; icon?: vscode.Uri } }); + }, {} as { [key: string]: { title: string; label: string; icon?: string } }); const reactions = reactionGroups .filter(group => group.reactors.totalCount > 0) @@ -551,7 +582,7 @@ export function parseGraphQLReaction(reactionGroups: GraphQL.ReactionGroup[]): R count: group.reactors.totalCount, icon: reactionContentEmojiMapping[group.content].icon, viewerHasReacted: group.viewerHasReacted, - reactors: group.reactors.nodes.map(node => node.login) + reactors: group.reactors.nodes.map(node => COPILOT_ACCOUNTS[node.login]?.name ?? node.login) }; return reaction; @@ -578,23 +609,62 @@ function parseRef(refName: string, oid: string, repository?: GraphQL.RefReposito }; } +export interface RestAccount { + login: string; + html_url: string; + avatar_url: string; + email?: string | null; + node_id: string; + name?: string | null; + type: string; +} + +export interface GraphQLAccount { + login: string; + url: string; + avatarUrl: string; + email?: string; + id: string; + name?: string; + __typename: string; +} + export function parseAccount( - author: { login: string; url: string; avatarUrl: string; email?: string, id: string, name?: string, __typename: string } | { login: string; url: string; avatar_url: string; email?: string | null, node_id: string, name?: string | null, type: string } | null, + author: GraphQLAccount | RestAccount | null, githubRepository?: GitHubRepository, ): IAccount { if (author) { - const avatarUrl = 'avatarUrl' in author ? author.avatarUrl : author.avatar_url; - const id = 'node_id' in author ? author.node_id : author.id; + let avatarUrl: string; + let id: string; + let url: string; + let accountType: string; + if ((author as RestAccount).html_url) { + const asRestAccount = author as RestAccount; + avatarUrl = asRestAccount.avatar_url; + id = asRestAccount.node_id; + url = asRestAccount.html_url; + accountType = asRestAccount.type; + } else { + const asGraphQLAccount = author as GraphQLAccount; + avatarUrl = asGraphQLAccount.avatarUrl; + id = asGraphQLAccount.id; + url = asGraphQLAccount.url; + accountType = asGraphQLAccount.__typename; + } + // In some places, Copilot comes in as a user, and in others as a bot + + const finalAvatarUrl = githubRepository ? getAvatarWithEnterpriseFallback(avatarUrl, undefined, githubRepository.remote.isEnterprise) : avatarUrl; + return { login: author.login, - url: author.url, - avatarUrl: githubRepository ? getAvatarWithEnterpriseFallback(avatarUrl, undefined, githubRepository.remote.isEnterprise) : avatarUrl, + url: COPILOT_ACCOUNTS[author.login]?.url ?? url, + avatarUrl: finalAvatarUrl, email: author.email ?? undefined, - id: id, + id, name: author.name ?? COPILOT_ACCOUNTS[author.login]?.name ?? undefined, specialDisplayName: COPILOT_ACCOUNTS[author.login] ? (author.name ?? COPILOT_ACCOUNTS[author.login].name) : undefined, - accountType: toAccountType('__typename' in author ? author.__typename : author.type), + accountType: toAccountType(accountType), }; } else { return { @@ -626,7 +696,7 @@ export function parseGraphQLReviewers(data: GraphQL.GetReviewRequestsResponse, r if (GraphQL.isTeam(reviewer.requestedReviewer)) { const team: ITeam = parseTeam(reviewer.requestedReviewer, repository); reviewers.push(team); - } else { + } else if (GraphQL.isAccount(reviewer.requestedReviewer)) { const account: IAccount = parseAccount(reviewer.requestedReviewer, repository); reviewers.push(account); } @@ -752,10 +822,10 @@ export function parsePullRequestState(state: string): GithubItemStateEnum { } } -export function parseGraphQLPullRequest( +export async function parseGraphQLPullRequest( graphQLPullRequest: GraphQL.PullRequest, githubRepository: GitHubRepository, -): PullRequest { +): Promise { const pr: PullRequest = { id: graphQLPullRequest.databaseId, graphNodeId: graphQLPullRequest.id, @@ -763,7 +833,7 @@ export function parseGraphQLPullRequest( number: graphQLPullRequest.number, state: graphQLPullRequest.state, body: graphQLPullRequest.body, - bodyHTML: graphQLPullRequest.bodyHTML, + bodyHTML: await transformHtmlUrlsToExtensionUrls(graphQLPullRequest.bodyHTML, githubRepository), title: graphQLPullRequest.title, titleHTML: graphQLPullRequest.titleHTML, createdAt: graphQLPullRequest.createdAt, @@ -790,7 +860,10 @@ export function parseGraphQLPullRequest( assignees: graphQLPullRequest.assignees?.nodes.map(assignee => parseAccount(assignee, githubRepository)), commits: parseCommits(graphQLPullRequest.commits.nodes), reactionCount: graphQLPullRequest.reactions.totalCount, + reactions: parseGraphQLReaction(graphQLPullRequest.reactionGroups), commentCount: graphQLPullRequest.comments.totalCount, + additions: graphQLPullRequest.additions, + deletions: graphQLPullRequest.deletions, closingIssues: parseClosingIssuesReferences(graphQLPullRequest.closingIssuesReferences?.nodes) }; pr.mergeCommitMeta = parseCommitMeta(graphQLPullRequest.baseRepository.mergeCommitTitle, graphQLPullRequest.baseRepository.mergeCommitMessage, pr); @@ -879,15 +952,16 @@ function parseComments(comments: GraphQL.AbbreviatedIssueComment[] | undefined, return parsedComments; } -export function parseGraphQLIssue(issue: GraphQL.Issue, githubRepository: GitHubRepository): Issue { +export async function parseGraphQLIssue(issue: GraphQL.Issue, githubRepository: GitHubRepository): Promise { return { id: issue.databaseId, graphNodeId: issue.id, url: issue.url, number: issue.number, state: issue.state, + stateReason: issue.stateReason, body: issue.body, - bodyHTML: issue.bodyHTML, + bodyHTML: await transformHtmlUrlsToExtensionUrls(issue.bodyHTML, githubRepository), title: issue.title, titleHTML: issue.titleHTML, createdAt: issue.createdAt, @@ -902,6 +976,7 @@ export function parseGraphQLIssue(issue: GraphQL.Issue, githubRepository: GitHub projectItems: parseProjectItems(issue.projectItems?.nodes), comments: issue.comments.nodes?.map(comment => parseIssueComment(comment, githubRepository)), reactionCount: issue.reactions.totalCount, + reactions: parseGraphQLReaction(issue.reactionGroups), commentCount: issue.comments.totalCount }; } @@ -981,10 +1056,89 @@ export function parseGraphQLReviewEvent( authorAssociation: review.authorAssociation, state: review.state, id: review.databaseId, + reactions: parseGraphQLReaction(review.reactionGroups), }; } -export function parseGraphQLTimelineEvents( +export function parseSelectRestTimelineEvents( + issueModel: IssueModel, + events: OctokitCommon.ListEventsForTimelineResponse[] +): Common.TimelineEvent[] { + const parsedEvents: Common.TimelineEvent[] = []; + + const prSessionLink: Common.SessionPullInfo = { + id: issueModel.id, + host: issueModel.githubRepository.remote.gitProtocol.host, + owner: issueModel.githubRepository.remote.owner, + repo: issueModel.githubRepository.remote.repositoryName, + pullNumber: issueModel.number, + }; + + let sessionIndex = 0; + for (const event of events) { + const eventNode = event as { created_at?: string; node_id?: string; actor: RestAccount }; + if (eventNode.created_at && eventNode.node_id) { + if (event.event === 'copilot_work_started') { + parsedEvents.push({ + id: eventNode.node_id, + event: Common.EventType.CopilotStarted, + createdAt: eventNode.created_at, + onBehalfOf: parseAccount(eventNode.actor), + sessionLink: { + ...prSessionLink, + sessionIndex + } + }); + } else if (event.event === 'copilot_work_finished') { + parsedEvents.push({ + id: eventNode.node_id, + event: Common.EventType.CopilotFinished, + createdAt: eventNode.created_at, + onBehalfOf: parseAccount(eventNode.actor) + }); + sessionIndex++; + } else if (event.event === 'copilot_work_finished_failure') { + sessionIndex++; + parsedEvents.push({ + id: eventNode.node_id, + event: Common.EventType.CopilotFinishedError, + createdAt: eventNode.created_at, + onBehalfOf: parseAccount(eventNode.actor), + sessionLink: { + ...prSessionLink, + sessionIndex + } + }); + } + } + } + + return parsedEvents; +} + +export function eventTime(event: Common.TimelineEvent): Date | undefined { + switch (event.event) { + case Common.EventType.Committed: + return new Date(event.committedDate); + case Common.EventType.Commented: + case Common.EventType.Assigned: + case Common.EventType.HeadRefDeleted: + case Common.EventType.Merged: + case Common.EventType.CrossReferenced: + case Common.EventType.Closed: + case Common.EventType.Reopened: + case Common.EventType.CopilotStarted: + case Common.EventType.CopilotFinished: + case Common.EventType.CopilotFinishedError: + return new Date(event.createdAt); + case Common.EventType.Reviewed: + return new Date(event.submittedAt); + default: + return undefined; + } +} + +export async function parseCombinedTimelineEvents( events: ( | GraphQL.MergedEvent | GraphQL.Review @@ -993,17 +1147,45 @@ export function parseGraphQLTimelineEvents( | GraphQL.AssignedEvent | GraphQL.HeadRefDeletedEvent | GraphQL.CrossReferencedEvent + | null )[], + restEvents: Common.TimelineEvent[], githubRepository: GitHubRepository, -): Common.TimelineEvent[] { +): Promise { const normalizedEvents: Common.TimelineEvent[] = []; - events.forEach(event => { + let restEventIndex = -1; + let restEventTime: number | undefined; + const incrementRestEvent = () => { + restEventIndex++; + restEventTime = restEvents.length > restEventIndex ? eventTime(restEvents[restEventIndex])?.getTime() : undefined; + }; + incrementRestEvent(); + const addTimelineEvent = (event: Common.TimelineEvent) => { + if (!restEventTime) { + normalizedEvents.push(event); + return; + } + const newEventTime = eventTime(event)?.getTime(); + if (newEventTime) { + while (restEventTime && newEventTime > restEventTime) { + normalizedEvents.push(restEvents[restEventIndex]); + incrementRestEvent(); + } + } + normalizedEvents.push(event); + }; + + // TODO: work the rest events into the appropriate place in the timeline + for (const event of events) { + if (!event) { + continue; + } const type = convertGraphQLEventType(event.__typename); switch (type) { case Common.EventType.Commented: const commentEvent = event as GraphQL.IssueComment; - normalizedEvents.push({ + addTimelineEvent({ htmlUrl: commentEvent.url, body: commentEvent.body, bodyHTML: commentEvent.bodyHTML, @@ -1014,11 +1196,12 @@ export function parseGraphQLTimelineEvents( id: commentEvent.databaseId, graphNodeId: commentEvent.id, createdAt: commentEvent.createdAt, + reactions: parseGraphQLReaction(commentEvent.reactionGroups), }); - return; + break; case Common.EventType.Reviewed: const reviewEvent = event as GraphQL.Review; - normalizedEvents.push({ + addTimelineEvent({ event: type, comments: [], submittedAt: reviewEvent.submittedAt, @@ -1029,11 +1212,12 @@ export function parseGraphQLTimelineEvents( authorAssociation: reviewEvent.authorAssociation, state: reviewEvent.state, id: reviewEvent.databaseId, + reactions: parseGraphQLReaction(reviewEvent.reactionGroups), }); - return; + break; case Common.EventType.Committed: const commitEv = event as GraphQL.Commit; - normalizedEvents.push({ + addTimelineEvent({ id: commitEv.id, event: type, sha: commitEv.commit.oid, @@ -1042,13 +1226,14 @@ export function parseGraphQLTimelineEvents( : { login: commitEv.commit.committer.name }, htmlUrl: commitEv.url, message: commitEv.commit.message, - authoredDate: new Date(commitEv.commit.authoredDate), + committedDate: new Date(commitEv.commit.committedDate), + status: commitEv.commit.statusCheckRollup?.state } as Common.CommitEvent); // TODO remove cast - return; + break; case Common.EventType.Merged: const mergeEv = event as GraphQL.MergedEvent; - normalizedEvents.push({ + addTimelineEvent({ id: mergeEv.id, event: type, user: parseActor(mergeEv.actor, githubRepository), @@ -1059,50 +1244,93 @@ export function parseGraphQLTimelineEvents( url: mergeEv.url, graphNodeId: mergeEv.id, }); - return; + break; case Common.EventType.Assigned: const assignEv = event as GraphQL.AssignedEvent; - normalizedEvents.push({ + addTimelineEvent({ id: assignEv.id, event: type, assignees: [parseAccount(assignEv.user, githubRepository)], actor: parseAccount(assignEv.actor), createdAt: assignEv.createdAt, }); - return; + break; + case Common.EventType.Unassigned: + const unassignEv = event as GraphQL.UnassignedEvent; + + normalizedEvents.push({ + id: unassignEv.id, + event: type, + unassignees: [parseAccount(unassignEv.user, githubRepository)], + actor: parseAccount(unassignEv.actor), + createdAt: unassignEv.createdAt, + }); + break; case Common.EventType.HeadRefDeleted: const deletedEv = event as GraphQL.HeadRefDeletedEvent; - normalizedEvents.push({ + addTimelineEvent({ id: deletedEv.id, event: type, actor: parseAccount(deletedEv.actor, githubRepository), createdAt: deletedEv.createdAt, headRef: deletedEv.headRefName, }); - return; + break; case Common.EventType.CrossReferenced: const crossRefEv = event as GraphQL.CrossReferencedEvent; - - normalizedEvents.push({ + const isIssue = crossRefEv.source.__typename === 'Issue'; + const extensionUrl = isIssue + ? await toOpenIssueWebviewUri({ owner: crossRefEv.source.repository.owner.login, repo: crossRefEv.source.repository.name, issueNumber: crossRefEv.source.number }) + : await toOpenPullRequestWebviewUri({ owner: crossRefEv.source.repository.owner.login, repo: crossRefEv.source.repository.name, pullRequestNumber: crossRefEv.source.number }); + addTimelineEvent({ id: crossRefEv.id, event: type, actor: parseAccount(crossRefEv.actor, githubRepository), createdAt: crossRefEv.createdAt, source: { url: crossRefEv.source.url, + extensionUrl: extensionUrl.toString(), number: crossRefEv.source.number, - title: crossRefEv.source.title + title: crossRefEv.source.title, + isIssue, + owner: crossRefEv.source.repository.owner.login, + repo: crossRefEv.source.repository.name, }, willCloseTarget: crossRefEv.willCloseTarget }); - return; + break; + case Common.EventType.Closed: + const closedEv = event as GraphQL.ClosedEvent; + + addTimelineEvent({ + id: closedEv.id, + event: type, + actor: parseAccount(closedEv.actor, githubRepository), + createdAt: closedEv.createdAt, + }); + break; + case Common.EventType.Reopened: + const reopenedEv = event as GraphQL.ReopenedEvent; + + addTimelineEvent({ + id: reopenedEv.id, + event: type, + actor: parseAccount(reopenedEv.actor, githubRepository), + createdAt: reopenedEv.createdAt, + }); + break; default: break; } - }); + } + // Add any remaining rest events + while (restEventTime) { + normalizedEvents.push(restEvents[restEventIndex]); + incrementRestEvent(); + } return normalizedEvents; } @@ -1136,55 +1364,47 @@ function parseGraphQLCommitContributions( return items; } -export function getReactionGroup(): { title: string; label: string; icon?: vscode.Uri }[] { +export function getReactionGroup(): { title: string; label: string; icon?: string }[] { const ret = [ { title: 'THUMBS_UP', // allow-any-unicode-next-line - label: '👍', - icon: Resource.icons.reactions.THUMBS_UP, + label: '👍' }, { title: 'THUMBS_DOWN', // allow-any-unicode-next-line - label: '👎', - icon: Resource.icons.reactions.THUMBS_DOWN, + label: '👎' }, { title: 'LAUGH', // allow-any-unicode-next-line - label: '😄', - icon: Resource.icons.reactions.LAUGH, + label: '😄' }, { title: 'HOORAY', // allow-any-unicode-next-line - label: '🎉', - icon: Resource.icons.reactions.HOORAY, + label: '🎉' }, { title: 'CONFUSED', // allow-any-unicode-next-line - label: '😕', - icon: Resource.icons.reactions.CONFUSED, + label: '😕' }, { title: 'HEART', // allow-any-unicode-next-line - label: '❤️', - icon: Resource.icons.reactions.HEART, + label: '❤️' }, { title: 'ROCKET', // allow-any-unicode-next-line - label: '🚀', - icon: Resource.icons.reactions.ROCKET, + label: '🚀' }, { title: 'EYES', // allow-any-unicode-next-line - label: '👀', - icon: Resource.icons.reactions.EYES, + label: '👀' }, ]; @@ -1199,6 +1419,7 @@ export async function restPaginate(r do { const result = await request( { + // eslint-disable-next-line rulesdir/no-cast-to-any ...(variables as any), per_page, page @@ -1363,7 +1584,7 @@ export function parseNotification(notification: OctokitCommon.Notification): Not lastReadAt: notification.last_read_at ? new Date(notification.last_read_at) : undefined, reason: notification.reason, unread: notification.unread, - updatedAd: new Date(notification.updated_at), + updatedAt: new Date(notification.updated_at), }; } @@ -1443,8 +1664,21 @@ export function generateGravatarUrl(gravatarId: string | undefined, size: number } export function getAvatarWithEnterpriseFallback(avatarUrl: string, email: string | undefined, isEnterpriseRemote: boolean): string | undefined { - return !isEnterpriseRemote ? avatarUrl : (email ? generateGravatarUrl( - crypto.createHash('sha256').update(email?.trim()?.toLowerCase()).digest('hex')) : undefined); + + // For non-enterprise, always use the provided avatarUrl + if (!isEnterpriseRemote) { + return avatarUrl; + } + + // For enterprise, prefer GitHub avatarUrl if available, fallback to Gravatar only if needed + if (avatarUrl && avatarUrl.trim()) { + return avatarUrl; + } + + // Only fallback to Gravatar if no avatarUrl is available and email is provided + const gravatarUrl = email ? generateGravatarUrl( + crypto.createHash('sha256').update(email.trim().toLowerCase()).digest('hex')) : undefined; + return gravatarUrl; } export function getPullsUrl(repo: GitHubRepository) { @@ -1477,12 +1711,12 @@ function computeSinceValue(sinceValue: string | undefined): string { const COPILOT_PATTERN = /\:(Copilot|copilot)(\s|$)/g; const VARIABLE_PATTERN = /\$\{([^-]*?)(-.*?)?\}/g; -export async function variableSubstitution( +export function variableSubstitution( value: string, issueModel?: IssueModel, defaults?: PullRequestDefaults, user?: string, -): Promise { +): string { const withVariables = value.replace(VARIABLE_PATTERN, (match: string, variable: string, extra: string) => { let result: string; switch (variable) { @@ -1523,7 +1757,7 @@ export async function variableSubstitution( // not a variable, but still a substitution that needs to be done const withCopilot = withVariables.replace(COPILOT_PATTERN, () => { - return `:copilot-swe-agent[bot]`; + return `:${COPILOT_SWE_AGENT}[bot] `; }); return withCopilot; } @@ -1583,7 +1817,8 @@ export function vscodeDevPrLink(pullRequest: IssueModel) { export function makeLabel(label: ILabel): string { const isDarkTheme = vscode.window.activeColorTheme.kind === vscode.ColorThemeKind.Dark; const labelColor = gitHubLabelColor(label.color, isDarkTheme, true); - return `  ${label.name.trim()}  `; + const labelName = emojify(label.name.trim()); + return `  ${labelName}  `; } @@ -1593,4 +1828,22 @@ export enum UnsatisfiedChecks { ChangesRequested = 1 << 1, CIFailed = 1 << 2, CIPending = 1 << 3 +} + +export async function extractRepoFromQuery(folderManager: FolderRepositoryManager, query: string | undefined): Promise { + if (!query) { + return undefined; + } + + const defaults = await folderManager.getPullRequestDefaults(); + // Use a fake user since we only care about pulling out the repo and repo owner + const substituted = variableSubstitution(query, undefined, defaults, 'fakeUser'); + + const repoRegex = /(?:^|\s)repo:(?:"?(?[A-Za-z0-9_.-]+)\/(?[A-Za-z0-9_.-]+)"?)/i; + const repoMatch = repoRegex.exec(substituted); + if (repoMatch && repoMatch.groups) { + return { owner: repoMatch.groups.owner, repositoryName: repoMatch.groups.repo }; + } + + return undefined; } \ No newline at end of file diff --git a/src/github/views.ts b/src/github/views.ts index 70ebc0fab4..9b2af7ccd3 100644 --- a/src/github/views.ts +++ b/src/github/views.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { TimelineEvent } from '../common/timelineEvent'; import { GithubItemStateEnum, IAccount, @@ -16,8 +15,12 @@ import { PullRequestChecks, PullRequestMergeability, PullRequestReviewRequirement, + Reaction, ReviewState, + StateReason, } from './interface'; +import { IComment } from '../common/comment'; +import { CommentEvent, ReviewEvent, SessionLinkInfo, TimelineEvent } from '../common/timelineEvent'; export enum ReviewType { Comment = 'comment', @@ -25,7 +28,13 @@ export enum ReviewType { RequestChanges = 'requestChanges', } +export interface DisplayLabel extends ILabel { + displayName: string; +} + export interface Issue { + owner: string; + repo: string; number: number; title: string; titleHTML: string; @@ -35,8 +44,9 @@ export interface Issue { bodyHTML?: string; author: IAccount; state: GithubItemStateEnum; // TODO: don't allow merged + stateReason?: StateReason; events: TimelineEvent[]; - labels: ILabel[]; + labels: DisplayLabel[]; assignees: IAccount[]; projectItems: IProjectItem[] | undefined; milestone: IMilestone | undefined; @@ -52,15 +62,17 @@ export interface Issue { pendingCommentText?: string; pendingCommentDrafts?: { [key: string]: string }; isIssue: boolean; - isAuthor?: boolean; + isAuthor: boolean; continueOnGitHub: boolean; isDarkTheme: boolean; isEnterprise: boolean; canAssignCopilot: boolean; + reactions: Reaction[]; busy?: boolean; } export interface PullRequest extends Issue { + isCopilotOnMyBehalf: boolean; isCurrentlyCheckedOut: boolean; isRemoteBaseDeleted?: boolean; base: string; @@ -96,6 +108,7 @@ export interface PullRequest extends Issue { lastReviewType?: ReviewType; revertable?: boolean; busy?: boolean; + loadingCommit?: string; closingIssues: Pick[]; } @@ -108,6 +121,19 @@ export interface ChangeAssigneesReply { events: TimelineEvent[]; } +export interface SubmitReviewReply { + events?: TimelineEvent[]; + reviewedEvent: ReviewEvent | CommentEvent; + reviewers?: ReviewState[]; +} + +export interface ReadyForReviewReply { + isDraft: boolean; + reviewEvent?: ReviewEvent; + reviewers?: ReviewState[]; + autoMerge?: boolean; +} + export interface MergeArguments { title: string | undefined; description: string | undefined; @@ -121,9 +147,31 @@ export interface MergeResult { events?: TimelineEvent[]; } +export interface DeleteReviewResult { + deletedReviewId: number; + deletedReviewComments: IComment[]; +} + export enum PreReviewState { None = 0, Available, ReviewedWithComments, ReviewedWithoutComments } + +export interface CancelCodingAgentReply { + events: TimelineEvent[]; +} + +export interface OverviewContext { + 'preventDefaultContextMenuItems': true; + owner: string; + repo: string; + number: number; + [key: string]: boolean | string | number; +} + +export interface CodingAgentContext extends SessionLinkInfo { + 'preventDefaultContextMenuItems': true; + [key: string]: boolean | string | number | undefined; +} \ No newline at end of file diff --git a/src/integrations/gitlens/gitlensImpl.ts b/src/integrations/gitlens/gitlensImpl.ts index 0ba73b1b0a..a589aa1252 100644 --- a/src/integrations/gitlens/gitlensImpl.ts +++ b/src/integrations/gitlens/gitlensImpl.ts @@ -4,8 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { Disposable } from '../../common/lifecycle'; import { CreatePullRequestActionContext, GitLensApi } from './gitlens'; +import { Disposable } from '../../common/lifecycle'; export class GitLensIntegration extends Disposable { private _extensionsDisposable: vscode.Disposable; diff --git a/src/issues/currentIssue.ts b/src/issues/currentIssue.ts index f1dae8478a..2839991f30 100644 --- a/src/issues/currentIssue.ts +++ b/src/issues/currentIssue.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { IssueState, StateManager } from './stateManager'; import { Branch, Repository } from '../api/api'; import { GitErrorCodes } from '../api/api1'; import { Disposable } from '../common/lifecycle'; @@ -15,11 +16,11 @@ import { USE_BRANCH_FOR_ISSUES, WORKING_ISSUE_FORMAT_SCM, } from '../common/settingKeys'; +import { escapeRegExp } from '../common/utils'; import { FolderRepositoryManager, PullRequestDefaults } from '../github/folderRepositoryManager'; import { GithubItemStateEnum } from '../github/interface'; import { IssueModel } from '../github/issueModel'; import { variableSubstitution } from '../github/utils'; -import { IssueState, StateManager } from './stateManager'; export class CurrentIssue extends Disposable { private _branchName: string | undefined; @@ -150,7 +151,7 @@ export class CurrentIssue extends Disposable { private async getUser(): Promise { if (!this.user) { - this.user = await this.issueModel.githubRepository.getAuthenticatedUser(); + this.user = (await this.issueModel.githubRepository.getAuthenticatedUser()).login; } return this.user; } @@ -212,13 +213,13 @@ export class CurrentIssue extends Disposable { } const state: IssueState = this.stateManager.getSavedIssueState(this.issueModel.number); this._branchName = this.shouldPromptForBranch ? undefined : state.branch; - const branchNameConfig = await variableSubstitution( + const branchNameConfig = variableSubstitution( await this.getBranchTitle(), this.issue, undefined, await this.getUser(), ); - const branchNameMatch = this._branchName?.match(new RegExp('^(' + branchNameConfig + ')(_)?(\\d*)')); + const branchNameMatch = this._branchName?.match(new RegExp('^(' + escapeRegExp(branchNameConfig) + ')(_)?(\\d*)')); if ((createBranchConfig === 'on')) { const branch = await this.getBranch(this._branchName!); if (!branch) { diff --git a/src/issues/issueCompletionProvider.ts b/src/issues/issueCompletionProvider.ts index 7d583c08df..63bd4a527d 100644 --- a/src/issues/issueCompletionProvider.ts +++ b/src/issues/issueCompletionProvider.ts @@ -10,17 +10,18 @@ import { ISSUES_SETTINGS_NAMESPACE, } from '../common/settingKeys'; import { fromNewIssueUri, Schemes } from '../common/uri'; +import { EXTENSION_ID } from '../constants'; +import { IssueQueryResult, StateManager } from './stateManager'; +import { + getRootUriFromScmInputUri, + isComment, +} from './util'; import { FolderRepositoryManager, PullRequestDefaults } from '../github/folderRepositoryManager'; import { IMilestone } from '../github/interface'; import { IssueModel } from '../github/issueModel'; import { issueMarkdown } from '../github/markdownUtils'; import { RepositoriesManager } from '../github/repositoriesManager'; import { getIssueNumberLabel, variableSubstitution } from '../github/utils'; -import { IssueQueryResult, StateManager } from './stateManager'; -import { - getRootUriFromScmInputUri, - isComment, -} from './util'; class IssueCompletionItem extends vscode.CompletionItem { constructor(public readonly issue: IssueModel) { @@ -28,6 +29,20 @@ class IssueCompletionItem extends vscode.CompletionItem { } } +class ConfigureIssueQueriesCompletionItem extends vscode.CompletionItem { + constructor() { + super(vscode.l10n.t('Configure issue queries...'), vscode.CompletionItemKind.Text); + this.detail = vscode.l10n.t('No issues found. Set up queries to see relevant issues.'); + this.insertText = ''; + this.command = { + command: 'workbench.action.openSettings', + title: vscode.l10n.t('Open Settings'), + arguments: [`@ext:${EXTENSION_ID} githubIssues.queries`] + }; + this.sortText = '~'; // Sort to bottom of list + } +} + export class IssueCompletionProvider implements vscode.CompletionItemProvider { constructor( private stateManager: StateManager, @@ -85,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 []; } @@ -115,7 +132,8 @@ export class IssueCompletionProvider implements vscode.CompletionItemProvider { } } - const completionItems: Map = new Map(); + const completionItems: IssueCompletionItem[] = []; + const seenIssues: Set = new Set(); let repo: PullRequestDefaults | undefined; let uri: vscode.Uri | undefined; if (document.languageId === 'scminput') { @@ -146,7 +164,9 @@ export class IssueCompletionProvider implements vscode.CompletionItemProvider { // leave repo undefined } const issueData = this.stateManager.getIssueCollection(folderManager?.repository.rootUri ?? uri); + let sortNumber = 0; + // Process queries in order to maintain query priority for (const issueQuery of issueData) { const issuesOrMilestones: IssueQueryResult = await issueQuery[1]; if ((issuesOrMilestones.issues ?? []).length === 0) { @@ -156,14 +176,25 @@ export class IssueCompletionProvider implements vscode.CompletionItemProvider { if (filterOwnerAndRepo && ((issue as IssueModel).remote.owner !== filterOwnerAndRepo.owner || (issue as IssueModel).remote.repositoryName !== filterOwnerAndRepo.repo)) { continue; } - completionItems.set( - getIssueNumberLabel(issue as IssueModel), - await this.completionItemFromIssue(repo, issue as IssueModel, range, document), - ); + const issueKey = getIssueNumberLabel(issue as IssueModel); + // Only add the issue if we haven't seen it before (first query wins) + if (!seenIssues.has(issueKey)) { + seenIssues.add(issueKey); + const completionItem = await this.completionItemFromIssue(repo, issue as IssueModel, range, document); + // Ensure that the sort order respects the query order + completionItem.sortText = sortNumber.toString().padStart(8, '0'); + sortNumber++; + completionItems.push(completionItem); + } } + } + // If no issues were found, show a configuration prompt + if (completionItems.length === 0) { + return [new ConfigureIssueQueriesCompletionItem()]; } - return [...completionItems.values()]; + + return completionItems; } private async completionItemFromIssue( @@ -181,7 +212,7 @@ export class IssueCompletionProvider implements vscode.CompletionItemProvider { .getConfiguration(ISSUES_SETTINGS_NAMESPACE) .get(ISSUE_COMPLETION_FORMAT_SCM); if (document.uri.path.match(/git\/scm\d\/input/) && typeof configuration === 'string') { - item.insertText = await variableSubstitution(configuration, issue, repo); + item.insertText = variableSubstitution(configuration, issue, repo); } else { item.insertText = `${getIssueNumberLabel(issue, repo)}`; } @@ -189,7 +220,6 @@ export class IssueCompletionProvider implements vscode.CompletionItemProvider { item.documentation = issue.body; item.range = range; item.detail = milestone ? milestone.title : issue.milestone?.title; - item.sortText = `${new Date(issue.updatedAt).getTime()}`; item.filterText = `${item.detail} # ${issue.number} ${issue.title} ${item.documentation}`; return item; } diff --git a/src/issues/issueFeatureRegistrar.ts b/src/issues/issueFeatureRegistrar.ts index 2faf677674..538e6418d5 100644 --- a/src/issues/issueFeatureRegistrar.ts +++ b/src/issues/issueFeatureRegistrar.ts @@ -5,8 +5,11 @@ import { basename } from 'path'; import * as vscode from 'vscode'; +import { CurrentIssue } from './currentIssue'; +import { IssueCompletionProvider } from './issueCompletionProvider'; import { Remote } from '../api/api'; import { GitApiImpl } from '../api/api1'; +import { COPILOT_ACCOUNTS } from '../common/comment'; import { commands } from '../common/executeCommands'; import { Disposable } from '../common/lifecycle'; import Logger from '../common/logger'; @@ -16,23 +19,12 @@ import { ENABLED, ISSUE_COMPLETIONS, ISSUES_SETTINGS_NAMESPACE, - QUERIES, USER_COMPLETIONS, } from '../common/settingKeys'; +import { editQuery } from '../common/settingsUtils'; import { ITelemetry } from '../common/telemetry'; import { fromRepoUri, RepoUriParams, Schemes, toNewIssueUri } from '../common/uri'; -import { OctokitCommon } from '../github/common'; -import { FolderRepositoryManager, PullRequestDefaults } from '../github/folderRepositoryManager'; -import { IProject } from '../github/interface'; -import { IssueModel } from '../github/issueModel'; -import { RepositoriesManager } from '../github/repositoriesManager'; -import { ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput } from '../github/utils'; -import { chatCommand } from '../lm/utils'; -import { ReviewManager } from '../view/reviewManager'; -import { ReviewsManager } from '../view/reviewsManager'; -import { PRNode } from '../view/treeNodes/pullRequestNode'; -import { CurrentIssue } from './currentIssue'; -import { IssueCompletionProvider } from './issueCompletionProvider'; +import { EXTENSION_ID } from '../constants'; import { ASSIGNEES, extractMetadataFromFile, @@ -65,12 +57,24 @@ import { pushAndCreatePR, USER_EXPRESSION, } from './util'; +import { truncate } from '../common/utils'; +import { OctokitCommon } from '../github/common'; +import { CopilotRemoteAgentManager } from '../github/copilotRemoteAgent'; +import { FolderRepositoryManager, PullRequestDefaults } from '../github/folderRepositoryManager'; +import { IProject } from '../github/interface'; +import { IssueModel } from '../github/issueModel'; +import { IssueOverviewPanel } from '../github/issueOverview'; +import { RepositoriesManager } from '../github/repositoriesManager'; +import { ISSUE_OR_URL_EXPRESSION, parseIssueExpressionOutput } from '../github/utils'; +import { chatCommand } from '../lm/utils'; +import { ReviewManager } from '../view/reviewManager'; +import { ReviewsManager } from '../view/reviewsManager'; +import { PRNode } from '../view/treeNodes/pullRequestNode'; const CREATING_ISSUE_FROM_FILE_CONTEXT = 'issues.creatingFromFile'; export class IssueFeatureRegistrar extends Disposable { private static readonly ID = 'IssueFeatureRegistrar'; - private _stateManager: StateManager; private _newIssueCache: NewIssueCache; private createIssueInfo: @@ -88,9 +92,10 @@ export class IssueFeatureRegistrar extends Disposable { private reviewsManager: ReviewsManager, private context: vscode.ExtensionContext, private telemetry: ITelemetry, + private readonly _stateManager: StateManager, + private copilotRemoteAgentManager: CopilotRemoteAgentManager, ) { super(); - this._stateManager = new StateManager(gitAPI, this.manager, this.context); this._newIssueCache = new NewIssueCache(context); } @@ -137,6 +142,32 @@ export class IssueFeatureRegistrar extends Disposable { this, ), ); + this._register( + vscode.commands.registerCommand( + 'issue.startCodingAgentFromTodo', + (todoInfo?: { document: vscode.TextDocument; lineNumber: number; line: string; insertIndex: number; range: vscode.Range }) => { + /* __GDPR__ + "issue.startCodingAgentFromTodo" : {} + */ + this.telemetry.sendTelemetryEvent('issue.startCodingAgentFromTodo'); + return this.startCodingAgentFromTodo(todoInfo); + }, + this, + ), + ); + this._register( + vscode.commands.registerCommand( + 'issue.assignToCodingAgent', + (issueModel: any) => { + /* __GDPR__ + "issue.assignToCodingAgent" : {} + */ + this.telemetry.sendTelemetryEvent('issue.assignToCodingAgent'); + return this.assignToCodingAgent(issueModel); + }, + this, + ), + ); this._register( vscode.commands.registerCommand( 'issue.copyGithubPermalink', @@ -433,7 +464,7 @@ export class IssueFeatureRegistrar extends Disposable { "issue.editQuery" : {} */ this.telemetry.sendTelemetryEvent('issue.editQuery'); - return this.editQuery(query); + return editQuery(ISSUES_SETTINGS_NAMESPACE, query.queryLabel); }, this, ), @@ -507,7 +538,7 @@ export class IssueFeatureRegistrar extends Disposable { } else { const pullRequestModel = issue.pullRequestModel; const remote = pullRequestModel.githubRepository.remote; - commands.executeCommand(chatCommandID, vscode.l10n.t('@githubpr Summarize PR {0}/{1}#{2}', remote.owner, remote.repositoryName, pullRequestModel.number)); + commands.executeCommand(chatCommandID, vscode.l10n.t('@githubpr Summarize pull request {0}/{1}#{2}', remote.owner, remote.repositoryName, pullRequestModel.number)); } }), ); @@ -523,6 +554,15 @@ export class IssueFeatureRegistrar extends Disposable { commands.executeCommand(chatCommandID, vscode.l10n.t('@githubpr Find a fix for issue {0}/{1}#{2}', issue.remote.owner, issue.remote.repositoryName, issue.number)); }), ); + this._register(vscode.commands.registerCommand('issues.configureIssuesViewlet', async () => { + /* __GDPR__ + "issues.configureIssuesViewlet" : {} + */ + return vscode.commands.executeCommand( + 'workbench.action.openSettings', + `@ext:${EXTENSION_ID} issues`, + ); + })); this._stateManager.tryInitializeAndWait().then(() => { this.registerCompletionProviders(); @@ -535,8 +575,12 @@ export class IssueFeatureRegistrar extends Disposable { this._register( vscode.languages.registerHoverProvider('*', new UserHoverProvider(this.manager, this.telemetry)), ); + const todoProvider = new IssueTodoProvider(this.context, this.copilotRemoteAgentManager); this._register( - vscode.languages.registerCodeActionsProvider('*', new IssueTodoProvider(this.context), { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }), + vscode.languages.registerCodeActionsProvider('*', todoProvider, { providedCodeActionKinds: [vscode.CodeActionKind.QuickFix] }), + ); + this._register( + vscode.languages.registerCodeLensProvider('*', todoProvider), ); }); } @@ -726,32 +770,6 @@ export class IssueFeatureRegistrar extends Disposable { } } - async editQuery(query: QueryNode) { - const config = vscode.workspace.getConfiguration(ISSUES_SETTINGS_NAMESPACE, null); - const inspect = config.inspect<{ label: string; query: string }[]>(QUERIES); - let command: string; - if (inspect?.workspaceValue) { - command = 'workbench.action.openWorkspaceSettingsFile'; - } else { - const value = config.get<{ label: string; query: string }[]>(QUERIES); - if (inspect?.defaultValue && JSON.stringify(inspect?.defaultValue) === JSON.stringify(value)) { - config.update(QUERIES, inspect.defaultValue, vscode.ConfigurationTarget.Global); - } - command = 'workbench.action.openSettingsJson'; - } - await vscode.commands.executeCommand(command); - const editor = vscode.window.activeTextEditor; - if (editor) { - const text = editor.document.getText(); - const search = text.search(query.queryLabel); - if (search >= 0) { - const position = editor.document.positionAt(search); - editor.revealRange(new vscode.Range(position, position)); - editor.selection = new vscode.Selection(position, position); - } - } - } - getCurrent() { // This is used by the "api" command issues.getCurrent const currentIssues = this._stateManager.currentIssues(); @@ -1280,14 +1298,18 @@ ${options?.body ?? ''}\n folderManager = await this.chooseRepo(vscode.l10n.t('Choose where to create the issue.')); } + const assigneesWithoutCopilot = assignees?.filter(assignee => !COPILOT_ACCOUNTS[assignee]); + const copilotAssignee = !!assignees?.find(assignee => COPILOT_ACCOUNTS[assignee]); + return vscode.window.withProgress({ location: vscode.ProgressLocation.Notification, title: vscode.l10n.t('Creating issue') }, async (progress) => { if (!folderManager) { return false; } + const constFolderManager: FolderRepositoryManager = folderManager; progress.report({ message: vscode.l10n.t('Verifying that issue data is valid...') }); try { if (!origin) { - origin = await folderManager.getPullRequestDefaults(); + origin = await constFolderManager.getPullRequestDefaults(); } } catch (e) { // There is no remote @@ -1303,17 +1325,23 @@ ${options?.body ?? ''}\n repo: origin.repo, title, body, - assignees, + assignees: assigneesWithoutCopilot, labels, milestone }; - if (!(await this.verifyLabels(folderManager, createParams))) { + if (!(await this.verifyLabels(constFolderManager, createParams))) { return false; } progress.report({ message: vscode.l10n.t('Creating issue in {0}...', `${createParams.owner}/${createParams.repo}`) }); - const issue = await folderManager.createIssue(createParams); + const issue = await constFolderManager.createIssue(createParams); if (issue) { + if (copilotAssignee) { + const copilotUser = (await folderManager.getAssignableUsers())[issue.remote.remoteName].find(user => COPILOT_ACCOUNTS[user.login]); + if (copilotUser) { + await issue.replaceAssignees([...(issue.assignees ?? []), copilotUser]); + } + } if (projects) { await issue.updateProjects(projects); } @@ -1328,14 +1356,14 @@ ${options?.body ?? ''}\n await vscode.workspace.applyEdit(edit); } else { const copyIssueUrl = vscode.l10n.t('Copy Issue Link'); - const openIssue = vscode.l10n.t({ message: 'Open Issue', comment: 'Open the issue description in the browser to see it\'s full contents.' }); + const openIssue = vscode.l10n.t({ message: 'Open Issue', comment: 'Open the issue description in the editor to see it\'s full contents.' }); vscode.window.showInformationMessage(vscode.l10n.t('Issue created'), copyIssueUrl, openIssue).then(async result => { switch (result) { case copyIssueUrl: await vscode.env.clipboard.writeText(issue.html_url); break; case openIssue: - await vscode.env.openExternal(vscode.Uri.parse(issue.html_url)); + await IssueOverviewPanel.createOrShow(this.telemetry, this.context.extensionUri, constFolderManager, issue); break; } }); @@ -1457,4 +1485,68 @@ ${options?.body ?? ''}\n } return undefined; } + + async startCodingAgentFromTodo(todoInfo?: { document: vscode.TextDocument; lineNumber: number; line: string; insertIndex: number; range: vscode.Range }) { + if (!todoInfo) { + return; + } + + const { document, line, insertIndex } = todoInfo; + const todoText = line.substring(insertIndex).trim(); + if (!todoText) { + vscode.window.showWarningMessage(vscode.l10n.t('No task description found in TODO comment')); + return; + } + + const relativePath = vscode.workspace.asRelativePath(document.uri); + const prompt = vscode.l10n.t('Work on TODO: {0} (from {1})', todoText, relativePath); + return vscode.window.withProgress({ + location: vscode.ProgressLocation.Notification, + title: vscode.l10n.t('Delegating \'{0}\' to coding agent', truncate(todoText, 20)) + }, async (_progress) => { + try { + await this.copilotRemoteAgentManager.commandImpl({ + userPrompt: prompt, + source: 'todo' + }); + } catch (error) { + vscode.window.showErrorMessage(vscode.l10n.t('Failed to start coding agent session: {0}', error.message)); + } + }); + } + + async assignToCodingAgent(issueModel: any) { + if (!issueModel) { + return; + } + + // Check if the issue model is an IssueModel + if (!(issueModel instanceof IssueModel)) { + return; + } + + try { + // Get the folder manager for this issue + const folderManager = this.manager.getManagerForIssueModel(issueModel); + if (!folderManager) { + vscode.window.showErrorMessage(vscode.l10n.t('Failed to find repository for issue #{0}', issueModel.number)); + return; + } + + // Get assignable users and find the copilot user + const assignableUsers = await folderManager.getAssignableUsers(); + const copilotUser = assignableUsers[issueModel.remote.remoteName]?.find(user => COPILOT_ACCOUNTS[user.login]); + + if (!copilotUser) { + vscode.window.showErrorMessage(vscode.l10n.t('Copilot coding agent is not available for assignment in this repository')); + return; + } + + // Assign the issue to the copilot user + await issueModel.replaceAssignees([...(issueModel.assignees ?? []), copilotUser]); + vscode.window.showInformationMessage(vscode.l10n.t('Issue #{0} has been assigned to Copilot coding agent', issueModel.number)); + } catch (error) { + vscode.window.showErrorMessage(vscode.l10n.t('Failed to assign issue to coding agent: {0}', error.message)); + } + } } diff --git a/src/issues/issueHoverProvider.ts b/src/issues/issueHoverProvider.ts index 0e8e677e8f..5a33c43f00 100644 --- a/src/issues/issueHoverProvider.ts +++ b/src/issues/issueHoverProvider.ts @@ -4,16 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { ITelemetry } from '../common/telemetry'; -import { FolderRepositoryManager } from '../github/folderRepositoryManager'; -import { issueMarkdown } from '../github/markdownUtils'; -import { RepositoriesManager } from '../github/repositoriesManager'; -import { ISSUE_OR_URL_EXPRESSION, ParsedIssue, parseIssueExpressionOutput } from '../github/utils'; import { StateManager } from './stateManager'; import { getIssue, shouldShowHover, } from './util'; +import { ITelemetry } from '../common/telemetry'; +import { FolderRepositoryManager } from '../github/folderRepositoryManager'; +import { issueMarkdown } from '../github/markdownUtils'; +import { RepositoriesManager } from '../github/repositoriesManager'; +import { ISSUE_OR_URL_EXPRESSION, ParsedIssue, parseIssueExpressionOutput } from '../github/utils'; export class IssueHoverProvider implements vscode.HoverProvider { constructor( diff --git a/src/issues/issueLinkProvider.ts b/src/issues/issueLinkProvider.ts deleted file mode 100644 index 3410d1a837..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 { EDITOR, WORD_WRAP } from '../common/settingKeys'; -import { ReposManagerState } from '../github/folderRepositoryManager'; -import { RepositoriesManager } from '../github/repositoriesManager'; -import { ISSUE_EXPRESSION, ParsedIssue, parseIssueExpressionOutput } from '../github/utils'; -import { StateManager } from './stateManager'; -import { - getIssue, - isComment, - MAX_LINE_LENGTH, -} from './util'; - -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 8d3717d4f3..383e6007aa 100644 --- a/src/issues/issueTodoProvider.ts +++ b/src/issues/issueTodoProvider.ts @@ -4,18 +4,19 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { CREATE_ISSUE_TRIGGERS, ISSUES_SETTINGS_NAMESPACE } from '../common/settingKeys'; +import { isComment, MAX_LINE_LENGTH } from './util'; +import { CODING_AGENT, CREATE_ISSUE_TRIGGERS, ISSUES_SETTINGS_NAMESPACE, SHOW_CODE_LENS } from '../common/settingKeys'; +import { escapeRegExp } from '../common/utils'; +import { CopilotRemoteAgentManager } from '../github/copilotRemoteAgent'; import { ISSUE_OR_URL_EXPRESSION } from '../github/utils'; -import { MAX_LINE_LENGTH } from './util'; -function escapeRegExp(string: string) { - return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); -} - -export class IssueTodoProvider implements vscode.CodeActionProvider { +export class IssueTodoProvider implements vscode.CodeActionProvider, vscode.CodeLensProvider { private expression: RegExp | undefined; - constructor(context: vscode.ExtensionContext) { + constructor( + context: vscode.ExtensionContext, + private copilotRemoteAgentManager: CopilotRemoteAgentManager + ) { context.subscriptions.push( vscode.workspace.onDidChangeConfiguration(() => { this.updateTriggers(); @@ -29,6 +30,24 @@ export class IssueTodoProvider implements vscode.CodeActionProvider { this.expression = triggers.length > 0 ? new RegExp(triggers.map(trigger => escapeRegExp(trigger)).join('|')) : undefined; } + private findTodoInLine(line: string): { match: RegExpMatchArray; search: number; insertIndex: number } | undefined { + const truncatedLine = line.substring(0, MAX_LINE_LENGTH); + const matches = truncatedLine.match(ISSUE_OR_URL_EXPRESSION); + if (matches) { + return undefined; + } + const match = truncatedLine.match(this.expression!); + const search = match?.index ?? -1; + if (search >= 0 && match) { + const indexOfWhiteSpace = truncatedLine.substring(search).search(/\s/); + const insertIndex = + search + + (indexOfWhiteSpace > 0 ? indexOfWhiteSpace : truncatedLine.match(this.expression!)![0].length); + return { match, search, insertIndex }; + } + return undefined; + } + async provideCodeActions( document: vscode.TextDocument, range: vscode.Range | vscode.Selection, @@ -42,32 +61,79 @@ export class IssueTodoProvider implements vscode.CodeActionProvider { let lineNumber = range.start.line; do { const line = document.lineAt(lineNumber).text; - const truncatedLine = line.substring(0, MAX_LINE_LENGTH); - const matches = truncatedLine.match(ISSUE_OR_URL_EXPRESSION); - if (!matches) { - const match = truncatedLine.match(this.expression); - const search = match?.index ?? -1; - if (search >= 0 && match) { - const codeAction: vscode.CodeAction = new vscode.CodeAction( - vscode.l10n.t('Create GitHub Issue'), + const todoInfo = this.findTodoInLine(line); + if (todoInfo) { + const { match, search, insertIndex } = todoInfo; + // Create GitHub Issue action + const createIssueAction: vscode.CodeAction = new vscode.CodeAction( + vscode.l10n.t('Create GitHub Issue'), + vscode.CodeActionKind.QuickFix, + ); + createIssueAction.ranges = [new vscode.Range(lineNumber, search, lineNumber, search + match[0].length)]; + createIssueAction.command = { + title: vscode.l10n.t('Create GitHub Issue'), + command: 'issue.createIssueFromSelection', + arguments: [{ document, lineNumber, line, insertIndex, range }], + }; + codeActions.push(createIssueAction); + + // Start Coding Agent Session action (if copilot manager is available) + if (this.copilotRemoteAgentManager) { + const startAgentAction: vscode.CodeAction = new vscode.CodeAction( + vscode.l10n.t('Delegate to agent'), vscode.CodeActionKind.QuickFix, ); - codeAction.ranges = [new vscode.Range(lineNumber, search, lineNumber, search + match[0].length)]; - const indexOfWhiteSpace = truncatedLine.substring(search).search(/\s/); - const insertIndex = - search + - (indexOfWhiteSpace > 0 ? indexOfWhiteSpace : truncatedLine.match(this.expression)![0].length); - codeAction.command = { - title: vscode.l10n.t('Create GitHub Issue'), - command: 'issue.createIssueFromSelection', + startAgentAction.ranges = [new vscode.Range(lineNumber, search, lineNumber, search + match[0].length)]; + startAgentAction.command = { + title: vscode.l10n.t('Delegate to agent'), + command: 'issue.startCodingAgentFromTodo', arguments: [{ document, lineNumber, line, insertIndex, range }], }; - codeActions.push(codeAction); - break; + codeActions.push(startAgentAction); } + break; } lineNumber++; } while (range.end.line >= lineNumber); return codeActions; } + + async provideCodeLenses( + document: vscode.TextDocument, + _token: vscode.CancellationToken, + ): Promise { + if (this.expression === undefined) { + return []; + } + + // Check if CodeLens is enabled + const isCodeLensEnabled = vscode.workspace.getConfiguration(CODING_AGENT).get(SHOW_CODE_LENS, true); + if (!isCodeLensEnabled) { + return []; + } + + const codeLenses: vscode.CodeLens[] = []; + for (let lineNumber = 0; lineNumber < document.lineCount; lineNumber++) { + const textLine = document.lineAt(lineNumber); + const { text: line, firstNonWhitespaceCharacterIndex } = textLine; + const todoInfo = this.findTodoInLine(line); + if (!todoInfo) { + continue; + } + if (!(await isComment(document, new vscode.Position(lineNumber, firstNonWhitespaceCharacterIndex)))) { + continue; + } + const { match, search, insertIndex } = todoInfo; + const range = new vscode.Range(lineNumber, search, lineNumber, search + match[0].length); + if (this.copilotRemoteAgentManager && (await this.copilotRemoteAgentManager.isAvailable())) { + const startAgentCodeLens = new vscode.CodeLens(range, { + title: vscode.l10n.t('Delegate to agent'), + command: 'issue.startCodingAgentFromTodo', + arguments: [{ document, lineNumber, line, insertIndex, range }], + }); + codeLenses.push(startAgentCodeLens); + } + } + return codeLenses; + } } diff --git a/src/issues/issuesView.ts b/src/issues/issuesView.ts index 41dec7c4cf..d839c2d6e8 100644 --- a/src/issues/issuesView.ts +++ b/src/issues/issuesView.ts @@ -5,15 +5,17 @@ import * as path from 'path'; import * as vscode from 'vscode'; +import { issueBodyHasLink } from './issueLinkLookup'; +import { IssueItem, QueryGroup, StateManager } from './stateManager'; import { commands, contexts } from '../common/executeCommands'; +import { ISSUE_AVATAR_DISPLAY, ISSUES_SETTINGS_NAMESPACE } from '../common/settingKeys'; import { DataUri } from '../common/uri'; import { groupBy } from '../common/utils'; import { FolderRepositoryManager, ReposManagerState } from '../github/folderRepositoryManager'; +import { IAccount } from '../github/interface'; import { IssueModel } from '../github/issueModel'; import { issueMarkdown } from '../github/markdownUtils'; import { RepositoriesManager } from '../github/repositoriesManager'; -import { issueBodyHasLink } from './issueLinkLookup'; -import { IssueItem, QueryGroup, StateManager } from './stateManager'; export class QueryNode { constructor( @@ -59,6 +61,15 @@ export class IssuesTreeData this._onDidChangeTreeData.fire(); }), ); + + // Listen for changes to the avatar display setting + context.subscriptions.push( + vscode.workspace.onDidChangeConfiguration(change => { + if (change.affectsConfiguration(`${ISSUES_SETTINGS_NAMESPACE}.${ISSUE_AVATAR_DISPLAY}`)) { + this._onDidChangeTreeData.fire(); + } + }), + ); } private getFolderRepoItem(element: FolderRepositoryManager): vscode.TreeItem { @@ -77,10 +88,27 @@ export class IssuesTreeData private async getIssueTreeItem(element: IssueItem): Promise { const treeItem = new vscode.TreeItem(element.title, vscode.TreeItemCollapsibleState.None); - treeItem.iconPath = (await DataUri.avatarCirclesAsImageDataUris(this.context, [element.author], 16, 16))[0] ?? - (element.isOpen - ? new vscode.ThemeIcon('issues', new vscode.ThemeColor('issues.open')) - : new vscode.ThemeIcon('issue-closed', new vscode.ThemeColor('issues.closed'))); + + const avatarDisplaySetting = vscode.workspace + .getConfiguration(ISSUES_SETTINGS_NAMESPACE, null) + .get<'author' | 'assignee'>(ISSUE_AVATAR_DISPLAY, 'author'); + + let avatarUser: IAccount | undefined; + if ((avatarDisplaySetting === 'assignee') && element.assignees && (element.assignees.length > 0)) { + avatarUser = element.assignees[0]; + } else if (avatarDisplaySetting === 'author') { + avatarUser = element.author; + } + + if (avatarUser) { + treeItem.iconPath = (await DataUri.avatarCirclesAsImageDataUris(this.context, [avatarUser], 16, 16))[0] ?? + (element.isOpen + ? new vscode.ThemeIcon('issues', new vscode.ThemeColor('issues.open')) + : new vscode.ThemeIcon('issue-closed', new vscode.ThemeColor('issues.closed'))); + } else { + // Use GitHub codicon when assignee setting is selected but no assignees exist + treeItem.iconPath = new vscode.ThemeIcon('github'); + } treeItem.command = { command: 'issue.openDescription', @@ -89,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); diff --git a/src/issues/shareProviders.ts b/src/issues/shareProviders.ts index 21fbe163ef..2b98217004 100644 --- a/src/issues/shareProviders.ts +++ b/src/issues/shareProviders.ts @@ -5,6 +5,7 @@ import * as pathLib from 'path'; import * as vscode from 'vscode'; +import { encodeURIComponentExceptSlashes, getBestPossibleUpstream, getOwnerAndRepo, getSimpleUpstream, getUpstreamOrigin, rangeString } from './util'; import { Commit, Remote, Repository } from '../api/api'; import { GitApiImpl } from '../api/api1'; import { Disposable, disposeAll } from '../common/lifecycle'; @@ -12,7 +13,6 @@ import Logger from '../common/logger'; import { fromReviewUri, Schemes } from '../common/uri'; import { FolderRepositoryManager } from '../github/folderRepositoryManager'; import { RepositoriesManager } from '../github/repositoriesManager'; -import { encodeURIComponentExceptSlashes, getBestPossibleUpstream, getOwnerAndRepo, getSimpleUpstream, getUpstreamOrigin, rangeString } from './util'; export class ShareProviderManager extends Disposable { diff --git a/src/issues/stateManager.ts b/src/issues/stateManager.ts index 6f8ebd1e30..b02b45ad07 100644 --- a/src/issues/stateManager.ts +++ b/src/issues/stateManager.ts @@ -5,6 +5,7 @@ import LRUCache from 'lru-cache'; import * as vscode from 'vscode'; +import { CurrentIssue } from './currentIssue'; import { Repository } from '../api/api'; import { GitApiImpl } from '../api/api1'; import { AuthProvider } from '../common/authentication'; @@ -25,7 +26,6 @@ import { IAccount } from '../github/interface'; import { IssueModel } from '../github/issueModel'; import { RepositoriesManager } from '../github/repositoriesManager'; import { getIssueNumberLabel, variableSubstitution } from '../github/utils'; -import { CurrentIssue } from './currentIssue'; const CURRENT_ISSUE_KEY = 'currentIssue'; @@ -99,13 +99,16 @@ export class StateManager { private context: vscode.ExtensionContext, ) { } - private getOrCreateSingleRepoState(uri: vscode.Uri, folderManager?: FolderRepositoryManager): SingleRepoState { + private getOrCreateSingleRepoState(uri: vscode.Uri, folderManager?: FolderRepositoryManager): SingleRepoState | undefined { let state = this._singleRepoStates.get(uri.path); if (state) { return state; } if (!folderManager) { - folderManager = this.manager.getManagerForFile(uri)!; + folderManager = this.manager.getManagerForFile(uri); + } + if (!folderManager) { + return undefined; } state = { issueCollection: new Map(), @@ -147,6 +150,9 @@ export class StateManager { private registerRepositoryChangeEvent() { async function updateRepository(that: StateManager, repository: Repository) { const state = that.getOrCreateSingleRepoState(repository.rootUri); + if (!state) { + return; + } // setIssueData can cause the last head and branch state to change. Capture them before that can happen. const oldHead = state.lastHead; const oldBranch = state.lastBranch; @@ -165,7 +171,7 @@ export class StateManager { await that.setCurrentIssueFromBranch(state, newBranch, true); } } else { - await that.setCurrentIssue(state, undefined, true); + await that.setCurrentIssue(state, undefined, !!newBranch); } } state.lastHead = repository.state.HEAD ? repository.state.HEAD.commit : undefined; @@ -233,16 +239,20 @@ export class StateManager { this.context.subscriptions.push(folderManager.onDidChangeRepositories(async (e) => { if (e.added) { const state = this.getOrCreateSingleRepoState(folderManager.repository.rootUri); - if ((state.issueCollection.size === 0) || (await Promise.all(state.issueCollection.values())).some(collection => collection.issues === undefined)) { + + if (state && ((state.issueCollection.size === 0) || (await Promise.all(state.issueCollection.values())).some(collection => collection.issues === undefined))) { this.refresh(folderManager); } } })); - const singleRepoState: SingleRepoState = this.getOrCreateSingleRepoState( + const singleRepoState: SingleRepoState | undefined = this.getOrCreateSingleRepoState( folderManager.repository.rootUri, folderManager, ); + if (!singleRepoState) { + continue; + } singleRepoState.lastHead = folderManager.repository.state.HEAD ? folderManager.repository.state.HEAD.commit : undefined; @@ -281,10 +291,10 @@ export class StateManager { } async getUserMap(uri: vscode.Uri): Promise> { - if (!this.initializePromise) { + const state = this.getOrCreateSingleRepoState(uri); + if (!this.initializePromise || !state) { return Promise.resolve(new Map()); } - const state = this.getOrCreateSingleRepoState(uri); if (!state.userMap || (await state.userMap).size === 0) { state.userMap = this.getUsers(uri); } @@ -301,6 +311,9 @@ export class StateManager { private async setIssueData(folderManager: FolderRepositoryManager) { const singleRepoState = this.getOrCreateSingleRepoState(folderManager.repository.rootUri, folderManager); + if (!singleRepoState) { + return; + } singleRepoState.issueCollection.clear(); const enterpriseRemotes = parseRepositoryRemotes(folderManager.repository).filter( remote => remote.isEnterprise @@ -316,7 +329,7 @@ export class StateManager { items = this.setIssues( folderManager, // Do not resolve pull request defaults as they will get resolved in the query later per repository - await variableSubstitution(query.query, undefined, undefined, user), + variableSubstitution(query.query, undefined, undefined, user), ).then(issues => ({ groupBy: query.groupBy ?? [], issues })); if (items) { @@ -330,7 +343,7 @@ export class StateManager { private setIssues(folderManager: FolderRepositoryManager, query: string): Promise { return new Promise(async resolve => { - const issues = await folderManager.getIssues(query); + const issues = await folderManager.getIssues(query, { fetchNextPage: false, fetchOnePagePerRepo: true }); this._onDidChangeIssueData.fire(); resolve( issues?.items.map(item => { @@ -418,8 +431,12 @@ export class StateManager { if (repoState.currentIssue && issue?.issue.number === repoState.currentIssue.issue.number) { return; } + // Check if branch management is disabled + const createBranchConfig = vscode.workspace.getConfiguration(ISSUES_SETTINGS_NAMESPACE).get(USE_BRANCH_FOR_ISSUES); + const shouldCheckoutDefaultBranch = createBranchConfig === 'off' ? false : checkoutDefaultBranch; + if (repoState.currentIssue) { - await repoState.currentIssue.stopWorking(checkoutDefaultBranch); + await repoState.currentIssue.stopWorking(shouldCheckoutDefaultBranch); } if (issue) { this.context.subscriptions.push(issue.onDidChangeCurrentIssueState(() => this.updateStatusBar())); diff --git a/src/issues/userCompletionProvider.ts b/src/issues/userCompletionProvider.ts index 92400ca155..fbd5413cfd 100644 --- a/src/issues/userCompletionProvider.ts +++ b/src/issues/userCompletionProvider.ts @@ -11,14 +11,14 @@ import { TimelineEvent } from '../common/timelineEvent'; import { fromNewIssueUri, fromPRUri, Schemes } from '../common/uri'; import { compareIgnoreCase, isDescendant } from '../common/utils'; import { EXTENSION_ID } from '../constants'; +import { ASSIGNEES } from './issueFile'; +import { StateManager } from './stateManager'; +import { getRootUriFromScmInputUri, isComment, UserCompletion } from './util'; import { FolderRepositoryManager } from '../github/folderRepositoryManager'; import { IAccount, User } from '../github/interface'; import { userMarkdown } from '../github/markdownUtils'; import { RepositoriesManager } from '../github/repositoriesManager'; import { getRelatedUsersFromTimelineEvents } from '../github/utils'; -import { ASSIGNEES } from './issueFile'; -import { StateManager } from './stateManager'; -import { getRootUriFromScmInputUri, isComment, UserCompletion } from './util'; export class UserCompletionProvider implements vscode.CompletionItemProvider { private static readonly ID: string = 'UserCompletionProvider'; @@ -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 []; } @@ -96,7 +98,15 @@ export class UserCompletionProvider implements vscode.CompletionItemProvider { uri = getRootUriFromScmInputUri(document.uri); } else if (document.uri.scheme === Schemes.Comment) { const activeTab = vscode.window.tabGroups.activeTabGroup.activeTab?.input; - uri = activeTab instanceof vscode.TabInputText ? activeTab.uri : (activeTab instanceof vscode.TabInputTextDiff ? activeTab.modified : undefined); + if (activeTab instanceof vscode.TabInputText) { + uri = activeTab.uri; + } else if (activeTab instanceof vscode.TabInputTextDiff) { + uri = activeTab.modified; + } else if ((activeTab as { textDiffs?: { modified: vscode.Uri, original: vscode.Uri }[] }).textDiffs) { + uri = (activeTab as { textDiffs: { modified: vscode.Uri, original: vscode.Uri }[] }).textDiffs[0].modified; + } else { + uri = vscode.workspace.workspaceFolders ? vscode.workspace.workspaceFolders[0].uri : undefined; + } } if (!uri) { diff --git a/src/issues/userHoverProvider.ts b/src/issues/userHoverProvider.ts index 6dc94c405c..671e71510f 100644 --- a/src/issues/userHoverProvider.ts +++ b/src/issues/userHoverProvider.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { shouldShowHover, USER_EXPRESSION } from './util'; import { ITelemetry } from '../common/telemetry'; import { DOXYGEN_NON_USERS, JSDOC_NON_USERS, PHPDOC_NON_USERS } from '../common/user'; import { userMarkdown } from '../github/markdownUtils'; import { RepositoriesManager } from '../github/repositoriesManager'; -import { shouldShowHover, USER_EXPRESSION } from './util'; export class UserHoverProvider implements vscode.HoverProvider { constructor(private manager: RepositoriesManager, private telemetry: ITelemetry) { } diff --git a/src/issues/util.ts b/src/issues/util.ts index 82a07e8751..87a4e7e382 100644 --- a/src/issues/util.ts +++ b/src/issues/util.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { URL } from 'url'; import LRUCache from 'lru-cache'; import 'url-search-params-polyfill'; import * as vscode from 'vscode'; +import { StateManager } from './stateManager'; import { Ref, Remote, Repository, UpstreamRef } from '../api/api'; import { GitApiImpl } from '../api/api1'; import Logger from '../common/logger'; @@ -17,7 +17,6 @@ import { IssueModel } from '../github/issueModel'; import { RepositoriesManager } from '../github/repositoriesManager'; import { getEnterpriseUri, getRepositoryForFile, ISSUE_OR_URL_EXPRESSION, ParsedIssue, parseIssueExpressionOutput } from '../github/utils'; import { ReviewManager } from '../view/reviewManager'; -import { StateManager } from './stateManager'; export const USER_EXPRESSION: RegExp = /\@([^\s]+)/; @@ -183,11 +182,9 @@ async function getUpstream(repositoriesManager: RepositoriesManager, repository: function extractContext(context: LinkContext): { fileUri: vscode.Uri | undefined, lineNumber: number | undefined } { if (context instanceof vscode.Uri) { return { fileUri: context, lineNumber: undefined }; - } else if (context !== undefined && 'lineNumber' in context && 'uri' in context) { - return { fileUri: context.uri, lineNumber: context.lineNumber }; - } else { - return { fileUri: undefined, lineNumber: undefined }; } + const asEditorLineNumberContext = context as Partial | undefined; + return { fileUri: asEditorLineNumberContext?.uri, lineNumber: asEditorLineNumberContext?.lineNumber }; } function getFileAndPosition(context: LinkContext, positionInfo?: NewIssue): { uri: vscode.Uri | undefined, range: vscode.Range | vscode.NotebookRange | undefined } { @@ -323,7 +320,7 @@ export async function createSinglePermalink( if (!rawUpstream || !rawUpstream.fetchUrl) { return { permalink: undefined, error: vscode.l10n.t('The selection may not exist on any remote.'), originalFile: uri }; } - const upstream: Remote & { fetchUrl: string } = rawUpstream as any; + const upstream: Remote & { fetchUrl: string } = rawUpstream as Remote & { fetchUrl: string }; Logger.debug(`upstream: ${upstream.fetchUrl}`, PERMALINK_COMPONENT); @@ -365,22 +362,9 @@ export function getUpstreamOrigin(upstream: Remote, resultHost: string = 'github const enterpriseUri = getEnterpriseUri(); let fetchUrl = upstream.fetchUrl; if (enterpriseUri && fetchUrl) { - // upstream's origin by https - if (fetchUrl.startsWith('https://') && !fetchUrl.startsWith('https://github.com/')) { - const host = new URL(fetchUrl).host; - if (host.startsWith(enterpriseUri.authority) || !host.includes('github.com')) { - resultHost = enterpriseUri.authority; - } - } - if (fetchUrl.startsWith('ssh://')) { - fetchUrl = fetchUrl.substr('ssh://'.length); - } - // upstream's origin by ssh - if ((fetchUrl.startsWith('git@') || fetchUrl.includes('@git')) && !fetchUrl.startsWith('git@github.com')) { - const host = fetchUrl.split('@')[1]?.split(':')[0]; - if (host.startsWith(enterpriseUri.authority) || !host.includes('github.com')) { - resultHost = enterpriseUri.authority; - } + const protocol = new Protocol(fetchUrl); + if (protocol.host.startsWith(enterpriseUri.authority) || !protocol.host.includes('github.com')) { + resultHost = enterpriseUri.authority; } } return `https://${resultHost}`; @@ -533,12 +517,11 @@ export async function pushAndCreatePR( } export async function isComment(document: vscode.TextDocument, position: vscode.Position): Promise { - if (document.languageId !== 'markdown' && document.languageId !== 'plaintext') { - const tokenInfo = await vscode.languages.getTokenInformationAtPosition(document, position); - if (tokenInfo.type !== vscode.StandardTokenType.Comment) { - return false; - } + const tokenInfo = await vscode.languages.getTokenInformationAtPosition(document, position); + if (tokenInfo.type !== vscode.StandardTokenType.Comment) { + return false; } + return true; } diff --git a/src/lm/issueContextProvider.ts b/src/lm/issueContextProvider.ts new file mode 100644 index 0000000000..81796099bb --- /dev/null +++ b/src/lm/issueContextProvider.ts @@ -0,0 +1,87 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { CommentEvent, EventType } from '../common/timelineEvent'; +import { IssueModel } from '../github/issueModel'; +import { IssueOverviewPanel } from '../github/issueOverview'; +import { RepositoriesManager } from '../github/repositoriesManager'; +import { getIssueNumberLabel } from '../github/utils'; +import { IssueQueryResult, StateManager } from '../issues/stateManager'; + +interface IssueChatContextItem extends vscode.ChatContextItem { + issue: IssueModel; +} + +export class IssueContextProvider implements vscode.ChatContextProvider { + constructor(private readonly _stateManager: StateManager, + private readonly _reposManager: RepositoriesManager + ) { } + + async provideChatContextForResource(_options: { resource: vscode.Uri }, _token: vscode.CancellationToken): Promise { + const item = IssueOverviewPanel.currentPanel?.getCurrentItem(); + if (item) { + return this._issueToUnresolvedContext(item); + } + } + + async resolveChatContext(context: IssueChatContextItem, _token: vscode.CancellationToken): Promise { + context.value = await this._resolvedIssueValue(context.issue); + context.modelDescription = 'All the information about the GitHub issue the user is viewing, including comments.'; + return context; + } + + async provideChatContextExplicit(_token: vscode.CancellationToken): Promise { + const contextItems: IssueChatContextItem[] = []; + const seenIssues: Set = new Set(); + for (const folderManager of this._reposManager.folderManagers) { + const issueData = this._stateManager.getIssueCollection(folderManager?.repository.rootUri); + + for (const issueQuery of issueData) { + const issuesOrMilestones: IssueQueryResult = await issueQuery[1]; + + if ((issuesOrMilestones.issues ?? []).length === 0) { + continue; + } + for (const issue of (issuesOrMilestones.issues ?? [])) { + const issueKey = getIssueNumberLabel(issue as IssueModel); + // Only add the issue if we haven't seen it before (first query wins) + if (seenIssues.has(issueKey)) { + continue; + } + seenIssues.add(issueKey); + contextItems.push(this._issueToUnresolvedContext(issue as IssueModel)); + + } + } + } + return contextItems; + } + + private _issueToUnresolvedContext(issue: IssueModel): IssueChatContextItem { + return { + icon: new vscode.ThemeIcon('issues'), + label: `#${issue.number} ${issue.title}`, + modelDescription: 'The GitHub issue the user is viewing.', + issue, + }; + } + + private async _resolvedIssueValue(issue: IssueModel): Promise { + const timeline = issue.timelineEvents ?? await issue.getIssueTimelineEvents(); + return JSON.stringify({ + issueNumber: issue.number, + owner: issue.remote.owner, + repo: issue.remote.repositoryName, + title: issue.title, + body: issue.body, + comments: timeline.filter(e => e.event === EventType.Commented).map((e: CommentEvent) => ({ + author: e.user?.login, + body: e.body, + createdAt: e.createdAt + })) + }); + } +} \ No newline at end of file diff --git a/src/lm/participants.ts b/src/lm/participants.ts index 71ea59bb34..6f2e339940 100644 --- a/src/lm/participants.ts +++ b/src/lm/participants.ts @@ -6,8 +6,8 @@ 'use strict'; import { renderPrompt } from '@vscode/prompt-tsx'; import * as vscode from 'vscode'; -import { Disposable } from '../common/lifecycle'; import { ParticipantsPrompt } from './participantsPrompt'; +import { Disposable } from '../common/lifecycle'; import { IToolCall, TOOL_COMMAND_RESULT, TOOL_MARKDOWN_RESULT } from './tools/toolsUtils'; export class ChatParticipantState { @@ -36,6 +36,7 @@ export class ChatParticipantState { } } } + return undefined; } get messages(): vscode.LanguageModelChatMessage[] { @@ -81,13 +82,16 @@ export class ChatParticipant extends Disposable { family: 'gpt-4o' }); const model = models[0]; - const allTools = vscode.lm.tools.map((tool): vscode.LanguageModelChatTool => { - return { - name: tool.name, - description: tool.description, - inputSchema: tool.inputSchema ?? {} - }; - }); + + + const allTools: vscode.LanguageModelChatTool[] = []; + for (const tool of vscode.lm.tools) { + if (request.tools.has(tool.name) && request.tools.get(tool.name)) { + allTools.push(tool); + } else if (tool.name.startsWith('github-pull-request')) { + allTools.push(tool); + } + } const { messages } = await renderPrompt( ParticipantsPrompt, diff --git a/src/lm/participantsPrompt.ts b/src/lm/participantsPrompt.ts new file mode 100644 index 0000000000..9533fd073d --- /dev/null +++ b/src/lm/participantsPrompt.ts @@ -0,0 +1,43 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { AssistantMessage, BasePromptElementProps, Chunk, PromptElement, PromptPiece, PromptSizing, UserMessage } from '@vscode/prompt-tsx'; + +interface ParticipantsPromptProps extends BasePromptElementProps { + readonly userMessage: string; +} + +export class ParticipantsPrompt extends PromptElement { + render(_state: void, _sizing: PromptSizing): PromptPiece { + const instructions = [ + 'Instructions:', + '- The user will ask a question related to GitHub, and it may require lots of research to answer correctly. There is a selection of tools that let you perform actions or retrieve helpful context to answer the user\'s question.', + "- If you aren't sure which tool is relevant, you can call multiple tools. You can call tools repeatedly to take actions or gather as much context as needed until you have completed the task fully. Don't give up unless you are sure the request cannot be fulfilled with the tools you have.", + "- Don't ask the user for confirmation to use tools, just use them.", + '- When talking about issues, be as concise as possible while still conveying all the information you need to. Avoid mentioning the following:', + ' - The fact that there are no comments.', + ' - Any info that seems like template info.' + ].join('\n'); + + const assistantPiece: PromptPiece = { + ctor: AssistantMessage, + props: {}, + children: [instructions] + }; + + const userPiece: PromptPiece = { + ctor: UserMessage, + props: {}, + children: [this.props.userMessage] + }; + + const container: PromptPiece = { + ctor: Chunk, + props: {}, + children: [assistantPiece, userPiece] + }; + return container; + } +} \ No newline at end of file diff --git a/src/lm/participantsPrompt.tsx b/src/lm/participantsPrompt.tsx deleted file mode 100644 index d8627906f1..0000000000 --- a/src/lm/participantsPrompt.tsx +++ /dev/null @@ -1,31 +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 { AssistantMessage, BasePromptElementProps, PromptElement, UserMessage } from "@vscode/prompt-tsx"; - -interface ParticipantsPromptProps extends BasePromptElementProps { - readonly userMessage: string; -} - -export class ParticipantsPrompt extends PromptElement { - render() { - return ( - <> - - Instructions:
- - The user will ask a question related to GitHub, and it may require lots of research to answer correctly. There is a selection of tools that let you perform actions or retrieve helpful context to answer the user's question.
- - If you aren't sure which tool is relevant, you can call multiple tools. You can call tools repeatedly to take actions or gather as much context as needed until you have completed the task fully. Don't give up unless you are sure the request cannot be fulfilled with the tools you have.
- - Don't ask the user for confirmation to use tools, just use them.
- - When talking about issues, be as concise as possible while still conveying all the information you need to. Avoid mentioning the following:
- - The fact that there are no comments.
- - Any info that seems like template info. -
- - {this.props.userMessage} - - - ); - } -} \ No newline at end of file diff --git a/src/lm/pullRequestContextProvider.ts b/src/lm/pullRequestContextProvider.ts new file mode 100644 index 0000000000..0415a81cc1 --- /dev/null +++ b/src/lm/pullRequestContextProvider.ts @@ -0,0 +1,143 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { GitApiImpl } from '../api/api1'; +import { Disposable } from '../common/lifecycle'; +import { onceEvent } from '../common/utils'; +import { PullRequestModel } from '../github/pullRequestModel'; +import { PullRequestOverviewPanel } from '../github/pullRequestOverview'; +import { RepositoriesManager } from '../github/repositoriesManager'; +import { PrsTreeModel } from '../view/prsTreeModel'; + +interface PRChatContextItem extends vscode.ChatContextItem { + pr?: PullRequestModel; +} + +export class PullRequestContextProvider extends Disposable implements vscode.ChatContextProvider { + private readonly _onDidChangeWorkspaceChatContext = new vscode.EventEmitter(); + readonly onDidChangeWorkspaceChatContext = this._onDidChangeWorkspaceChatContext.event; + + constructor(private readonly _prsTreeModel: PrsTreeModel, + private readonly _reposManager: RepositoriesManager, + private readonly _git: GitApiImpl + ) { + super(); + } + + /** + * Do this setup in the initialize method so that it can be called after the provider is registered. + */ + async initialize() { + if (this._git.state === 'uninitialized') { + await new Promise(resolve => { + this._register(onceEvent(this._git.onDidChangeState)(() => resolve())); + }); + } + this._reposManager.folderManagers.forEach(folderManager => { + this._register(folderManager.onDidChangeActivePullRequest(() => { + this._onDidChangeWorkspaceChatContext.fire(); + })); + }); + this._register(this._reposManager.onDidChangeFolderRepositories(e => { + if (!e.added) { + return; + } + this._register(e.added.onDidChangeActivePullRequest(() => { + this._onDidChangeWorkspaceChatContext.fire(); + })); + this._onDidChangeWorkspaceChatContext.fire(); + })); + this._register(this._reposManager.onDidChangeAnyGitHubRepository(() => { + this._onDidChangeWorkspaceChatContext.fire(); + })); + this._onDidChangeWorkspaceChatContext.fire(); + } + + async provideWorkspaceChatContext(_token: vscode.CancellationToken): Promise { + const modelDescription = this._reposManager.folderManagers.length > 1 ? 'Information about one of the current repositories. You can use this information when you need to calculate diffs or compare changes with the default branch' : 'Information about the current repository. You can use this information when you need to calculate diffs or compare changes with the default branch'; + const contexts: vscode.ChatContextItem[] = []; + for (const folderManager of this._reposManager.folderManagers) { + if (folderManager.gitHubRepositories.length === 0) { + continue; + } + const defaults = await folderManager.getPullRequestDefaults(); + + let value = `Repository name: ${defaults.repo} +Owner: ${defaults.owner} +Current branch: ${folderManager.repository.state.HEAD?.name ?? 'unknown'} +Default branch: ${defaults.base}`; + if (folderManager.activePullRequest) { + value = `${value} +Active pull request (may not be the same as open pull request): ${folderManager.activePullRequest.title} ${folderManager.activePullRequest.html_url}`; + } + contexts.push({ + icon: new vscode.ThemeIcon('github-alt'), + label: `${defaults.owner}/${defaults.repo}`, + modelDescription, + value + }); + } + return contexts; + } + + async provideChatContextForResource(_options: { resource: vscode.Uri }, _token: vscode.CancellationToken): Promise { + const item = PullRequestOverviewPanel.currentPanel?.getCurrentItem(); + if (item) { + return this._prToUnresolvedContext(item); + } + } + + async resolveChatContext(context: PRChatContextItem, _token: vscode.CancellationToken): Promise { + if (!context.pr) { + return context; + } + context.value = await this._resolvedPrValue(context.pr); + context.modelDescription = 'All the information about the GitHub pull request the user is viewing, including comments, review threads, and changes.'; + return context; + } + + async provideChatContextExplicit(_token: vscode.CancellationToken): Promise { + const prs = await this._prsTreeModel.getAllPullRequests(this._reposManager.folderManagers[0], false); + return prs.items.map(pr => { + return this._prToUnresolvedContext(pr); + }); + } + + private _prToUnresolvedContext(pr: PullRequestModel): PRChatContextItem { + return { + icon: new vscode.ThemeIcon('git-pull-request'), + label: `#${pr.number} ${pr.title}`, + modelDescription: 'The GitHub pull request the user is viewing.', + pr, + }; + } + + private async _resolvedPrValue(pr: PullRequestModel): Promise { + return JSON.stringify({ + prNumber: pr.number, + owner: pr.remote.owner, + repo: pr.remote.repositoryName, + title: pr.title, + body: pr.body, + comments: pr.comments.map(comment => ({ + author: comment.user?.login, + body: comment.body, + createdAt: comment.createdAt + })), + threads: (pr.reviewThreadsCache ?? await pr.getReviewThreads()).map(thread => ({ + comments: thread.comments.map(comment => ({ + author: comment.user?.login, + body: comment.body, + createdAt: comment.createdAt + })), + isResolved: thread.isResolved + })), + changes: (pr.rawFileChanges ?? await pr.getRawFileChangesInfo()).map(change => { + return change.patch; + }) + }); + } +} \ No newline at end of file diff --git a/src/lm/tools/activePullRequestTool.ts b/src/lm/tools/activePullRequestTool.ts index 47d1b98336..010c1fb2e4 100644 --- a/src/lm/tools/activePullRequestTool.ts +++ b/src/lm/tools/activePullRequestTool.ts @@ -5,39 +5,139 @@ 'use strict'; import * as vscode from 'vscode'; +import { FetchIssueResult } from './fetchIssueTool'; +import { COPILOT_LOGINS } from '../../common/copilot'; +import { GitChangeType, InMemFileChange } from '../../common/file'; +import Logger from '../../common/logger'; +import { CommentEvent, EventType, ReviewEvent } from '../../common/timelineEvent'; +import { CopilotRemoteAgentManager } from '../../github/copilotRemoteAgent'; import { PullRequestModel } from '../../github/pullRequestModel'; -import { PullRequestOverviewPanel } from '../../github/pullRequestOverview'; import { RepositoriesManager } from '../../github/repositoriesManager'; -import { FetchIssueResult } from './fetchIssueTool'; -export class ActivePullRequestTool implements vscode.LanguageModelTool { - public static readonly toolId = 'github-pull-request_activePullRequest'; - constructor(private readonly folderManagers: RepositoriesManager) { } +export abstract class PullRequestTool implements vscode.LanguageModelTool { + constructor( + protected readonly folderManagers: RepositoriesManager, + private readonly copilotRemoteAgentManager: CopilotRemoteAgentManager + ) { } - private _findActivePullRequest(): PullRequestModel | undefined { - const folderManager = this.folderManagers.folderManagers.find((manager) => manager.activePullRequest); - return folderManager?.activePullRequest ?? PullRequestOverviewPanel.currentPanel?.getCurrentItem(); + protected abstract _findActivePullRequest(): PullRequestModel | undefined; + + protected abstract _confirmationTitle(): string; + + private shouldIncludeCodingAgentSession(pullRequest?: PullRequestModel): boolean { + return !!pullRequest && this.copilotRemoteAgentManager.enabled && COPILOT_LOGINS.includes(pullRequest.author.login); + } + + private _getPullRequestLabel(pullRequest: PullRequestModel): string { + return `${pullRequest.title} (#${pullRequest.number})`; } async prepareInvocation(): Promise { const pullRequest = this._findActivePullRequest(); + if (!pullRequest) { + return { + pastTenseMessage: vscode.l10n.t('No active pull request'), + invocationMessage: vscode.l10n.t('Reading active pull request'), + confirmationMessages: { title: this._confirmationTitle(), message: vscode.l10n.t('Allow reading the details of the active pull request?') }, + }; + } + + const label = this._getPullRequestLabel(pullRequest); return { - invocationMessage: pullRequest ? vscode.l10n.t('Pull request "{0}"', pullRequest.title) : vscode.l10n.t('Active pull request'), + pastTenseMessage: vscode.l10n.t('Read pull request "{0}"', label), + invocationMessage: vscode.l10n.t('Reading pull request "{0}"', label), + confirmationMessages: { title: this._confirmationTitle(), message: vscode.l10n.t('Allow reading the details of "{0}"?', label) }, }; } - async invoke(_options: vscode.LanguageModelToolInvocationOptions, _token: vscode.CancellationToken): Promise { + private parseCopilotEventStream(logsResponseText: string): string[] { + const result: string[] = []; + logsResponseText + .split('\n') + .filter(line => line.startsWith('data:')) + .forEach(line => { + try { + const obj = JSON.parse(line.replace(/^data:\s*/, '')); + if (Array.isArray(obj.choices)) { + for (const choice of obj.choices) { + const delta = choice.delta || {}; + if (typeof delta.content === 'string' && !!delta.role) { + result.push(delta.content); + } + } + } + } catch { /* ignore parse errors */ } + }); + + return result; + } + + async fallbackSessionLogs( + pullRequest: PullRequestModel, + model: vscode.LanguageModelChat, + cancellationToken: vscode.CancellationToken + ) { + const logs = await this.copilotRemoteAgentManager.getSessionLogsFromAction(pullRequest); + // Summarize the Copilot agent's thinking process using the model + const messages = [ + vscode.LanguageModelChatMessage.Assistant('You are an expert summarizer. The following logs show the thinking process and performed actions of a GitHub Copilot agent that was in charge of working on the current pull request. Read the logs and always maintain the thinking process. You can remove information on the tool call results that you think are not necessary for building context.'), + vscode.LanguageModelChatMessage.User(`Copilot Agent Logs (JSON):\n${JSON.stringify(logs)}`) + ]; + + let summaryText: string | undefined; + try { + const response = await model.sendRequest(messages, { justification: 'Summarizing Copilot agent logs for the active pull request.' }, cancellationToken); + summaryText = await (typeof response.text === 'string' ? response.text : (typeof response.text?.[Symbol.asyncIterator] === 'function' ? (async () => { let out = ''; for await (const chunk of response.text) { out += chunk; } return out; })() : '')); + } catch (e) { + summaryText = ''; + } + + return summaryText; + } + + async fetchCodingAgentSession( + pullRequest: PullRequestModel, + model: vscode.LanguageModelChat, + token: vscode.CancellationToken + ): Promise { + let copilotSteps: string | string[] = []; + try { + const logs = await this.copilotRemoteAgentManager.getSessionLogFromPullRequest(pullRequest); + if (!logs) { + throw new Error('Could not get session logs'); + } + + copilotSteps = this.parseCopilotEventStream(logs.logs); + if (copilotSteps.length === 0) { + throw new Error('Empty Copilot agent logs received'); + } + } catch (e) { + Logger.debug(`Failed to fetch Copilot agent logs from API: ${e}.`, ActivePullRequestTool.toolId); + copilotSteps = await this.fallbackSessionLogs(pullRequest, model, token); + } + + return copilotSteps; + } + + async invoke(options: vscode.LanguageModelToolInvocationOptions, token: vscode.CancellationToken): Promise { let pullRequest = this._findActivePullRequest(); if (!pullRequest) { return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart('There is no active pull request')]); } + let codingAgentSession: string | string[] = []; + if (this.shouldIncludeCodingAgentSession(pullRequest) && options.model) { + codingAgentSession = await this.fetchCodingAgentSession(pullRequest, options.model, token); + } + const status = await pullRequest.getStatusChecks(); + const timeline = (pullRequest.timelineEvents && pullRequest.timelineEvents.length > 0) ? pullRequest.timelineEvents : await pullRequest.getTimelineEvents(); const pullRequestInfo = { title: pullRequest.title, body: pullRequest.body, author: pullRequest.author, + assignees: pullRequest.assignees, comments: pullRequest.comments.map(comment => { return { author: comment.user?.login, @@ -46,6 +146,13 @@ export class ActivePullRequestTool implements vscode.LanguageModelTool event.event === EventType.Reviewed || event.event === EventType.Commented).map(event => { + return { + author: event.user?.login, + body: event.body, + commentType: event.event === EventType.Reviewed ? event.state : 'COMMENTED', + }; + }), state: pullRequest.state, statusChecks: status[0]?.statuses.map((status) => { return { @@ -61,11 +168,33 @@ export class ActivePullRequestTool implements vscode.LanguageModelTool 0, }, - isDraft: pullRequest.isDraft, + isDraft: pullRequest.isDraft ? 'is a draft and cannot be merged until marked as ready for review' : 'false', + codingAgentSession, + changes: (await pullRequest.getFileChangesInfo()).map(change => { + if (change instanceof InMemFileChange) { + return change.diffHunks?.map(hunk => hunk.diffLines.map(line => line.raw).join('\n')).join('\n') || ''; + } else { + return `File: ${change.fileName} was ${change.status === GitChangeType.ADD ? 'added' : change.status === GitChangeType.DELETE ? 'deleted' : 'modified'}.`; + } + }) }; - return new vscode.LanguageModelToolResult([new vscode.LanguageModelTextPart(JSON.stringify(pullRequestInfo))]); + const result = new vscode.ExtendedLanguageModelToolResult([new vscode.LanguageModelTextPart(JSON.stringify(pullRequestInfo))]); + result.toolResultDetails = [vscode.Uri.parse(pullRequest.html_url)]; + return result; + } + +} +export class ActivePullRequestTool extends PullRequestTool { + public static readonly toolId = 'github-pull-request_activePullRequest'; + + protected _findActivePullRequest(): PullRequestModel | undefined { + const folderManager = this.folderManagers.folderManagers.find((manager) => manager.activePullRequest); + return folderManager?.activePullRequest; } + protected _confirmationTitle(): string { + return vscode.l10n.t('Active Pull Request'); + } } \ No newline at end of file diff --git a/src/lm/tools/copilotRemoteAgentTool.ts b/src/lm/tools/copilotRemoteAgentTool.ts new file mode 100644 index 0000000000..822d2d556a --- /dev/null +++ b/src/lm/tools/copilotRemoteAgentTool.ts @@ -0,0 +1,154 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as marked from 'marked'; +import * as vscode from 'vscode'; +import { COPILOT_ACCOUNTS } from '../../common/comment'; +import { ITelemetry } from '../../common/telemetry'; +import { toOpenPullRequestWebviewUri } from '../../common/uri'; +import { CopilotRemoteAgentManager } from '../../github/copilotRemoteAgent'; +import { FolderRepositoryManager } from '../../github/folderRepositoryManager'; +import { PlainTextRenderer } from '../../github/markdownUtils'; +import { PrsTreeModel } from '../../view/prsTreeModel'; + +export interface CopilotRemoteAgentToolParameters { + // The LLM is inconsistent in providing repo information. + // For now, we only support the active repository in the current workspace. + // repo?: { + // owner?: string; + // name?: string; + // }; + title: string; + body?: string; + existingPullRequest?: string; +} + +export class CopilotRemoteAgentTool implements vscode.LanguageModelTool { + public static readonly toolId = 'github-pull-request_copilot-coding-agent'; + + constructor(private manager: CopilotRemoteAgentManager, private telemetry: ITelemetry, private prsTreeModel: PrsTreeModel) { } + + async prepareInvocation(options: vscode.LanguageModelToolInvocationPrepareOptions): Promise { + const { title, existingPullRequest } = options.input; + const folderManager = existingPullRequest ? undefined : await this.manager.tryPromptForAuthAndRepo(); + + // Check if the coding agent is available (enabled and assignable) + const isAvailable = await this.manager.isAvailable(); + if (!isAvailable) { + throw new Error(vscode.l10n.t('Copilot coding agent is not available for this repository. Make sure the agent is enabled and assignable to this repository.')); + } + + const targetRepo = await this.manager.repoInfo(folderManager); + const autoPushEnabled = this.manager.autoCommitAndPushEnabled; + const openPR = existingPullRequest || await this.getActivePullRequestWithSession(targetRepo); + + /* __GDPR__ + "remoteAgent.tool.prepare" : {} + */ + this.telemetry.sendTelemetryEvent('copilot.remoteAgent.tool.prepare', {}); + + return { + pastTenseMessage: vscode.l10n.t('Launched coding agent'), + invocationMessage: vscode.l10n.t('Launching coding agent'), + confirmationMessages: { + message: openPR + ? vscode.l10n.t('The coding agent will incorporate your feedback on existing pull request **#{0}**.', openPR) + : (targetRepo && autoPushEnabled + ? vscode.l10n.t('The coding agent will continue work on "**{0}**" in a new branch on "**{1}/{2}**". Any uncommitted changes will be **automatically pushed**.', title, targetRepo.owner, targetRepo.repo) + : vscode.l10n.t('The coding agent will start working on "**{0}**"', title)), + title: vscode.l10n.t('Start coding agent?'), + } + }; + } + + async invoke( + options: vscode.LanguageModelToolInvocationOptions, + _: vscode.CancellationToken + ): Promise { + const title = options.input.title; + const body = options.input.body || ''; + const existingPullRequest = options.input.existingPullRequest || ''; + const folderManager = existingPullRequest ? undefined : await this.manager.tryPromptForAuthAndRepo(); + + const targetRepo = await this.manager.repoInfo(folderManager); + if (!targetRepo) { + return new vscode.LanguageModelToolResult([ + new vscode.LanguageModelTextPart(vscode.l10n.t('No repository information found. Please open a workspace with a Git repository.')) + ]); + } + + let pullRequestNumber: number | undefined; + if (existingPullRequest) { + pullRequestNumber = parseInt(existingPullRequest, 10); + if (isNaN(pullRequestNumber)) { + return new vscode.LanguageModelToolResult([ + new vscode.LanguageModelTextPart(vscode.l10n.t('Invalid pull request number: {0}', existingPullRequest)) + ]); + } + } else { + pullRequestNumber = await this.getActivePullRequestWithSession(targetRepo); + } + + /* __GDPR__ + "copilot.remoteAgent.tool.invoke" : { + "hasExistingPR" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" }, + "hasBody" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetry.sendTelemetryEvent('copilot.remoteAgent.tool.invoke', { + hasExistingPR: pullRequestNumber ? 'true' : 'false', + hasBody: body ? 'true' : 'false' + }); + + if (pullRequestNumber) { + await this.manager.addFollowUpToExistingPR(pullRequestNumber, title, body); + return new vscode.LanguageModelToolResult([ + new vscode.LanguageModelTextPart(vscode.l10n.t('Follow-up added to pull request #{0}.', pullRequestNumber)), + ]); + } + + const result = await this.manager.invokeRemoteAgent(title, body); + if (result.state === 'error') { + /* __GDPR__ + "copilot.remoteAgent.tool.error" : { + "reason" : { "classification": "SystemMetaData", "purpose": "FeatureInsight" } + } + */ + this.telemetry.sendTelemetryErrorEvent('copilot.remoteAgent.tool.error', { reason: 'invocationError' }); + throw new Error(result.error); + } + + let lmResult: (vscode.LanguageModelTextPart | vscode.LanguageModelDataPart)[] = [new vscode.LanguageModelTextPart(result.llmDetails)]; + const pr = await targetRepo.fm.resolvePullRequest(targetRepo.owner, targetRepo.repo, result.number); + if (pr) { + const plaintextBody = marked.parse(pr.body, { renderer: new PlainTextRenderer(true), smartypants: true }).trim(); + const preferredRendering = { + uri: (await toOpenPullRequestWebviewUri({ owner: pr.githubRepository.remote.owner, repo: pr.githubRepository.remote.repositoryName, pullRequestNumber: pr.number })).toString(), + title: pr.title, + description: plaintextBody, + author: COPILOT_ACCOUNTS[pr.author.login].name, + linkTag: `#${pr.number}` + }; + const buffer: Buffer = Buffer.from(JSON.stringify(preferredRendering)); + const data: Uint8Array = Uint8Array.from(buffer); + + // API might not be available for tests, this guarantees we are still able to test + const userAudience = vscode.LanguageModelPartAudience?.User ?? 1; + lmResult.push(new vscode.LanguageModelDataPart2(data, 'application/pull-request+json', [userAudience])); + } + + return new vscode.LanguageModelToolResult2(lmResult); + } + + protected async getActivePullRequestWithSession(repoInfo: { repo: string; owner: string; fm: FolderRepositoryManager } | undefined): Promise { + if (!repoInfo) { + return; + } + const activePR = repoInfo.fm.activePullRequest; + if (activePR && this.prsTreeModel.getCopilotStateForPR(repoInfo.owner, repoInfo.repo, activePR.number)) { + return activePR.number; + } + } +} \ No newline at end of file diff --git a/src/lm/tools/displayIssuesTool.ts b/src/lm/tools/displayIssuesTool.ts index 782fe73ffe..8177718f87 100644 --- a/src/lm/tools/displayIssuesTool.ts +++ b/src/lm/tools/displayIssuesTool.ts @@ -5,6 +5,7 @@ 'use strict'; import * as vscode from 'vscode'; +import { ensureEmojis } from '../../common/emoji'; import Logger from '../../common/logger'; import { reviewerLabel } from '../../github/interface'; import { makeLabel } from '../../github/utils'; @@ -29,7 +30,7 @@ Here are the possible columns: export class DisplayIssuesTool extends ToolBase { public static readonly toolId = 'github-pull-request_renderIssues'; private static ID = 'DisplayIssuesTool'; - constructor(chatParticipantState: ChatParticipantState) { + constructor(private readonly context: vscode.ExtensionContext, chatParticipantState: ChatParticipantState) { super(chatParticipantState); } @@ -144,6 +145,7 @@ export class DisplayIssuesTool extends ToolBase { } async invoke(options: vscode.LanguageModelToolInvocationOptions, token: vscode.CancellationToken): Promise { + await ensureEmojis(this.context); const issueItemsInfo: vscode.LanguageModelTextPart | undefined = this.chatParticipantState.firstUserMessage; const issueItems: IssueSearchResultItem[] | undefined = options.input.arrayOfIssues; if (!issueItems || issueItems.length === 0) { diff --git a/src/lm/tools/fetchIssueTool.ts b/src/lm/tools/fetchIssueTool.ts index d81433d708..e63b81b9b9 100644 --- a/src/lm/tools/fetchIssueTool.ts +++ b/src/lm/tools/fetchIssueTool.ts @@ -5,9 +5,10 @@ 'use strict'; import * as vscode from 'vscode'; +import { RepoToolBase } from './toolsUtils'; import { InMemFileChange } from '../../common/file'; +import { isITeam } from '../../github/interface'; import { PullRequestModel } from '../../github/pullRequestModel'; -import { RepoToolBase } from './toolsUtils'; interface FetchIssueToolParameters { issueNumber?: number; @@ -32,6 +33,9 @@ export interface FetchIssueResult { owner?: string; repo?: string; fileChanges?: FileChange[]; + author?: string; + assignees?: string[]; + reviewers?: string[]; } export class FetchIssueTool extends RepoToolBase { @@ -40,19 +44,22 @@ export class FetchIssueTool extends RepoToolBase { async invoke(options: vscode.LanguageModelToolInvocationOptions, _token: vscode.CancellationToken): Promise { const issueNumber = options.input.issueNumber; if (!issueNumber) { - throw new Error('No issue/PR number provided.'); + throw new Error('No issue/pull-request number provided.'); } const { owner, name, folderManager } = await this.getRepoInfo({ owner: options.input.repo?.owner, name: options.input.repo?.name }); const issueOrPullRequest = await folderManager.resolveIssueOrPullRequest(owner, name, issueNumber); if (!issueOrPullRequest) { - throw new Error(`No issue or PR found for ${owner}/${name}/${issueNumber}. Make sure the issue or PR exists.`); + throw new Error(`No issue or pull request found for ${owner}/${name}/${issueNumber}. Make sure the issue or pull request exists.`); } const result: FetchIssueResult = { owner, repo: name, title: issueOrPullRequest.title, body: issueOrPullRequest.body, - comments: issueOrPullRequest.item.comments?.map(c => ({ body: c.body, author: c.author.login })) ?? [] + comments: issueOrPullRequest.item.comments?.map(c => ({ body: c.body, author: c.author.login })) ?? [], + author: issueOrPullRequest.author?.login, + assignees: issueOrPullRequest.assignees?.map(a => a.login), + reviewers: issueOrPullRequest instanceof PullRequestModel ? issueOrPullRequest.reviewers?.map(r => isITeam(r) ? r.name : r.login).filter((login): login is string => !!login) : undefined }; if (issueOrPullRequest instanceof PullRequestModel && issueOrPullRequest.isResolved()) { const fileChanges = await issueOrPullRequest.getFileChangesInfo(); diff --git a/src/lm/tools/fetchNotificationTool.ts b/src/lm/tools/fetchNotificationTool.ts index 4437298359..ef4eb2dd72 100644 --- a/src/lm/tools/fetchNotificationTool.ts +++ b/src/lm/tools/fetchNotificationTool.ts @@ -5,10 +5,10 @@ 'use strict'; import * as vscode from 'vscode'; +import { RepoToolBase } from './toolsUtils'; import { InMemFileChange } from '../../common/file'; import { PullRequestModel } from '../../github/pullRequestModel'; import { getNotificationKey } from '../../github/utils'; -import { RepoToolBase } from './toolsUtils'; interface FetchNotificationToolParameters { thread_id?: number; diff --git a/src/lm/tools/openPullRequestTool.ts b/src/lm/tools/openPullRequestTool.ts new file mode 100644 index 0000000000..74b5d2f0bc --- /dev/null +++ b/src/lm/tools/openPullRequestTool.ts @@ -0,0 +1,86 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { PullRequestTool } from './activePullRequestTool'; +import { fromPRUri, fromReviewUri, Schemes } from '../../common/uri'; +import { PullRequestModel } from '../../github/pullRequestModel'; +import { PullRequestOverviewPanel } from '../../github/pullRequestOverview'; + +export class OpenPullRequestTool extends PullRequestTool { + public static readonly toolId = 'github-pull-request_openPullRequest'; + + protected _findActivePullRequest(): PullRequestModel | undefined { + // First check if there's a PR overview panel open + const panelPR = PullRequestOverviewPanel.currentPanel?.getCurrentItem(); + if (panelPR) { + return panelPR; + } + + // Check if the active tab is a diff editor showing PR content + const activeTab = vscode.window.tabGroups.activeTabGroup.activeTab; + if (activeTab?.input instanceof vscode.TabInputTextDiff) { + const diffInput = activeTab.input; + const urisToCheck = [diffInput.original, diffInput.modified]; + + for (const uri of urisToCheck) { + if (uri.scheme === Schemes.Pr) { + // This is a PR diff from GitHub + const prParams = fromPRUri(uri); + if (prParams) { + return this._findPullRequestByNumber(prParams.prNumber, prParams.remoteName); + } + } else if (uri.scheme === Schemes.Review) { + // This is a review diff from a checked out PR + const reviewParams = fromReviewUri(uri.query); + if (reviewParams) { + // For review scheme, find the folder manager based on the root path + const rootUri = vscode.Uri.file(reviewParams.rootPath); + const folderManager = this.folderManagers.getManagerForFile(rootUri); + return folderManager?.activePullRequest; + } + } + } + } else if (activeTab?.input instanceof vscode.TabInputText) { + // Check if a single file with PR scheme is open (e.g., newly added files) + const textInput = activeTab.input; + if (textInput.uri.scheme === Schemes.Pr) { + const prParams = fromPRUri(textInput.uri); + if (prParams) { + return this._findPullRequestByNumber(prParams.prNumber, prParams.remoteName); + } + } else if (textInput.uri.scheme === Schemes.Review) { + const reviewParams = fromReviewUri(textInput.uri.query); + if (reviewParams) { + const rootUri = vscode.Uri.file(reviewParams.rootPath); + const folderManager = this.folderManagers.getManagerForFile(rootUri); + return folderManager?.activePullRequest; + } + } + } + + return undefined; + } + + private _findPullRequestByNumber(prNumber: number, remoteName: string): PullRequestModel | undefined { + for (const manager of this.folderManagers.folderManagers) { + for (const repo of manager.gitHubRepositories) { + if (repo.remote.remoteName === remoteName) { + // Look for the PR in the repository's PR cache + for (const pr of repo.pullRequestModels) { + if (pr.number === prNumber) { + return pr; + } + } + } + } + } + return undefined; + } + + protected _confirmationTitle(): string { + return vscode.l10n.t('Open Pull Request'); + } +} diff --git a/src/lm/tools/searchTools.ts b/src/lm/tools/searchTools.ts index 37a66ec002..4d5c99273a 100644 --- a/src/lm/tools/searchTools.ts +++ b/src/lm/tools/searchTools.ts @@ -5,11 +5,11 @@ 'use strict'; import * as vscode from 'vscode'; +import { concatAsyncIterable, RepoToolBase } from './toolsUtils'; import Logger from '../../common/logger'; import { FolderRepositoryManager } from '../../github/folderRepositoryManager'; import { ILabel } from '../../github/interface'; import { escapeMarkdown } from '../../issues/util'; -import { concatAsyncIterable, RepoToolBase } from './toolsUtils'; interface ConvertToQuerySyntaxParameters { naturalLanguageString?: string; @@ -124,7 +124,7 @@ You are an expert on choosing search keywords based on a natural language search - Only choose labels that you're sure are relevant. Having no labels is preferable than lables that aren't relevant. - Don't choose labels that the user has explicitly excluded. - Respond with label names chosen from this JSON array of options: -${JSON.stringify(labels.filter(label => !label.name.includes('required') && !label.name.includes('search') && !label.name.includes('question') && !label.name.includes('find')).map(label => ({ name: label.name, description: label.description })))} +${JSON.stringify(labels.filter(label => !label.name.includes('required') && !label.name.includes('search') && !label.name.includes('question') && !label.name.includes('find') && !label.name.includes('issue')).map(label => ({ name: label.name, description: label.description })))} `; } diff --git a/src/lm/tools/summarizeIssueTool.ts b/src/lm/tools/summarizeIssueTool.ts index 1114299fb0..a0c6fb71d8 100644 --- a/src/lm/tools/summarizeIssueTool.ts +++ b/src/lm/tools/summarizeIssueTool.ts @@ -73,7 +73,7 @@ Body: ${comment.body} private summarizeInstructions(repo: string, owner: string): string { return ` -You are an AI assistant who is very proficient in summarizing issues and PRs. +You are an AI assistant who is very proficient in summarizing issues and pull requests (PRs). You will be given information relative to an issue or PR : the title, the body and the comments. In the case of a PR you will also be given patches of the PR changes. Your task is to output a summary of all this information. Do not output code. When you try to summarize PR changes, summarize in a textual format. diff --git a/src/lm/tools/tools.ts b/src/lm/tools/tools.ts index 4b5392c672..09c833a7a9 100644 --- a/src/lm/tools/tools.ts +++ b/src/lm/tools/tools.ts @@ -5,24 +5,31 @@ 'use strict'; import * as vscode from 'vscode'; +import { ITelemetry } from '../../common/telemetry'; +import { CopilotRemoteAgentManager } from '../../github/copilotRemoteAgent'; import { CredentialStore } from '../../github/credentials'; import { RepositoriesManager } from '../../github/repositoriesManager'; import { ChatParticipantState } from '../participants'; import { ActivePullRequestTool } from './activePullRequestTool'; +import { CopilotRemoteAgentTool } from './copilotRemoteAgentTool'; import { DisplayIssuesTool } from './displayIssuesTool'; import { FetchIssueTool } from './fetchIssueTool'; import { FetchNotificationTool } from './fetchNotificationTool'; +import { OpenPullRequestTool } from './openPullRequestTool'; import { ConvertToSearchSyntaxTool, SearchTool } from './searchTools'; import { SuggestFixTool } from './suggestFixTool'; import { IssueSummarizationTool } from './summarizeIssueTool'; import { NotificationSummarizationTool } from './summarizeNotificationsTool'; +import { PrsTreeModel } from '../../view/prsTreeModel'; -export function registerTools(context: vscode.ExtensionContext, credentialStore: CredentialStore, repositoriesManager: RepositoriesManager, chatParticipantState: ChatParticipantState) { +export function registerTools(context: vscode.ExtensionContext, credentialStore: CredentialStore, repositoriesManager: RepositoriesManager, chatParticipantState: ChatParticipantState, copilotRemoteAgentManager: CopilotRemoteAgentManager, telemetry: ITelemetry, prsTreeModel: PrsTreeModel) { registerFetchingTools(context, credentialStore, repositoriesManager, chatParticipantState); registerSummarizationTools(context); registerSuggestFixTool(context, credentialStore, repositoriesManager, chatParticipantState); registerSearchTools(context, credentialStore, repositoriesManager, chatParticipantState); - context.subscriptions.push(vscode.lm.registerTool(ActivePullRequestTool.toolId, new ActivePullRequestTool(repositoriesManager))); + registerCopilotAgentTools(context, copilotRemoteAgentManager, telemetry, prsTreeModel); + context.subscriptions.push(vscode.lm.registerTool(ActivePullRequestTool.toolId, new ActivePullRequestTool(repositoriesManager, copilotRemoteAgentManager))); + context.subscriptions.push(vscode.lm.registerTool(OpenPullRequestTool.toolId, new OpenPullRequestTool(repositoriesManager, copilotRemoteAgentManager))); } function registerFetchingTools(context: vscode.ExtensionContext, credentialStore: CredentialStore, repositoriesManager: RepositoriesManager, chatParticipantState: ChatParticipantState) { @@ -39,8 +46,12 @@ function registerSuggestFixTool(context: vscode.ExtensionContext, credentialStor context.subscriptions.push(vscode.lm.registerTool(SuggestFixTool.toolId, new SuggestFixTool(credentialStore, repositoriesManager, chatParticipantState))); } +function registerCopilotAgentTools(context: vscode.ExtensionContext, copilotRemoteAgentManager: CopilotRemoteAgentManager, telemetry: ITelemetry, prsTreeModel: PrsTreeModel) { + context.subscriptions.push(vscode.lm.registerTool(CopilotRemoteAgentTool.toolId, new CopilotRemoteAgentTool(copilotRemoteAgentManager, telemetry, prsTreeModel))); +} + function registerSearchTools(context: vscode.ExtensionContext, credentialStore: CredentialStore, repositoriesManager: RepositoriesManager, chatParticipantState: ChatParticipantState) { context.subscriptions.push(vscode.lm.registerTool(ConvertToSearchSyntaxTool.toolId, new ConvertToSearchSyntaxTool(credentialStore, repositoriesManager, chatParticipantState))); context.subscriptions.push(vscode.lm.registerTool(SearchTool.toolId, new SearchTool(credentialStore, repositoriesManager, chatParticipantState))); - context.subscriptions.push(vscode.lm.registerTool(DisplayIssuesTool.toolId, new DisplayIssuesTool(chatParticipantState))); + context.subscriptions.push(vscode.lm.registerTool(DisplayIssuesTool.toolId, new DisplayIssuesTool(context, chatParticipantState))); } \ No newline at end of file diff --git a/src/migrations.ts b/src/migrations.ts index 372880e0e9..9c7eaf2d5e 100644 --- a/src/migrations.ts +++ b/src/migrations.ts @@ -6,6 +6,8 @@ import * as vscode from 'vscode'; import * as PersistentState from './common/persistentState'; import { BRANCH_PUBLISH, PR_SETTINGS_NAMESPACE, QUERIES } from './common/settingKeys'; +import { DefaultQueries, isAllQuery, isLocalQuery } from './view/treeNodes/categoryNode'; +import { IQueryInfo } from './view/treeNodes/workspaceFolderNode'; const PROMPTS_SCOPE = 'prompts'; const PROMPT_TO_CREATE_PR_ON_PUBLISH_KEY = 'createPROnPublish'; @@ -13,6 +15,7 @@ const PROMPT_TO_CREATE_PR_ON_PUBLISH_KEY = 'createPROnPublish'; export async function migrate(context: vscode.ExtensionContext) { await createOnPublish(); await makeQueriesScopedToRepo(context); + await addDefaultQueries(context); } async function createOnPublish() { @@ -34,12 +37,11 @@ async function makeQueriesScopedToRepo(context: vscode.ExtensionContext) { const configuration = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE); const settingValue = configuration.inspect(QUERIES); - type Query = { - label: string, - query: string, - }; - const addRepoScope = (queries: Query[]) => { + const addRepoScope = (queries: IQueryInfo[]) => { return queries.map(query => { + if (isLocalQuery(query) || isAllQuery(query)) { + return query; + } return { label: query.label, query: query.query.includes('repo:') ? query.query : `repo:\${owner}/\${repository} ${query.query}`, @@ -49,13 +51,55 @@ async function makeQueriesScopedToRepo(context: vscode.ExtensionContext) { // User setting if (!hasMigratedUserQueries && settingValue?.globalValue) { - await configuration.update(QUERIES, addRepoScope(settingValue.globalValue as Query[]), vscode.ConfigurationTarget.Global); - context.globalState.update(HAS_MIGRATED_QUERIES, true); + await configuration.update(QUERIES, addRepoScope(settingValue.globalValue as IQueryInfo[]), vscode.ConfigurationTarget.Global); + } + context.globalState.update(HAS_MIGRATED_QUERIES, true); + + // Workspace setting + if (!hasMigratedWorkspaceQueries && settingValue?.workspaceValue) { + await configuration.update(QUERIES, addRepoScope(settingValue.workspaceValue as IQueryInfo[]), vscode.ConfigurationTarget.Workspace); + } + context.workspaceState.update(HAS_MIGRATED_QUERIES, true); +} + +const HAS_MIGRATED_DEFAULT_QUERIES = 'hasMigratedDefaultQueries4'; +async function addDefaultQueries(context: vscode.ExtensionContext) { + const hasMigratedUserQueries = context.globalState.get(HAS_MIGRATED_DEFAULT_QUERIES, false); + const hasMigratedWorkspaceQueries = context.workspaceState.get(HAS_MIGRATED_DEFAULT_QUERIES, false); + if (hasMigratedUserQueries && hasMigratedWorkspaceQueries) { + return; + } + + const configuration = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE); + const settingValue = configuration.inspect(QUERIES); + + const addNewDefaultQueries = (queries: IQueryInfo[]) => { + const hasLocalQuery = queries.some(query => isLocalQuery(query)); + const hasAllQuery = queries.some(query => isAllQuery(query)); + if (!hasLocalQuery) { + queries.unshift({ + label: DefaultQueries.Queries.LOCAL, + query: DefaultQueries.Values.DEFAULT, + }); + } + if (!hasAllQuery) { + queries.push({ + label: DefaultQueries.Queries.ALL, + query: DefaultQueries.Values.DEFAULT, + }); + } + return queries; + }; + + // User setting + if (!hasMigratedUserQueries && settingValue?.globalValue) { + await configuration.update(QUERIES, addNewDefaultQueries(settingValue.globalValue as IQueryInfo[]), vscode.ConfigurationTarget.Global); } + context.globalState.update(HAS_MIGRATED_DEFAULT_QUERIES, true); // Workspace setting if (!hasMigratedWorkspaceQueries && settingValue?.workspaceValue) { - await configuration.update(QUERIES, addRepoScope(settingValue.workspaceValue as Query[]), vscode.ConfigurationTarget.Workspace); - context.workspaceState.update(HAS_MIGRATED_QUERIES, true); + await configuration.update(QUERIES, addNewDefaultQueries(settingValue.workspaceValue as IQueryInfo[]), vscode.ConfigurationTarget.Workspace); } + context.workspaceState.update(HAS_MIGRATED_DEFAULT_QUERIES, true); } \ No newline at end of file diff --git a/src/notifications/notificationDecorationProvider.ts b/src/notifications/notificationDecorationProvider.ts index 62e02efeb6..486a29e9ca 100644 --- a/src/notifications/notificationDecorationProvider.ts +++ b/src/notifications/notificationDecorationProvider.ts @@ -4,10 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { NotificationsManager, NotificationsSortMethod } from './notificationsManager'; import { Disposable } from '../common/lifecycle'; import { EXPERIMENTAL_NOTIFICATIONS_SCORE, PR_SETTINGS_NAMESPACE } from '../common/settingKeys'; import { fromNotificationUri, toNotificationUri } from '../common/uri'; -import { NotificationsManager, NotificationsSortMethod } from './notificationsManager'; export class NotificationsDecorationProvider extends Disposable implements vscode.FileDecorationProvider { private _readonlyOnDidChangeFileDecorations: vscode.EventEmitter = this._register(new vscode.EventEmitter()); diff --git a/src/notifications/notificationItem.ts b/src/notifications/notificationItem.ts index 2e87bc65f4..7f312c118a 100644 --- a/src/notifications/notificationItem.ts +++ b/src/notifications/notificationItem.ts @@ -20,6 +20,11 @@ export interface NotificationTreeItem { readonly kind: 'notification'; } -export function isNotificationTreeItem(item: any): item is NotificationTreeItem { - return item.kind === 'notification'; -} \ No newline at end of file +export function isNotificationTreeItem(item: unknown): item is NotificationTreeItem { + return !!item && (item as Partial).kind === 'notification'; +} + +export interface NotificationID { + threadId: string; + notificationKey: string; +} diff --git a/src/notifications/notificationsFeatureRegistar.ts b/src/notifications/notificationsFeatureRegistar.ts index 534980827d..708233f040 100644 --- a/src/notifications/notificationsFeatureRegistar.ts +++ b/src/notifications/notificationsFeatureRegistar.ts @@ -4,16 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; -import { Disposable } from '../common/lifecycle'; import { ITelemetry } from '../common/telemetry'; import { onceEvent } from '../common/utils'; +import { EXTENSION_ID } from '../constants'; +import { NotificationsDecorationProvider } from './notificationDecorationProvider'; +import { isNotificationTreeItem, NotificationID, NotificationTreeDataItem } from './notificationItem'; +import { NotificationsManager, NotificationsSortMethod } from './notificationsManager'; +import { Disposable } from '../common/lifecycle'; import { CredentialStore } from '../github/credentials'; import { RepositoriesManager } from '../github/repositoriesManager'; import { chatCommand } from '../lm/utils'; -import { NotificationsDecorationProvider } from './notificationDecorationProvider'; -import { isNotificationTreeItem, NotificationTreeDataItem } from './notificationItem'; -import { NotificationsManager, NotificationsSortMethod } from './notificationsManager'; -import { NotificationsProvider } from './notificationsProvider'; export class NotificationsFeatureRegister extends Disposable { @@ -21,15 +21,10 @@ export class NotificationsFeatureRegister extends Disposable { constructor( readonly credentialStore: CredentialStore, private readonly _repositoriesManager: RepositoriesManager, - private readonly _telemetry: ITelemetry + private readonly _telemetry: ITelemetry, + notificationsManager: NotificationsManager ) { super(); - const notificationsProvider = new NotificationsProvider(credentialStore, this._repositoriesManager); - this._register(notificationsProvider); - - const notificationsManager = new NotificationsManager(notificationsProvider, credentialStore); - this._register(notificationsManager); - // Decorations const decorationsProvider = new NotificationsDecorationProvider(notificationsManager); this._register(vscode.window.registerFileDecorationProvider(decorationsProvider)); @@ -38,6 +33,7 @@ export class NotificationsFeatureRegister extends Disposable { this._register(vscode.window.createTreeView('notifications:github', { treeDataProvider: notificationsManager })); + notificationsManager.refresh(); // Commands this._register( @@ -101,7 +97,7 @@ export class NotificationsFeatureRegister extends Disposable { }) ); this._register( - vscode.commands.registerCommand('notification.markAsRead', (options: any) => { + vscode.commands.registerCommand('notification.markAsRead', (options: NotificationTreeDataItem) => { const { threadId, notificationKey } = this._extractMarkAsCommandOptions(options); /* __GDPR__ "notification.markAsRead" : {} @@ -111,7 +107,7 @@ export class NotificationsFeatureRegister extends Disposable { }) ); this._register( - vscode.commands.registerCommand('notification.markAsDone', (options: any) => { + vscode.commands.registerCommand('notification.markAsDone', (options: NotificationTreeDataItem) => { const { threadId, notificationKey } = this._extractMarkAsCommandOptions(options); /* __GDPR__ "notification.markAsDone" : {} @@ -122,22 +118,34 @@ export class NotificationsFeatureRegister extends Disposable { ); this._register( - vscode.commands.registerCommand('notifications.markMergedPullRequestsAsRead', () => { + vscode.commands.registerCommand('notifications.markPullRequestsAsRead', () => { /* __GDPR__ - "notifications.markMergedPullRequestsAsRead" : {} + "notifications.markPullRequestsAsRead" : {} */ - this._telemetry.sendTelemetryEvent('notifications.markMergedPullRequestsAsRead'); - return notificationsManager.markMergedPullRequest(); + this._telemetry.sendTelemetryEvent('notifications.markPullRequestsAsRead'); + return notificationsManager.markPullRequests(); }) ); this._register( - vscode.commands.registerCommand('notifications.markMergedPullRequestsAsDone', () => { + vscode.commands.registerCommand('notifications.markPullRequestsAsDone', () => { + /* __GDPR__ + "notifications.markPullRequestsAsDone" : {} + */ + this._telemetry.sendTelemetryEvent('notifications.markPullRequestsAsDone'); + return notificationsManager.markPullRequests(true); + }) + ); + this._register( + vscode.commands.registerCommand('notifications.configureNotificationsViewlet', () => { /* __GDPR__ - "notifications.markMergedPullRequestsAsDone" : {} + "notifications.configureNotificationsViewlet" : {} */ - this._telemetry.sendTelemetryEvent('notifications.markMergedPullRequestsAsDone'); - return notificationsManager.markMergedPullRequest(true); + this._telemetry.sendTelemetryEvent('notifications.configureNotificationsViewlet'); + return vscode.commands.executeCommand( + 'workbench.action.openSettings', + `@ext:${EXTENSION_ID} notifications`, + ); }) ); @@ -147,15 +155,16 @@ export class NotificationsFeatureRegister extends Disposable { })); } - private _extractMarkAsCommandOptions(options: any): { threadId: string, notificationKey: string } { + private _extractMarkAsCommandOptions(options: NotificationTreeDataItem | NotificationID | unknown): { threadId: string, notificationKey: string } { let threadId: string; let notificationKey: string; + const asID = options as Partial; if (isNotificationTreeItem(options)) { threadId = options.notification.id; notificationKey = options.notification.key; - } else if ('threadId' in options && 'notificationKey' in options && typeof options.threadId === 'number' && typeof options.notificationKey === 'string') { - threadId = options.threadId; - notificationKey = options.notificationKey; + } else if (asID.threadId !== undefined && asID.notificationKey !== undefined) { + threadId = asID.threadId; + notificationKey = asID.notificationKey; } else { throw new Error(`Invalid arguments for command notification.markAsRead : ${JSON.stringify(options)}`); } diff --git a/src/notifications/notificationsManager.ts b/src/notifications/notificationsManager.ts index 0cf865ff12..663971096a 100644 --- a/src/notifications/notificationsManager.ts +++ b/src/notifications/notificationsManager.ts @@ -4,19 +4,27 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { isNotificationTreeItem, NotificationTreeDataItem, NotificationTreeItem } from './notificationItem'; +import { NotificationsProvider } from './notificationsProvider'; +import { commands, contexts } from '../common/executeCommands'; import { Disposable } from '../common/lifecycle'; +import Logger from '../common/logger'; +import { NOTIFICATION_SETTING, NotificationVariants, PR_SETTINGS_NAMESPACE } from '../common/settingKeys'; import { EventType, TimelineEvent } from '../common/timelineEvent'; import { toNotificationUri } from '../common/uri'; import { CredentialStore } from '../github/credentials'; import { NotificationSubjectType } from '../github/interface'; import { IssueModel } from '../github/issueModel'; +import { issueMarkdown } from '../github/markdownUtils'; import { PullRequestModel } from '../github/pullRequestModel'; -import { isNotificationTreeItem, NotificationTreeDataItem, NotificationTreeItem } from './notificationItem'; -import { NotificationsProvider } from './notificationsProvider'; +import { PullRequestOverviewPanel } from '../github/pullRequestOverview'; +import { RepositoriesManager } from '../github/repositoriesManager'; export interface INotificationTreeItems { readonly notifications: NotificationTreeItem[]; readonly hasNextPage: boolean + readonly pollInterval: number; + readonly lastModified: string; } export enum NotificationsSortMethod { @@ -25,6 +33,8 @@ export enum NotificationsSortMethod { } export class NotificationsManager extends Disposable implements vscode.TreeDataProvider { + private static ID = 'NotificationsManager'; + private _onDidChangeTreeData: vscode.EventEmitter = this._register(new vscode.EventEmitter()); readonly onDidChangeTreeData: vscode.Event = this._onDidChangeTreeData.event; @@ -37,13 +47,33 @@ export class NotificationsManager extends Disposable implements vscode.TreeDataP private _fetchNotifications: boolean = false; private _notifications = new Map(); + private _pollingDuration: number = 60; // Default polling duration + private _pollingHandler: NodeJS.Timeout | null; + private _pollingLastModified: string; + private _sortingMethod: NotificationsSortMethod = NotificationsSortMethod.Timestamp; get sortingMethod(): NotificationsSortMethod { return this._sortingMethod; } - constructor(private readonly _notificationProvider: NotificationsProvider, private readonly _credentialStore: CredentialStore) { + constructor( + private readonly _notificationProvider: NotificationsProvider, + private readonly _credentialStore: CredentialStore, + private readonly _repositoriesManager: RepositoriesManager, + private readonly _context: vscode.ExtensionContext + ) { super(); this._register(this._onDidChangeTreeData); this._register(this._onDidChangeNotifications); + this._startPolling(); + this._register(vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(`${PR_SETTINGS_NAMESPACE}.${NOTIFICATION_SETTING}`)) { + if (this.isPRNotificationsOn() && !this._pollingHandler) { + this._startPolling(); + } + } + })); + this._register(PullRequestOverviewPanel.onVisible(e => { + this.markPrNotificationsAsRead(e); + })); } //#region TreeDataProvider @@ -72,6 +102,16 @@ export class NotificationsManager extends Disposable implements vscode.TreeDataP return this._resolveLoadMoreNotificationsTreeItem(); } + async resolveTreeItem( + item: vscode.TreeItem, + element: NotificationTreeDataItem, + ): Promise { + if (isNotificationTreeItem(element)) { + item.tooltip = await this._notificationMarkdownHover(element); + } + return item; + } + private _resolveNotificationTreeItem(element: NotificationTreeItem): vscode.TreeItem { const label = element.notification.subject.title; const item = new vscode.TreeItem(label, vscode.TreeItemCollapsibleState.None); @@ -119,15 +159,52 @@ export class NotificationsManager extends Disposable implements vscode.TreeDataP return item; } + private async _notificationMarkdownHover(element: NotificationTreeItem): Promise { + const markdown = new vscode.MarkdownString(undefined, true); + markdown.supportHtml = true; + + const notification = element.notification; + const model = element.model; + + // Add notification-specific information + if (notification.subject.type === NotificationSubjectType.Issue) { + const issueModel = model as IssueModel; + const issueMarkdownContent = await issueMarkdown(issueModel, this._context, this._repositoriesManager); + return issueMarkdownContent; + } else if (notification.subject.type === NotificationSubjectType.PullRequest) { + const prModel = model as PullRequestModel; + const prMarkdownContent = await issueMarkdown(prModel, this._context, this._repositoriesManager); + return prMarkdownContent; + } + + // Fallback for other types + const ownerName = `${notification.owner}/${notification.name}`; + markdown.appendMarkdown(`[${ownerName}](https://github.com/${ownerName}) \n`); + markdown.appendMarkdown(`**${notification.subject.title}** \n`); + markdown.appendMarkdown(`Type: ${notification.subject.type} \n`); + markdown.appendMarkdown(`Updated: ${notification.updatedAt.toLocaleString()} \n`); + markdown.appendMarkdown(`Reason: ${notification.reason} \n`); + + return markdown; + } + //#endregion + public get prNotifications(): PullRequestModel[] { + return Array.from(this._notifications.values()).filter(notification => notification.notification.subject.type === NotificationSubjectType.PullRequest).map(n => n.model) as PullRequestModel[]; + } + public async getNotifications(): Promise { + let pollInterval = this._pollingDuration; + let lastModified = this._pollingLastModified; if (this._fetchNotifications) { // Get raw notifications const notificationsData = await this._notificationProvider.getNotifications(this._dateTime.toISOString(), this._pageCount); if (!notificationsData) { return undefined; } + pollInterval = notificationsData.pollInterval; + lastModified = notificationsData.lastModified; // Resolve notifications const notificationTreeItems = new Map(); @@ -172,14 +249,23 @@ export class NotificationsManager extends Disposable implements vscode.TreeDataP } const notifications = Array.from(this._notifications.values()); + this._updateContext(); this._onDidChangeNotifications.fire(notifications); return { notifications: this._sortNotifications(notifications), - hasNextPage: this._hasNextPage + hasNextPage: this._hasNextPage, + pollInterval, + lastModified }; } + private _updateContext(): void { + const notificationCount = this._notifications.size; + commands.setContext(contexts.NOTIFICATION_COUNT, notificationCount === 0 ? -1 : notificationCount); + } + + public getNotification(key: string): NotificationTreeItem | undefined { return this._notifications.get(key); } @@ -218,6 +304,7 @@ export class NotificationsManager extends Disposable implements vscode.TreeDataP this._onDidChangeNotifications.fire([notification]); this._notifications.delete(notificationIdentifier.notificationKey); + this._updateContext(); this._refresh(false); } @@ -230,6 +317,7 @@ export class NotificationsManager extends Disposable implements vscode.TreeDataP this._onDidChangeNotifications.fire([notification]); this._notifications.delete(notificationIdentifier.notificationKey); + this._updateContext(); this._refresh(false); } @@ -246,7 +334,7 @@ export class NotificationsManager extends Disposable implements vscode.TreeDataP if (event.event === EventType.Committed) { if (userCheck(event.author.login)) { - return new Date(event.authoredDate); + return new Date(event.committedDate); } } else if (event.event === EventType.Commented) { if (userCheck(event.user?.login)) { @@ -262,8 +350,8 @@ export class NotificationsManager extends Disposable implements vscode.TreeDataP } } - public async markMergedPullRequest(markAsDone: boolean = false): Promise { - const filteredNotifications = Array.from(this._notifications.values()).filter(notification => notification.notification.subject.type === NotificationSubjectType.PullRequest && notification.model.isMerged); + public async markPullRequests(markAsDone: boolean = false): Promise { + const filteredNotifications = Array.from(this._notifications.values()).filter(notification => notification.notification.subject.type === NotificationSubjectType.PullRequest); const timlines = await Promise.all(filteredNotifications.map(notification => (notification.model as PullRequestModel).getTimelineEvents())); const markPromises: Promise[] = []; @@ -312,11 +400,86 @@ export class NotificationsManager extends Disposable implements vscode.TreeDataP private _sortNotifications(notifications: NotificationTreeItem[]): NotificationTreeItem[] { if (this._sortingMethod === NotificationsSortMethod.Timestamp) { - return notifications.sort((n1, n2) => n2.notification.updatedAd.getTime() - n1.notification.updatedAd.getTime()); + return notifications.sort((n1, n2) => n2.notification.updatedAt.getTime() - n1.notification.updatedAt.getTime()); } else if (this._sortingMethod === NotificationsSortMethod.Priority) { return notifications.sort((n1, n2) => Number(n2.priority) - Number(n1.priority)); } return notifications; } + + public isPRNotificationsOn() { + return (vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(NOTIFICATION_SETTING) === 'pullRequests'); + } + + private async _pollForNewNotifications() { + this._pageCount = 1; + this._dateTime = new Date(); + this._notifications.clear(); + this._fetchNotifications = true; + + const response = await this.getNotifications(); + if (!response) { + return; + } + + // Adapt polling interval if it has changed. + if (response.pollInterval !== this._pollingDuration) { + this._pollingDuration = response.pollInterval; + if (this._pollingHandler && this.isPRNotificationsOn()) { + Logger.appendLine('Notifications: Clearing interval', NotificationsManager.ID); + clearInterval(this._pollingHandler); + Logger.appendLine(`Notifications: Starting new polling interval with ${this._pollingDuration}`, NotificationsManager.ID); + this._startPolling(); + } + } + if (response.lastModified !== this._pollingLastModified) { + this._pollingLastModified = response.lastModified; + this._onDidChangeTreeData.fire(); + } + // this._onDidChangeNotifications.fire(oldPRNodesToUpdate); + } + + private _startPolling() { + if (!this.isPRNotificationsOn()) { + return; + } + this._pollForNewNotifications(); + this._pollingHandler = setInterval( + function (notificationProvider: NotificationsManager) { + notificationProvider._pollForNewNotifications(); + }, + this._pollingDuration * 1000, + this + ); + this._register({ dispose: () => clearInterval(this._pollingHandler!) }); + } + + private _findNotificationKeyForIssueModel(issueModel: IssueModel | PullRequestModel | { owner: string; repo: string; number: number }): string | undefined { + for (const [key, notification] of this._notifications.entries()) { + if ((issueModel instanceof IssueModel || issueModel instanceof PullRequestModel)) { + if (notification.model.equals(issueModel)) { + return key; + } + } else { + if (notification.notification.owner === issueModel.owner && + notification.notification.name === issueModel.repo && + notification.model.number === issueModel.number) { + return key; + } + } + } + return undefined; + } + + public markPrNotificationsAsRead(issueModel: IssueModel): void { + const notificationKey = this._findNotificationKeyForIssueModel(issueModel); + if (notificationKey) { + this.markAsRead({ threadId: this._notifications.get(notificationKey)!.notification.id, notificationKey }); + } + } + + public hasNotification(issueModel: IssueModel | PullRequestModel | { owner: string; repo: string; number: number }): boolean { + return this._findNotificationKeyForIssueModel(issueModel) !== undefined; + } } \ No newline at end of file diff --git a/src/notifications/notificationsProvider.ts b/src/notifications/notificationsProvider.ts index 2a8457b538..e1120bc882 100644 --- a/src/notifications/notificationsProvider.ts +++ b/src/notifications/notificationsProvider.ts @@ -4,8 +4,10 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { NotificationTreeItem } from './notificationItem'; import { AuthProvider } from '../common/authentication'; import { Disposable } from '../common/lifecycle'; +import Logger from '../common/logger'; import { EXPERIMENTAL_NOTIFICATIONS_PAGE_SIZE, PR_SETTINGS_NAMESPACE } from '../common/settingKeys'; import { OctokitCommon } from '../github/common'; import { CredentialStore, GitHub } from '../github/credentials'; @@ -15,11 +17,12 @@ import { PullRequestModel } from '../github/pullRequestModel'; import { RepositoriesManager } from '../github/repositoriesManager'; import { hasEnterpriseUri, parseNotification } from '../github/utils'; import { concatAsyncIterable } from '../lm/tools/toolsUtils'; -import { NotificationTreeItem } from './notificationItem'; export interface INotifications { readonly notifications: Notification[]; readonly hasNextPage: boolean; + readonly pollInterval: number; + readonly lastModified: string; } export interface INotificationPriority { @@ -29,27 +32,25 @@ export interface INotificationPriority { } export class NotificationsProvider extends Disposable { + private static readonly ID = 'NotificationsProvider'; private _authProvider: AuthProvider | undefined; - constructor( private readonly _credentialStore: CredentialStore, private readonly _repositoriesManager: RepositoriesManager ) { super(); - if (_credentialStore.isAuthenticated(AuthProvider.githubEnterprise) && hasEnterpriseUri()) { - this._authProvider = AuthProvider.githubEnterprise; - } else if (_credentialStore.isAuthenticated(AuthProvider.github)) { - this._authProvider = AuthProvider.github; - } + const setAuthProvider = () => { + if (_credentialStore.isAuthenticated(AuthProvider.githubEnterprise) && hasEnterpriseUri()) { + this._authProvider = AuthProvider.githubEnterprise; + } else if (_credentialStore.isAuthenticated(AuthProvider.github)) { + this._authProvider = AuthProvider.github; + } + }; + setAuthProvider(); this._register( _credentialStore.onDidChangeSessions(_ => { - if (_credentialStore.isAuthenticated(AuthProvider.githubEnterprise) && hasEnterpriseUri()) { - this._authProvider = AuthProvider.githubEnterprise; - } - if (_credentialStore.isAuthenticated(AuthProvider.github)) { - this._authProvider = AuthProvider.github; - } + setAuthProvider(); }) ); } @@ -101,7 +102,9 @@ export class NotificationsProvider extends Disposable { .map((notification: OctokitCommon.Notification) => parseNotification(notification)) .filter(notification => !!notification) as Notification[]; - return { notifications, hasNextPage: headers.link?.includes(`rel="next"`) === true }; + const pollInterval = Number(headers['x-poll-interval']); + Logger.debug(`Notifications: Fetched ${notifications.length} notifications. Poll interval: ${pollInterval}`, NotificationsProvider.ID); + return { notifications, hasNextPage: headers.link?.includes(`rel="next"`) === true, pollInterval, lastModified: headers['last-modified'] ?? '' }; } async getNotificationModel(notification: Notification): Promise | undefined> { @@ -114,9 +117,22 @@ export class NotificationsProvider extends Disposable { return undefined; } const folderManager = this._repositoriesManager.getManagerForRepository(notification.owner, notification.name) ?? this._repositoriesManager.folderManagers[0]; - const model = notification.subject.type === NotificationSubjectType.Issue ? - await folderManager.resolveIssue(notification.owner, notification.name, parseInt(issueOrPrNumber), true) : - await folderManager.resolvePullRequest(notification.owner, notification.name, parseInt(issueOrPrNumber)); + let model: IssueModel | undefined; + const isIssue = notification.subject.type === NotificationSubjectType.Issue; + + model = isIssue + ? await folderManager.resolveIssue(notification.owner, notification.name, parseInt(issueOrPrNumber), true, true) + : await folderManager.resolvePullRequest(notification.owner, notification.name, parseInt(issueOrPrNumber), true); + + if (model) { + const modelCheckedForUpdates = model.lastCheckedForUpdatesAt; + const notificationUpdated = notification.updatedAt; + if (notificationUpdated.getTime() > (modelCheckedForUpdates?.getTime() ?? 0)) { + model = isIssue + ? await folderManager.resolveIssue(notification.owner, notification.name, parseInt(issueOrPrNumber), true, false) + : await folderManager.resolvePullRequest(notification.owner, notification.name, parseInt(issueOrPrNumber), false); + } + } return model; } diff --git a/src/test/browser/index.ts b/src/test/browser/index.ts index 4a34fbef75..ead80a2baf 100644 --- a/src/test/browser/index.ts +++ b/src/test/browser/index.ts @@ -1,3 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// @ts-nocheck // This file is providing the test runner to use when running extension tests. import * as vscode from 'vscode'; require('mocha/mocha'); diff --git a/src/test/builders/graphql/latestReviewCommitBuilder.ts b/src/test/builders/graphql/latestReviewCommitBuilder.ts index a51807ae01..07ab147fbd 100644 --- a/src/test/builders/graphql/latestReviewCommitBuilder.ts +++ b/src/test/builders/graphql/latestReviewCommitBuilder.ts @@ -8,7 +8,7 @@ import { LatestReviewCommitResponse } from '../../../github/graphql'; import { RateLimitBuilder } from './rateLimitBuilder'; -type Repository = LatestReviewCommitResponse['repository']; +type Repository = NonNullable; type PullRequest = Repository['pullRequest']; type ViewerLatestReview = PullRequest['viewerLatestReview']; type Commit = ViewerLatestReview['commit']; diff --git a/src/test/builders/graphql/pullRequestBuilder.ts b/src/test/builders/graphql/pullRequestBuilder.ts index 72f7574001..cd8b73682e 100644 --- a/src/test/builders/graphql/pullRequestBuilder.ts +++ b/src/test/builders/graphql/pullRequestBuilder.ts @@ -37,7 +37,7 @@ const RefBuilder = createBuilderClass()({ }), }); -type Repository = PullRequestResponse['repository']; +type Repository = NonNullable; type PullRequest = Repository['pullRequest']; type Author = PullRequest['author']; type AssigneesConn = PullRequest['assignees']; @@ -110,7 +110,8 @@ export const PullRequestBuilder = createBuilderClass()({ } }), reactions: { default: { totalCount: 0 } }, - comments: { default: { totalCount: 0 } } + comments: { default: { totalCount: 0 } }, + reactionGroups: { default: [] }, }) }), rateLimit: { linked: RateLimitBuilder }, diff --git a/src/test/builders/graphql/timelineEventsBuilder.ts b/src/test/builders/graphql/timelineEventsBuilder.ts index 48eeb16e79..ec8a12b955 100644 --- a/src/test/builders/graphql/timelineEventsBuilder.ts +++ b/src/test/builders/graphql/timelineEventsBuilder.ts @@ -1,9 +1,14 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + import { createBuilderClass, createLink } from '../base'; import { TimelineEventsResponse } from '../../../github/graphql'; import { RateLimitBuilder } from './rateLimitBuilder'; -type Repository = TimelineEventsResponse['repository']; +type Repository = NonNullable; type PullRequest = Repository['pullRequest']; type TimelineConn = PullRequest['timelineItems']; diff --git a/src/test/builders/rest/pullRequestBuilder.ts b/src/test/builders/rest/pullRequestBuilder.ts index 70af54d5f8..d65f476c16 100644 --- a/src/test/builders/rest/pullRequestBuilder.ts +++ b/src/test/builders/rest/pullRequestBuilder.ts @@ -1,5 +1,10 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + import { UserBuilder } from './userBuilder'; -import { RefBuilder } from './refBuilder'; +import { NonNullUserRefBuilder, RefBuilder } from './refBuilder'; import { createLink, createBuilderClass } from '../base'; import { OctokitCommon } from '../../../github/common'; @@ -40,8 +45,8 @@ export const PullRequestBuilder = createBuilderClass()({ closed_at: { default: '' }, merged_at: { default: '' }, merge_commit_sha: { default: '' }, - head: { linked: RefBuilder }, - base: { linked: RefBuilder }, + head: { linked: NonNullUserRefBuilder }, + base: { linked: NonNullUserRefBuilder }, draft: { default: false }, merged: { default: false }, mergeable: { default: true }, diff --git a/src/test/builders/rest/refBuilder.ts b/src/test/builders/rest/refBuilder.ts index 6796e9a19f..8e4f070616 100644 --- a/src/test/builders/rest/refBuilder.ts +++ b/src/test/builders/rest/refBuilder.ts @@ -1,3 +1,8 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + import { UserBuilder } from './userBuilder'; import { RepositoryBuilder } from './repoBuilder'; import { createBuilderClass } from '../base'; @@ -5,7 +10,7 @@ import { OctokitCommon } from '../../../github/common'; type RefUnion = OctokitCommon.PullsListResponseItemHead & OctokitCommon.PullsListResponseItemBase; -export const RefBuilder = createBuilderClass()({ +export const RefBuilder = createBuilderClass>()({ label: { default: 'octocat:new-feature' }, ref: { default: 'new-feature' }, user: { linked: UserBuilder }, @@ -14,4 +19,15 @@ export const RefBuilder = createBuilderClass()({ repo: { linked: RepositoryBuilder }, }); +// Variant where user is guaranteed non-null. +type NonNullUserRef = Omit & { user: NonNullable }; + +export const NonNullUserRefBuilder = createBuilderClass()({ + label: { default: 'octocat:new-feature' }, + ref: { default: 'new-feature' }, + user: { linked: UserBuilder }, // non-null guarantee + sha: { default: '0000000000000000000000000000000000000000' }, + repo: { linked: RepositoryBuilder }, +}); + export type RefBuilder = InstanceType; diff --git a/src/test/builders/rest/repoBuilder.ts b/src/test/builders/rest/repoBuilder.ts index 2fc8038731..3f05aaf494 100644 --- a/src/test/builders/rest/repoBuilder.ts +++ b/src/test/builders/rest/repoBuilder.ts @@ -16,7 +16,7 @@ type License = RepoUnion['license']; type Permissions = RepoUnion['permissions']; type CodeOfConduct = RepoUnion['code_of_conduct']; -export const RepositoryBuilder = createBuilderClass()({ +export const RepositoryBuilder = createBuilderClass>()({ id: { default: 0 }, node_id: { default: 'node0' }, name: { default: 'reponame' }, @@ -123,9 +123,9 @@ export const RepositoryBuilder = createBuilderClass()({ name: { default: 'name' }, url: { default: 'https://github.com/octocat/reponame' }, }), - forks: { default: null }, - open_issues: { default: null }, - watchers: { default: null }, + forks: { default: 0 }, + open_issues: { default: 0 }, + watchers: { default: 0 }, }); export type RepositoryBuilder = InstanceType; diff --git a/src/test/builders/rest/userBuilder.ts b/src/test/builders/rest/userBuilder.ts index c1846eab71..412b21548d 100644 --- a/src/test/builders/rest/userBuilder.ts +++ b/src/test/builders/rest/userBuilder.ts @@ -17,7 +17,9 @@ type UserUnion = | OctokitCommon.PullsListResponseItemHeadRepoOwner | OctokitCommon.IssuesListEventsForTimelineResponseItemActor; -export const UserBuilder = createBuilderClass>()({ +type NonNullUser = NonNullable; + +export const UserBuilder = createBuilderClass()({ id: { default: 0 }, node_id: { default: 'node0' }, login: { default: 'octocat' }, diff --git a/src/test/common/fixtures/gitdiff/sessionParsing.ts b/src/test/common/fixtures/gitdiff/sessionParsing.ts new file mode 100644 index 0000000000..8ffc905a3f --- /dev/null +++ b/src/test/common/fixtures/gitdiff/sessionParsing.ts @@ -0,0 +1,23 @@ +export const simpleDiff = `diff --git a/src/file.ts b/src/file.ts +index 1234567..abcdefg 100644 +--- a/src/file.ts ++++ b/src/file.ts +@@ -1,4 +1,4 @@ + export function hello() { +- console.log('hello'); ++ console.log('hello world'); + }` + +export const diffHeaders = `diff --git a/package.json b/package.json +index 1111111..2222222 100644 +--- a/package.json ++++ b/package.json +@@ -1,5 +1,5 @@ + { + "name": "test" + }`; + +export const diffNoAts = `diff --git a/file.txt b/file.txt +index 1234567..abcdefg 100644 +--- a/file.txt ++++ b/file.txt`; \ No newline at end of file diff --git a/src/test/common/sessionParsing.test.ts b/src/test/common/sessionParsing.test.ts new file mode 100644 index 0000000000..0ea95e865d --- /dev/null +++ b/src/test/common/sessionParsing.test.ts @@ -0,0 +1,609 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { + parseSessionLogs, + parseToolCallDetails, + parseDiff, + toFileLabel, + SessionResponseLogChunk +} from '../../../common/sessionParsing'; +import { diffHeaders, diffNoAts, simpleDiff } from './fixtures/gitdiff/sessionParsing'; + +// Helper to construct a toolCall object +function makeToolCall(name: string, args: any): any { + return { + function: { name, arguments: JSON.stringify(args) }, + id: 'id_' + name + '_' + Math.random().toString(36).slice(2), + type: 'function', + index: 0 + }; +} + +describe('sessionParsing', function () { + describe('parseSessionLogs()', function () { + it('should parse valid session logs', function () { + const rawText = `data: {"choices":[{"finish_reason":"tool_calls","delta":{"content":"","role":"assistant","tool_calls":[{"function":{"arguments":"{\\"command\\": \\"view\\", \\"path\\": \\"/home/runner/work/repo/repo/src/file.ts\\"}","name":"str_replace_editor"},"id":"call_123","type":"function","index":0}]}}],"created":1640995200,"id":"chatcmpl-123","usage":{"completion_tokens":10,"prompt_tokens":50,"prompt_tokens_details":{"cached_tokens":0},"total_tokens":60},"model":"gpt-4","object":"chat.completion.chunk"}`; + + const result = parseSessionLogs(rawText); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].choices.length, 1); + assert.strictEqual(result[0].choices[0].finish_reason, 'tool_calls'); + assert.strictEqual(result[0].choices[0].delta.role, 'assistant'); + assert.strictEqual(result[0].choices[0].delta.tool_calls?.length, 1); + assert.strictEqual(result[0].choices[0].delta.tool_calls?.[0].function.name, 'str_replace_editor'); + }); + + it('should handle malformed JSON gracefully', function () { + const rawText = `data: {"invalid": "json" +data: {"choices":[{"finish_reason":"stop","delta":{"content":"Hello","role":"assistant"}}],"created":1640995200,"id":"chatcmpl-123","usage":{"completion_tokens":1,"prompt_tokens":10,"prompt_tokens_details":{"cached_tokens":0},"total_tokens":11},"model":"gpt-4","object":"chat.completion.chunk"}`; + + assert.throws(() => { + parseSessionLogs(rawText); + }); + }); + + it('should parse tool calls correctly', function () { + const rawText = `data: {"choices":[{"finish_reason":"tool_calls","delta":{"content":"","role":"assistant","tool_calls":[{"function":{"arguments":"{\\"command\\": \\"bash\\", \\"args\\": \\"ls -la\\"}","name":"bash"},"id":"call_456","type":"function","index":0}]}}],"created":1640995200,"id":"chatcmpl-456","usage":{"completion_tokens":5,"prompt_tokens":20,"prompt_tokens_details":{"cached_tokens":0},"total_tokens":25},"model":"gpt-4","object":"chat.completion.chunk"}`; + + const result = parseSessionLogs(rawText); + + assert.strictEqual(result.length, 1); + assert.strictEqual(result[0].choices[0].delta.tool_calls?.[0].function.name, 'bash'); + }); + + it('should filter out non-data lines', function () { + const rawText = `some random line +data: {"choices":[{"finish_reason":"stop","delta":{"content":"Hello","role":"assistant"}}],"created":1640995200,"id":"chatcmpl-123","usage":{"completion_tokens":1,"prompt_tokens":10,"prompt_tokens_details":{"cached_tokens":0},"total_tokens":11},"model":"gpt-4","object":"chat.completion.chunk"} +another non-data line`; + + const result = parseSessionLogs(rawText); + + assert.strictEqual(result.length, 1); + }); + }); + + describe('parseToolCallDetails()', function () { + it('should handle empty arguments string (covers ternary else)', function () { + // forces the ternary at line ~165 in sessionParsing.ts to take the else branch + const toolCall = { + function: { + name: 'str_replace_editor', + arguments: '' // empty string -> falsy -> args stays {} + }, + id: 'call_empty_args', + type: 'function', + index: 0 + }; + + const result = parseToolCallDetails(toolCall as any, ''); + assert.strictEqual(result.toolName, 'Edit'); + assert.strictEqual(result.invocationMessage, 'Edit'); + }); + it('should parse str_replace_editor tool calls with view command', function () { + const toolCall = { + function: { + name: 'str_replace_editor', + arguments: '{"command": "view", "path": "/home/runner/work/repo/repo/src/example.ts"}' + }, + id: 'call_123', + type: 'function', + index: 0 + }; + + const result = parseToolCallDetails(toolCall, ''); + + assert.strictEqual(result.toolName, 'Read'); + assert.strictEqual(result.invocationMessage, 'Read src/example.ts'); + assert.strictEqual(result.pastTenseMessage, 'Read src/example.ts'); + if (result.toolSpecificData && 'command' in result.toolSpecificData) { + assert.strictEqual(result.toolSpecificData.command, 'view'); + } + }); + + it('should parse str_replace_editor tool calls with edit command', function () { + const toolCall = { + function: { + name: 'str_replace_editor', + arguments: '{"command": "str_replace", "path": "/home/runner/work/repo/repo/src/example.ts"}' + }, + id: 'call_123', + type: 'function', + index: 0 + }; + + const result = parseToolCallDetails(toolCall, ''); + + assert.strictEqual(result.toolName, 'Edit'); + assert.strictEqual(result.invocationMessage, 'Edit [](src/example.ts)'); + assert.strictEqual(result.pastTenseMessage, 'Edit [](src/example.ts)'); + if (result.toolSpecificData && 'command' in result.toolSpecificData) { + assert.strictEqual(result.toolSpecificData.command, 'str_replace'); + } + }); + + it('should parse bash tool calls', function () { + const toolCall = { + function: { + name: 'bash', + arguments: '{"command": "npm test"}' + }, + id: 'call_456', + type: 'function', + index: 0 + }; + + const result = parseToolCallDetails(toolCall, 'Test output here'); + + assert.strictEqual(result.toolName, 'Run Bash command'); + assert.strictEqual(result.invocationMessage, '$ npm test\nTest output here'); + if (result.toolSpecificData && 'language' in result.toolSpecificData) { + assert.strictEqual(result.toolSpecificData.language, 'bash'); + assert.strictEqual(result.toolSpecificData.commandLine.original, 'npm test'); + } + }); + + it('should parse think tool calls', function () { + const toolCall = { + function: { + name: 'think', + arguments: '{}' + }, + id: 'call_789', + type: 'function', + index: 0 + }; + + const result = parseToolCallDetails(toolCall, 'I need to analyze this code'); + + assert.strictEqual(result.toolName, 'think'); + assert.strictEqual(result.invocationMessage, 'I need to analyze this code'); + }); + + it('should default think tool call to Thought when no args.thought and no content', function () { + const toolCall = { + function: { + name: 'think', + arguments: '{}' // no thought provided + }, + id: 'call_790', + type: 'function', + index: 0 + }; + + // Pass empty string content so code falls back to 'Thought' + const result = parseToolCallDetails(toolCall, ''); + assert.strictEqual(result.toolName, 'think'); + assert.strictEqual(result.invocationMessage, 'Thought'); + }); + + it('should parse report_progress tool calls', function () { + const toolCall = { + function: { + name: 'report_progress', + arguments: '{"prDescription": "Updated the test files", "commitMessage": "feat: add new tests"}' + }, + id: 'call_101', + type: 'function', + index: 0 + }; + + const result = parseToolCallDetails(toolCall, ''); + + assert.strictEqual(result.toolName, 'Progress Update'); + assert.strictEqual(result.invocationMessage, 'Updated the test files'); + assert.strictEqual(result.originMessage, 'Commit: feat: add new tests'); + }); + + it('report_progress falls back to content when prDescription empty string', function () { + // prDescription provided but empty => falsy, so chain uses content + const toolCall = { + function: { + name: 'report_progress', + arguments: '{"prDescription": ""}' + }, + id: 'call_102', + type: 'function', + index: 0 + }; + + const fallbackContent = 'Using content as progress update'; + const result = parseToolCallDetails(toolCall, fallbackContent); + assert.strictEqual(result.toolName, 'Progress Update'); + assert.strictEqual(result.invocationMessage, fallbackContent); + }); + + it('report_progress falls back to default when prDescription and content empty', function () { + // Both prDescription (empty string) and content ('') are falsy => 'Progress Update' + const toolCall = { + function: { + name: 'report_progress', + arguments: '{"prDescription": ""}' + }, + id: 'call_103', + type: 'function', + index: 0 + }; + + const result = parseToolCallDetails(toolCall, ''); + assert.strictEqual(result.toolName, 'Progress Update'); + assert.strictEqual(result.invocationMessage, 'Progress Update'); + }); + + it('should handle unknown tool types', function () { + const toolCall = { + function: { + name: 'unknown_tool', + arguments: '{"param": "value"}' + }, + id: 'call_999', + type: 'function', + index: 0 + }; + + const result = parseToolCallDetails(toolCall, 'some content'); + + assert.strictEqual(result.toolName, 'unknown_tool'); + assert.strictEqual(result.invocationMessage, 'some content'); + }); + + it('should handle malformed tool arguments', function () { + const toolCall = { + function: { + name: 'str_replace_editor', + arguments: '{"invalid": json}' + }, + id: 'call_error', + type: 'function', + index: 0 + }; + + const result = parseToolCallDetails(toolCall, ''); + + // Should fall back gracefully with empty args - goes to the 'else' branch which returns 'Edit' + assert.strictEqual(result.toolName, 'Edit'); + }); + + it('should handle repository root paths correctly', function () { + const toolCall = { + function: { + name: 'str_replace_editor', + arguments: '{"command": "view", "path": "/home/runner/work/repo/repo/"}' + }, + id: 'call_root', + type: 'function', + index: 0 + }; + + const result = parseToolCallDetails(toolCall, ''); + + assert.strictEqual(result.toolName, 'Read repository'); + assert.strictEqual(result.invocationMessage, 'Read repository'); + }); + + it('handles str_replace_editor view with diff-parsed content (empty file label -> repository)', function () { + const diff = [ + 'diff --git a/src/file.ts b/src/file.ts', + 'index 1111111..2222222 100644', + '--- a/src/file.ts', + '+++ b/src/file.ts', + '@@ -1,2 +1,2 @@', + '-old line', + '+new line' + ].join('\n'); + const toolCall = makeToolCall('str_replace_editor', { command: 'view', view_range: [1, 10] }); + const result = parseToolCallDetails(toolCall, diff); + assert.strictEqual(result.toolName, 'Read repository'); + assert.strictEqual(result.invocationMessage, 'Read repository'); + }); + + it('handles str_replace_editor view with diff-parsed content (non-empty file label)', function () { + const diff = [ + 'diff --git a/home/runner/work/repo/repo/src/deep/file.ts b/home/runner/work/repo/repo/src/deep/file.ts', + 'index 1111111..2222222 100644', + '--- a/home/runner/work/repo/repo/src/deep/file.ts', + '+++ b/home/runner/work/repo/repo/src/deep/file.ts', + '@@ -1,2 +1,2 @@', + '-old line', + '+new line' + ].join('\n'); + const toolCall = makeToolCall('str_replace_editor', { command: 'view', view_range: [2, 8] }); + const result = parseToolCallDetails(toolCall, diff); + assert.strictEqual(result.toolName, 'Read'); + assert.ok(result.invocationMessage.includes('src/deep/file.ts')); + assert.ok(result.invocationMessage.includes('lines 2 to 8')); + assert.ok(result.toolSpecificData && 'command' in result.toolSpecificData); + }); + + it('handles str_replace_editor view with diff-parsed content and no range (parsedRange undefined)', function () { + // This exercises the branch where parsedRange is falsy so no ", lines X to Y" suffix is appended + const diff = [ + 'diff --git a/home/runner/work/repo/repo/src/another/file.ts b/home/runner/work/repo/repo/src/another/file.ts', + 'index aaaaaaa..bbbbbbb 100644', + '--- a/home/runner/work/repo/repo/src/another/file.ts', + '+++ b/home/runner/work/repo/repo/src/another/file.ts', + '@@ -1,2 +1,2 @@', + '-old line', + '+new line' + ].join('\n'); + const toolCall = makeToolCall('str_replace_editor', { command: 'view' }); // no view_range provided + const result = parseToolCallDetails(toolCall, diff); + assert.strictEqual(result.toolName, 'Read'); + assert.ok(result.invocationMessage.includes('src/another/file.ts')); + assert.ok(!/lines \d+ to \d+/.test(result.invocationMessage), 'invocationMessage should not contain line range'); + assert.ok(result.pastTenseMessage && result.pastTenseMessage === result.invocationMessage); + }); + + it('handles str_replace_editor view with path but unparsable diff content (no diff headers)', function () { + const content = 'just some file content without diff headers'; + const toolCall = makeToolCall('str_replace_editor', { command: 'view', path: '/home/runner/work/repo/repo/src/other.ts' }); + const result = parseToolCallDetails(toolCall, content); + assert.strictEqual(result.toolName, 'Read'); + assert.strictEqual(result.invocationMessage, 'Read src/other.ts'); + }); + + it('handles str_replace_editor view with path and range (parsedRange defined)', function () { + // This covers the branch in sessionParsing.ts lines ~202-212 where parsedRange is defined + // and a normal file path (no diff content) is provided so invocationMessage includes the lines suffix. + const toolCall = makeToolCall('str_replace_editor', { command: 'view', path: '/home/runner/work/repo/repo/src/ranged.ts', view_range: [4, 9] }); + const result = parseToolCallDetails(toolCall, 'plain file content'); + assert.strictEqual(result.toolName, 'Read'); + assert.strictEqual(result.invocationMessage, 'Read src/ranged.ts, lines 4 to 9'); + assert.strictEqual(result.pastTenseMessage, 'Read src/ranged.ts, lines 4 to 9'); + assert.ok(result.toolSpecificData && 'viewRange' in result.toolSpecificData, 'Expected viewRange in toolSpecificData'); + if (result.toolSpecificData && 'viewRange' in result.toolSpecificData) { + assert.strictEqual(result.toolSpecificData.viewRange?.start, 4); + assert.strictEqual(result.toolSpecificData.viewRange?.end, 9); + } + }); + + it('handles str_replace_editor view with diff hunk but no diff header (fileA undefined)', function () { + // This diff content has an @@ hunk so parseDiff returns an object, but no 'diff --git' header, + // therefore fileA and fileB remain undefined. This exercises the fallback at line ~177 where + // file is chosen via parsedContent.fileA ?? parsedContent.fileB resulting in undefined and thus + // a repository-level read. + const diffOnlyHunk = [ + '@@ -1,2 +1,2 @@', + '-old line', + '+new line' + ].join('\n'); + const toolCall = makeToolCall('str_replace_editor', { command: 'view', view_range: [1, 2] }); + const result = parseToolCallDetails(toolCall, diffOnlyHunk); + // fileLabel is undefined so toolName is 'Read' but invocation message falls back to 'Read repository' + assert.strictEqual(result.toolName, 'Read'); + assert.strictEqual(result.invocationMessage, 'Read repository'); + }); + + it('handles str_replace_editor view with undefined path (no label)', function () { + const toolCall = makeToolCall('str_replace_editor', { command: 'view' }); + const result = parseToolCallDetails(toolCall, 'plain content'); + assert.strictEqual(result.toolName, 'Read repository'); + assert.strictEqual(result.invocationMessage, 'Read repository'); + }); + + it('handles str_replace_editor view with root repository path empty label branch', function () { + const toolCall = makeToolCall('str_replace_editor', { command: 'view', path: '/home/runner/work/repo/repo/' }); + const result = parseToolCallDetails(toolCall, 'content'); + assert.strictEqual(result.toolName, 'Read repository'); + }); + + it('handles str_replace_editor edit with range', function () { + const toolCall = makeToolCall('str_replace_editor', { command: 'edit', path: '/home/runner/work/repo/repo/src/editMe.ts', view_range: [5, 15] }); + const result = parseToolCallDetails(toolCall, ''); + assert.strictEqual(result.toolName, 'Edit'); + assert.ok(result.invocationMessage.includes('lines 5 to 15')); + }); + + it('handles str_replace_editor edit when args.command is undefined (defaults to edit)', function () { + // Covers sessionParsing.ts lines 220-230 where args.command || 'edit' supplies default + const toolCall = makeToolCall('str_replace_editor', { path: '/home/runner/work/repo/repo/src/implicitEdit.ts' }); + const result = parseToolCallDetails(toolCall, ''); + assert.strictEqual(result.toolName, 'Edit'); + assert.strictEqual(result.invocationMessage, 'Edit [](src/implicitEdit.ts)'); + assert.ok(result.toolSpecificData && 'command' in result.toolSpecificData, 'Expected toolSpecificData for edit operation'); + if (result.toolSpecificData && 'command' in result.toolSpecificData) { + assert.strictEqual(result.toolSpecificData.command, 'edit'); // default applied + } + }); + + it('handles str_replace (non-editor) path missing label fallback', function () { + // Provide a path that toFileLabel will still shorten; assert structure + const toolCall = makeToolCall('str_replace', { path: '/home/runner/work/repo/repo/src/x.ts' }); + const result = parseToolCallDetails(toolCall, ''); + assert.strictEqual(result.toolName, 'Edit'); + assert.strictEqual(result.invocationMessage, 'Edit [](src/x.ts)'); + }); + + it('handles str_replace with undefined path (fileLabel undefined)', function () { + // No path provided -> filePath undefined -> fileLabel undefined, should fall back to `Edit ${filePath}` which is 'Edit undefined' + const toolCall = makeToolCall('str_replace', { /* no path */ }); + const result = parseToolCallDetails(toolCall, ''); + assert.strictEqual(result.toolName, 'Edit'); + assert.strictEqual(result.invocationMessage, 'Edit undefined'); + assert.strictEqual(result.pastTenseMessage, 'Edit undefined'); + assert.strictEqual(result.toolSpecificData, undefined); + }); + + it('handles create tool call', function () { + const toolCall = makeToolCall('create', { path: '/home/runner/work/repo/repo/new/file.txt' }); + const result = parseToolCallDetails(toolCall, ''); + assert.strictEqual(result.toolName, 'Create'); + assert.strictEqual(result.invocationMessage, 'Create [](new/file.txt)'); + }); + + it('handles create tool call without path (fileLabel undefined)', function () { + const toolCall = makeToolCall('create', { /* no path provided */ }); + const result = parseToolCallDetails(toolCall, ''); + assert.strictEqual(result.toolName, 'Create'); + assert.strictEqual(result.invocationMessage, 'Create File undefined'); + assert.strictEqual(result.pastTenseMessage, 'Create File undefined'); + assert.strictEqual(result.toolSpecificData, undefined); + }); + + it('handles view tool call (non str_replace_editor) with range and root path giving repository label', function () { + const toolCall = makeToolCall('view', { path: '/home/runner/work/repo/repo/', view_range: [2, 3] }); + const result = parseToolCallDetails(toolCall, ''); + assert.strictEqual(result.toolName, 'Read repository'); + assert.strictEqual(result.invocationMessage, 'Read repository'); + }); + + it('handles view tool call (non str_replace_editor) with file path and range', function () { + const toolCall = makeToolCall('view', { path: '/home/runner/work/repo/repo/src/app.ts', view_range: [3, 7] }); + const result = parseToolCallDetails(toolCall, ''); + assert.strictEqual(result.toolName, 'Read'); + assert.ok(result.invocationMessage.includes('lines 3 to 7')); + }); + + it('handles view tool call (non str_replace_editor) with file path and no range (parsedRange undefined)', function () { + // Covers lines 261-275 in sessionParsing.ts where parsedRange is falsy, so + // the ", lines X to Y" suffix should NOT be appended. + const toolCall = makeToolCall('view', { path: '/home/runner/work/repo/repo/src/noRange.ts' }); + const result = parseToolCallDetails(toolCall, 'file content'); + assert.strictEqual(result.toolName, 'Read'); + assert.ok(result.invocationMessage === 'Read [](src/noRange.ts)', 'invocationMessage should not contain line range'); + assert.ok(result.pastTenseMessage === 'Read [](src/noRange.ts)', 'pastTenseMessage should not contain line range'); + assert.ok(result.toolSpecificData && 'viewRange' in result.toolSpecificData && !result.toolSpecificData.viewRange, 'viewRange should be undefined'); + }); + + it('handles bash tool call without command (only content)', function () { + const toolCall = makeToolCall('bash', {}); + const result = parseToolCallDetails(toolCall, 'only output'); + assert.strictEqual(result.toolName, 'Run Bash command'); + assert.strictEqual(result.invocationMessage, 'only output'); + assert.ok(!result.toolSpecificData); // no command so no toolSpecificData + }); + + it('handles bash tool call without command and without content (fallback to default message)', function () { + // Exercises bashContent empty so code uses 'Run Bash command' fallback (lines ~292-300) + const toolCall = makeToolCall('bash', {}); + const result = parseToolCallDetails(toolCall, ''); + assert.strictEqual(result.toolName, 'Run Bash command'); + assert.strictEqual(result.invocationMessage, 'Run Bash command'); + }); + + it('handles read_bash tool call', function () { + const toolCall = makeToolCall('read_bash', {}); + const result = parseToolCallDetails(toolCall, 'ignored'); + assert.strictEqual(result.toolName, 'read_bash'); + assert.strictEqual(result.invocationMessage, 'Read logs from Bash session'); + }); + + it('handles stop_bash tool call', function () { + const toolCall = makeToolCall('stop_bash', {}); + const result = parseToolCallDetails(toolCall, 'ignored'); + assert.strictEqual(result.toolName, 'stop_bash'); + assert.strictEqual(result.invocationMessage, 'Stop Bash session'); + }); + + it('handles unknown tool call with empty content falling back to name', function () { + const toolCall = makeToolCall('mystery_tool', { some: 'arg' }); + const result = parseToolCallDetails(toolCall, ''); + assert.strictEqual(result.toolName, 'mystery_tool'); + assert.strictEqual(result.invocationMessage, 'mystery_tool'); + }); + + it('handles unknown tool call with falsy name (empty string) returning unknown', function () { + // Directly craft toolCall without using makeToolCall so we can force empty name + const toolCall = { + function: { name: '', arguments: '{}' }, + id: 'call_empty_name', + type: 'function', + index: 0 + }; + const result = parseToolCallDetails(toolCall as any, ''); + assert.strictEqual(result.toolName, 'unknown'); + assert.strictEqual(result.invocationMessage, 'unknown'); + }); + + it('gracefully handles invalid JSON arguments for non-view str_replace_editor (edit path undefined)', function () { + const toolCall = { + function: { name: 'str_replace_editor', arguments: '{"command": "edit", invalid' }, + id: 'bad_json', + type: 'function', + index: 0 + }; + // Since JSON parse fails, args becomes {} and we are in else branch -> toolName Edit without file label + const result = parseToolCallDetails(toolCall as any, ''); + assert.strictEqual(result.toolName, 'Edit'); + assert.strictEqual(result.invocationMessage, 'Edit'); + }); + + it('handles str_replace_editor view with no path and no range (fileLabel undefined branch)', function () { + // Triggers the branch where args.path is undefined and thus fileLabel is undefined + const toolCall = makeToolCall('str_replace_editor', { command: 'view', path: '' }); + const result = parseToolCallDetails(toolCall, 'plain non-diff content'); + assert.strictEqual(result.toolName, 'Read repository'); + assert.strictEqual(result.invocationMessage, 'Read repository'); + assert.strictEqual(result.pastTenseMessage, 'Read repository'); + }); + }); + + describe('parseDiff()', function () { + it('should parse diff content correctly', function () { + const result = parseDiff(simpleDiff); + + assert(result); + assert.strictEqual(result.fileA, '/src/file.ts'); + assert.strictEqual(result.fileB, '/src/file.ts'); + assert(result.content.includes("export function hello()")); + assert(result.content.includes("console.log('hello world')")); + }); + + it('should extract file paths from diff headers', function () { + const result = parseDiff(diffHeaders); + + assert(result); + assert.strictEqual(result.fileA, '/package.json'); + assert.strictEqual(result.fileB, '/package.json'); + }); + + it('should handle malformed diffs', function () { + const diffContent = `not a diff at all`; + + const result = parseDiff(diffContent); + + assert.strictEqual(result, undefined); + }); + + it('should handle diffs without @@ lines', function () { + const result = parseDiff(diffNoAts); + + assert.strictEqual(result, undefined); + }); + }); + + describe('toFileLabel()', function () { + it('should convert absolute paths to relative labels', function () { + const path = '/home/runner/work/repo/repo/src/components/Button.tsx'; + + const result = toFileLabel(path); + + assert.strictEqual(result, 'src/components/Button.tsx'); + }); + + it('should handle various path formats', function () { + assert.strictEqual(toFileLabel('/home/runner/work/repo/repo/package.json'), 'package.json'); + assert.strictEqual(toFileLabel('/home/runner/work/repo/repo/src/index.ts'), 'src/index.ts'); + assert.strictEqual(toFileLabel('/home/runner/work/repo/repo/docs/README.md'), 'docs/README.md'); + }); + + it('should handle edge cases', function () { + assert.strictEqual(toFileLabel('/home/runner/work/repo/repo/'), ''); + assert.strictEqual(toFileLabel('/'), ''); + assert.strictEqual(toFileLabel(''), ''); + }); + + it('should handle shorter paths', function () { + const shortPath = '/home/runner/work/repo'; + + const result = toFileLabel(shortPath); + + // Should return empty string when path is too short + assert.strictEqual(result, ''); + }); + }); +}); 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/test/extension.isSubmodule.test.ts b/src/test/extension.isSubmodule.test.ts new file mode 100644 index 0000000000..c29ce07a37 --- /dev/null +++ b/src/test/extension.isSubmodule.test.ts @@ -0,0 +1,121 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Repository, Submodule } from '../api/api'; +import { GitApiImpl } from '../api/api1'; +import { isSubmodule } from '../common/gitUtils' + +describe('isSubmodule Tests', function () { + it('should return false for repositories with no submodules', () => { + const mockRepo: Repository = { + rootUri: vscode.Uri.file('/home/user/repo1'), + state: { + submodules: [], + remotes: [], + HEAD: undefined, + rebaseCommit: undefined, + mergeChanges: [], + indexChanges: [], + workingTreeChanges: [], + onDidChange: new vscode.EventEmitter().event, + }, + } as Partial as Repository; + + const mockGit: GitApiImpl = { + repositories: [mockRepo], + } as GitApiImpl; + + const result = isSubmodule(mockRepo, mockGit); + assert.strictEqual(result, false); + }); + + it('should return true when repository is listed as submodule in another repo', () => { + const submoduleRepo: Repository = { + rootUri: vscode.Uri.file('/home/user/parent/submodule'), + state: { + submodules: [], + remotes: [], + HEAD: undefined, + rebaseCommit: undefined, + mergeChanges: [], + indexChanges: [], + workingTreeChanges: [], + onDidChange: new vscode.EventEmitter().event, + }, + } as Partial as Repository; + + const parentRepo: Repository = { + rootUri: vscode.Uri.file('/home/user/parent'), + state: { + submodules: [ + { + name: 'submodule', + path: 'submodule', + url: 'https://github.com/example/submodule.git' + } as Submodule + ], + remotes: [], + HEAD: undefined, + rebaseCommit: undefined, + mergeChanges: [], + indexChanges: [], + workingTreeChanges: [], + onDidChange: new vscode.EventEmitter().event, + }, + } as Partial as Repository; + + const mockGit: GitApiImpl = { + repositories: [parentRepo, submoduleRepo], + } as GitApiImpl; + + const result = isSubmodule(submoduleRepo, mockGit); + assert.strictEqual(result, true); + }); + + it('should return false when repository is not listed as submodule', () => { + const repo1: Repository = { + rootUri: vscode.Uri.file('/home/user/repo1'), + state: { + submodules: [], + remotes: [], + HEAD: undefined, + rebaseCommit: undefined, + mergeChanges: [], + indexChanges: [], + workingTreeChanges: [], + onDidChange: new vscode.EventEmitter().event, + }, + } as Partial as Repository; + + const repo2: Repository = { + rootUri: vscode.Uri.file('/home/user/repo2'), + state: { + submodules: [ + { + name: 'different-submodule', + path: 'different-submodule', + url: 'https://github.com/example/different.git' + } as Submodule + ], + remotes: [], + HEAD: undefined, + rebaseCommit: undefined, + mergeChanges: [], + indexChanges: [], + workingTreeChanges: [], + onDidChange: new vscode.EventEmitter().event, + }, + } as Partial as Repository; + + const mockGit: GitApiImpl = { + repositories: [repo1, repo2], + } as GitApiImpl; + + const result = isSubmodule(repo1, mockGit); + assert.strictEqual(result, false); + }); +}); diff --git a/src/test/github/copilotPrWatcher.test.ts b/src/test/github/copilotPrWatcher.test.ts new file mode 100644 index 0000000000..5df018ce63 --- /dev/null +++ b/src/test/github/copilotPrWatcher.test.ts @@ -0,0 +1,140 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { CopilotStateModel } from '../../github/copilotPrWatcher'; +import { CopilotPRStatus } from '../../common/copilot'; +import { PullRequestModel } from '../../github/pullRequestModel'; + +describe('Copilot PR watcher', () => { + + describe('CopilotStateModel', () => { + + const createPullRequest = (owner: string, repo: string, number: number): PullRequestModel => { + return { + number, + remote: { owner, repositoryName: repo }, + author: { login: 'copilot' } + } as unknown as PullRequestModel; + }; + + it('stores statuses and emits notifications after initialization', () => { + const model = new CopilotStateModel(); + let changeEvents = 0; + const notifications: PullRequestModel[][] = []; + model.onDidChangeCopilotStates(() => changeEvents++); + model.onDidChangeCopilotNotifications(items => notifications.push(items)); + + const pr = createPullRequest('octo', 'repo', 1); + model.set([{ item: pr, status: CopilotPRStatus.Started }]); + + assert.strictEqual(model.get('octo', 'repo', 1), CopilotPRStatus.Started); + assert.strictEqual(changeEvents, 1); + assert.strictEqual(notifications.length, 0); + assert.strictEqual(model.notifications.size, 0); + + model.set([{ item: pr, status: CopilotPRStatus.Started }]); + assert.strictEqual(changeEvents, 1); + + model.setInitialized(); + const updated = createPullRequest('octo', 'repo', 1); + model.set([{ item: updated, status: CopilotPRStatus.Completed }]); + + assert.strictEqual(model.get('octo', 'repo', 1), CopilotPRStatus.Completed); + assert.strictEqual(changeEvents, 2); + assert.strictEqual(notifications.length, 1); + assert.deepStrictEqual(notifications[0], [updated]); + assert.ok(model.notifications.has('octo/repo#1')); + }); + + it('deletes keys and clears related notifications', () => { + const model = new CopilotStateModel(); + let changeEvents = 0; + const notifications: PullRequestModel[][] = []; + model.onDidChangeCopilotStates(() => changeEvents++); + model.onDidChangeCopilotNotifications(items => notifications.push(items)); + + model.setInitialized(); + const pr = createPullRequest('octo', 'repo', 42); + model.set([{ item: pr, status: CopilotPRStatus.Started }]); + + assert.strictEqual(model.notifications.size, 1); + assert.strictEqual(changeEvents, 1); + + model.deleteKey('octo/repo#42'); + assert.strictEqual(model.get('octo', 'repo', 42), CopilotPRStatus.None); + assert.strictEqual(changeEvents, 2); + assert.strictEqual(model.notifications.size, 0); + assert.strictEqual(notifications.length, 2); + assert.deepStrictEqual(notifications[1], [pr]); + assert.deepStrictEqual(model.keys(), []); + }); + + it('clears individual notifications and reports changes', () => { + const model = new CopilotStateModel(); + const notifications: PullRequestModel[][] = []; + model.onDidChangeCopilotNotifications(items => notifications.push(items)); + + model.setInitialized(); + const pr = createPullRequest('octo', 'repo', 5); + model.set([{ item: pr, status: CopilotPRStatus.Started }]); + assert.strictEqual(model.notifications.size, 1); + assert.strictEqual(notifications.length, 1); + + model.clearNotification('octo', 'repo', 5); + assert.strictEqual(model.notifications.size, 0); + assert.strictEqual(notifications.length, 2); + assert.deepStrictEqual(notifications[1], [pr]); + + model.clearNotification('octo', 'repo', 5); + assert.strictEqual(notifications.length, 2); + }); + + it('supports clearing notifications by repository or entirely', () => { + const model = new CopilotStateModel(); + const notifications: PullRequestModel[][] = []; + model.onDidChangeCopilotNotifications(items => notifications.push(items)); + + assert.strictEqual(model.isInitialized, false); + model.setInitialized(); + assert.strictEqual(model.isInitialized, true); + + const prOne = createPullRequest('octo', 'repo', 1); + const prTwo = createPullRequest('octo', 'repo', 2); + const prThree = createPullRequest('other', 'repo', 3); + model.set([ + { item: prOne, status: CopilotPRStatus.Started }, + { item: prTwo, status: CopilotPRStatus.Failed }, + { item: prThree, status: CopilotPRStatus.Completed } + ]); + + assert.strictEqual(model.notifications.size, 3); + assert.strictEqual(notifications.length, 1); + assert.deepStrictEqual(notifications[0], [prOne, prTwo, prThree]); + assert.strictEqual(model.getNotificationsCount('octo', 'repo'), 2); + assert.deepStrictEqual(model.keys().sort(), ['octo/repo#1', 'octo/repo#2', 'other/repo#3']); + + model.clearAllNotifications('octo', 'repo'); + assert.strictEqual(model.notifications.size, 1); + assert.strictEqual(model.getNotificationsCount('octo', 'repo'), 0); + assert.strictEqual(notifications.length, 2); + assert.deepStrictEqual(notifications[1], [prOne, prTwo]); + + model.clearAllNotifications(); + assert.strictEqual(model.notifications.size, 0); + assert.strictEqual(notifications.length, 3); + assert.deepStrictEqual(notifications[2], [prThree]); + + const counts = model.getCounts('octo', 'repo'); + assert.deepStrictEqual(counts, { total: 3, inProgress: 1, error: 1 }); + + const allStates = model.all; + assert.strictEqual(allStates.length, 3); + assert.deepStrictEqual(allStates.map(v => v.status).sort(), [CopilotPRStatus.Started, CopilotPRStatus.Completed, CopilotPRStatus.Failed]); + }); + }); + + +}); \ No newline at end of file diff --git a/src/test/github/copilotRemoteAgent.test.ts b/src/test/github/copilotRemoteAgent.test.ts new file mode 100644 index 0000000000..f0618b8bb1 --- /dev/null +++ b/src/test/github/copilotRemoteAgent.test.ts @@ -0,0 +1,344 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { SinonSandbox, createSandbox } from 'sinon'; +import * as vscode from 'vscode'; +import { CopilotRemoteAgentManager, SessionIdForPr } from '../../github/copilotRemoteAgent'; +import { MockCommandRegistry } from '../mocks/mockCommandRegistry'; +import { MockTelemetry } from '../mocks/mockTelemetry'; +import { CredentialStore } from '../../github/credentials'; +import { RepositoriesManager } from '../../github/repositoriesManager'; +import { MockExtensionContext } from '../mocks/mockExtensionContext'; +import { PullRequestModel } from '../../github/pullRequestModel'; +import { MockGitHubRepository } from '../mocks/mockGitHubRepository'; +import { GitHubRemote } from '../../common/remote'; +import { Protocol } from '../../common/protocol'; +import { GitHubServerType } from '../../common/authentication'; +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'); +const remote = new GitHubRemote('test', 'github/test', protocol, GitHubServerType.GitHubDotCom); + +describe('CopilotRemoteAgentManager', function () { + let sinon: SinonSandbox; + let manager: CopilotRemoteAgentManager; + let credentialStore: CredentialStore; + let reposManager: RepositoriesManager; + let context: MockExtensionContext; + let mockRepo: MockGitHubRepository; + let gitAPIImp: GitApiImpl; + let mockPrsTreeModel: MockPrsTreeModel; + + beforeEach(function () { + sinon = createSandbox(); + MockCommandRegistry.install(sinon); + + // Mock workspace configuration to return disabled by default + sinon.stub(vscode.workspace, 'getConfiguration').callsFake((section?: string) => ({ + get: sinon.stub().callsFake((key: string, defaultValue?: any) => { + if (section === 'githubPR.copilotRemoteAgent' && key === 'enabled') { + return false; // Default to disabled + } + if (section === 'githubPR.copilotRemoteAgent' && key === 'autoCommitAndPushEnabled') { + return false; + } + if (section === 'githubPR.copilotRemoteAgent' && key === 'promptForConfirmation') { + return true; + } + return defaultValue; + }), + update: sinon.stub().resolves(), + has: sinon.stub().returns(true), + inspect: sinon.stub() + } as any)); + + context = new MockExtensionContext(); + credentialStore = new CredentialStore(telemetry, context); + reposManager = new RepositoriesManager(credentialStore, telemetry); + + mockRepo = new MockGitHubRepository(remote, credentialStore, telemetry, sinon); + + gitAPIImp = new GitApiImpl(reposManager); + + mockPrsTreeModel = new MockPrsTreeModel(); + manager = new CopilotRemoteAgentManager(credentialStore, reposManager, telemetry, context, gitAPIImp, mockPrsTreeModel as unknown as PrsTreeModel); + }); + + afterEach(function () { + manager.dispose(); + reposManager.dispose(); + credentialStore.dispose(); + context.dispose(); + mockRepo.dispose(); + sinon.restore(); + }); + + describe('enabled', function () { + it('should return false when coding agent is disabled by default', function () { + // The config should default to disabled state + const enabled = manager.enabled; + assert.strictEqual(enabled, false); + }); + + it('should reflect configuration changes', function () { + // Test would require mocking workspace configuration + // For now, just test the getter exists + assert.strictEqual(typeof manager.enabled, 'boolean'); + }); + }); + + describe('autoCommitAndPushEnabled', function () { + it('should return boolean value', function () { + const autoCommitEnabled = manager.autoCommitAndPushEnabled; + assert.strictEqual(typeof autoCommitEnabled, 'boolean'); + }); + }); + + describe('isAssignable()', function () { + it('should return false when no repository info is available', async function () { + // No folder managers setup + const result = await manager.isAssignable(); + assert.strictEqual(result, false); + }); + + it('should return false when assignable users cannot be fetched', async function () { + // Mock repository manager state but no assignable users + sinon.stub(manager, 'repoInfo').resolves(undefined); + + const result = await manager.isAssignable(); + assert.strictEqual(result, false); + }); + }); + + describe('isAvailable()', function () { + it('should return false when manager is disabled', async function () { + sinon.stub(manager, 'enabled').get(() => false); + + const result = await manager.isAvailable(); + assert.strictEqual(result, false); + }); + + it('should return false when no repo info is available', async function () { + sinon.stub(manager, 'enabled').get(() => true); + sinon.stub(manager, 'repoInfo').resolves(undefined); + + const result = await manager.isAvailable(); + assert.strictEqual(result, false); + }); + + it('should return false when copilot API is not available', async function () { + sinon.stub(manager, 'enabled').get(() => true); + sinon.stub(manager, 'repoInfo').resolves({ + owner: 'test', + repo: 'test', + baseRef: 'main', + remote: remote, + repository: {} as any, + ghRepository: mockRepo, + fm: {} as any + }); + // copilotApi will return undefined by default in tests + + const result = await manager.isAvailable(); + assert.strictEqual(result, false); + }); + }); + + describe('repoInfo()', function () { + it('should return undefined when no folder managers exist', async function () { + const result = await manager.repoInfo(); + assert.strictEqual(result, undefined); + }); + + it('should return undefined when no repository is found', async function () { + // Mock empty folder managers + sinon.stub(reposManager, 'folderManagers').get(() => []); + + const result = await manager.repoInfo(); + assert.strictEqual(result, undefined); + }); + }); + + describe('addFollowUpToExistingPR()', function () { + it('should return undefined when no repo info is available', async function () { + sinon.stub(manager, 'repoInfo').resolves(undefined); + + const result = await manager.addFollowUpToExistingPR(123, 'test prompt'); + assert.strictEqual(result, undefined); + }); + + it('should return undefined when PR is not found', async function () { + sinon.stub(manager, 'repoInfo').resolves({ + owner: 'test', + repo: 'test', + baseRef: 'main', + remote: remote, + repository: {} as any, + ghRepository: mockRepo, + fm: {} as any + }); + + sinon.stub(mockRepo, 'getPullRequest').resolves(undefined); + + const result = await manager.addFollowUpToExistingPR(123, 'test prompt'); + assert.strictEqual(result, undefined); + }); + }); + + describe('invokeRemoteAgent()', function () { + it('should return error when copilot API is not available', async function () { + const result = await manager.invokeRemoteAgent('test prompt', 'test context'); + + assert.strictEqual(result.state, 'error'); + if (result.state === 'error') { + assert(result.error.includes('Failed to initialize Copilot API')); + } + }); + + it('should return error when no repository info is available', async function () { + // Mock copilot API to be available but no repo info + sinon.stub(manager as any, '_copilotApiPromise').value(Promise.resolve({} as any)); + sinon.stub(manager, 'repoInfo').resolves(undefined); + + const result = await manager.invokeRemoteAgent('test prompt', 'test context'); + + assert.strictEqual(result.state, 'error'); + }); + }); + + describe('getSessionLogsFromAction()', function () { + it('should return empty array when copilot API is not available', async function () { + const mockPr = {} as PullRequestModel; + + const result = await manager.getSessionLogsFromAction(mockPr); + + assert.strictEqual(Array.isArray(result), true); + assert.strictEqual(result.length, 0); + }); + }); + + describe('getWorkflowStepsFromAction()', function () { + it('should return empty array when no workflow run is found', async function () { + const mockPr = {} as PullRequestModel; + sinon.stub(manager, 'getLatestCodingAgentFromAction').resolves(undefined); + + const result = await manager.getWorkflowStepsFromAction(mockPr); + + assert.strictEqual(Array.isArray(result), true); + assert.strictEqual(result.length, 0); + }); + }); + + describe('getSessionLogFromPullRequest()', function () { + it('should return undefined when copilot API is not available', async function () { + const mockPr = {} as PullRequestModel; + + const result = await manager.getSessionLogFromPullRequest(mockPr); + + assert.strictEqual(result, undefined); + }); + }); + + describe('provideChatSessions()', function () { + it('should return empty array when copilot API is not available', async function () { + const token = new vscode.CancellationTokenSource().token; + + const result = await manager.provideChatSessions(token); + + assert.strictEqual(Array.isArray(result), true); + assert.strictEqual(result.length, 0); + }); + + it('should return empty array when cancellation is requested', async function () { + const tokenSource = new vscode.CancellationTokenSource(); + tokenSource.cancel(); + + const result = await manager.provideChatSessions(tokenSource.token); + + assert.strictEqual(Array.isArray(result), true); + assert.strictEqual(result.length, 0); + }); + }); + + describe('provideChatSessionContent()', 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(SessionIdForPr.getResource(123, 0), token); + + assert.strictEqual(Array.isArray(result.history), true); + assert.strictEqual(result.history.length, 0); + assert.strictEqual(result.requestHandler, undefined); + }); + + it('should return empty session when cancellation is requested', async function () { + const tokenSource = new vscode.CancellationTokenSource(); + tokenSource.cancel(); + + const result = await manager.provideChatSessionContent(SessionIdForPr.getResource(123, 0), tokenSource.token); + + assert.strictEqual(Array.isArray(result.history), true); + assert.strictEqual(result.history.length, 0); + }); + + it('should return empty session for invalid PR number', async function () { + const token = new vscode.CancellationTokenSource().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); + }); + }); + + describe('event handlers', function () { + it('should expose onDidChangeStates event', function () { + assert.strictEqual(typeof manager.onDidChangeStates, 'function'); + }); + + it('should expose onDidChangeNotifications event', function () { + assert.strictEqual(typeof manager.onDidChangeNotifications, 'function'); + }); + + it('should expose onDidCreatePullRequest event', function () { + assert.strictEqual(typeof manager.onDidCreatePullRequest, 'function'); + }); + + it('should expose onDidChangeChatSessions event', function () { + assert.strictEqual(typeof manager.onDidChangeChatSessions, 'function'); + }); + }); + + describe('waitRepoManagerInitialization()', function () { + it('should resolve immediately when repos are loaded', async function () { + // Mock the state as already loaded + sinon.stub(reposManager, 'state').get(() => ReposManagerState.RepositoriesLoaded); + + // This should resolve quickly + const startTime = Date.now(); + await (manager as any).waitRepoManagerInitialization(); + const endTime = Date.now(); + + // Should be very fast since it should return immediately + assert(endTime - startTime < 100); + }); + + it('should resolve immediately when authentication is needed', async function () { + sinon.stub(reposManager, 'state').get(() => ReposManagerState.NeedsAuthentication); + + const startTime = Date.now(); + await (manager as any).waitRepoManagerInitialization(); + const endTime = Date.now(); + + assert(endTime - startTime < 100); + }); + }); +}); diff --git a/src/test/github/copilotRemoteAgentUtils.test.ts b/src/test/github/copilotRemoteAgentUtils.test.ts new file mode 100644 index 0000000000..1008fcfc2b --- /dev/null +++ b/src/test/github/copilotRemoteAgentUtils.test.ts @@ -0,0 +1,98 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { truncatePrompt, extractTitle, formatBodyPlaceholder } from '../../github/copilotRemoteAgentUtils'; +import { MAX_PROBLEM_STATEMENT_LENGTH } from '../../github/copilotApi'; + +describe('copilotRemoteAgentUtils', () => { + + describe('truncatePrompt', () => { + it('should return prompt and context unchanged when under limit', () => { + const prompt = 'This is a short prompt'; + const context = 'This is some additional context'; + const result = truncatePrompt(prompt, context); + assert.strictEqual(result.problemStatement, `${prompt}\n\n${context}`); + assert.strictEqual(result.isTruncated, false); + }); + + it('should return only prompt when no context provided and under limit', () => { + const prompt = 'This is a short prompt'; + const result = truncatePrompt(prompt); + assert.strictEqual(result.problemStatement, 'This is a short prompt'); + assert.strictEqual(result.isTruncated, false); + }); + + it('should truncate prompt when it exceeds the maximum length', () => { + const longPrompt = 'a'.repeat(MAX_PROBLEM_STATEMENT_LENGTH + 100); + const result = truncatePrompt(longPrompt); + assert.strictEqual(result.problemStatement.length, MAX_PROBLEM_STATEMENT_LENGTH); + assert.strictEqual(result.isTruncated, true); + }); + + it('should truncate context when combined length exceeds limit', () => { + const prompt = 'Short prompt'; + const longContext = 'b'.repeat(MAX_PROBLEM_STATEMENT_LENGTH); + + const result = truncatePrompt(prompt, longContext); + + assert.strictEqual(result.isTruncated, true); + assert(result.problemStatement.startsWith(prompt)); + assert(result.problemStatement.includes('\n\n')); + const expectedAvailableLength = MAX_PROBLEM_STATEMENT_LENGTH - prompt.length; + const expectedContext = longContext.slice(-expectedAvailableLength + 2); + assert.strictEqual(result.problemStatement, `${prompt}\n\n${expectedContext}`); + }); + + it('long prompts are prioritized when truncating', () => { + const longPrompt = 'a'.repeat(MAX_PROBLEM_STATEMENT_LENGTH + 100); + const context = 'B'; + + const result = truncatePrompt(longPrompt, context); + + assert.strictEqual(result.isTruncated, true); + assert.strictEqual(result.problemStatement.length, MAX_PROBLEM_STATEMENT_LENGTH); + assert(!result.problemStatement.includes(context)); + }); + }); + + describe('extractTitle', () => { + it('should extract title from context with TITLE prefix', () => { + const context = 'Some initial text\nTITLE: Fix authentication bug\nSome other content'; + const result = extractTitle('', context); + assert.strictEqual(result, 'Fix authentication bug'); + }); + + it('should extract title with case insensitive matching', () => { + const context = 'Some text\ntitle: Add new feature\nMore text'; + const result = extractTitle('', context); + assert.strictEqual(result, 'Add new feature'); + }); + + it('should extract title with extra whitespace', () => { + const context = 'TITLE: Refactor code structure \n'; + const result = extractTitle('', context); + assert.strictEqual(result, 'Refactor code structure'); + }); + + it('should use prompt when no title is found', () => { + const context = 'Some text without any title marker\nJust regular content'; + const result = extractTitle('Default Title', context); + assert.strictEqual(result, 'Default Title'); + }); + + it('should use prompt when context is undefined', () => { + const result = extractTitle('Default Title', undefined); + assert.strictEqual(result, 'Default Title'); + }); + + it('should return truncated title when context is empty string', () => { + const title = 'TEST TEST TEST TEST TEST TEST'; // will truncate to 20 characters + const result = extractTitle(title, ''); + + assert.strictEqual(result, 'TEST TEST TEST TEST ...'); + }); + }); +}); \ No newline at end of file diff --git a/src/test/github/folderRepositoryManager.test.ts b/src/test/github/folderRepositoryManager.test.ts index 604c5243b8..ad1cbff0f6 100644 --- a/src/test/github/folderRepositoryManager.test.ts +++ b/src/test/github/folderRepositoryManager.test.ts @@ -22,21 +22,26 @@ import { MockExtensionContext } from '../mocks/mockExtensionContext'; import { Uri } from 'vscode'; import { GitHubServerType } from '../../common/authentication'; import { CreatePullRequestHelper } from '../../view/createPullRequestHelper'; +import { RepositoriesManager } from '../../github/repositoriesManager'; +import { MockThemeWatcher } from '../mocks/mockThemeWatcher'; describe('PullRequestManager', function () { let sinon: SinonSandbox; let manager: FolderRepositoryManager; let telemetry: MockTelemetry; + let mockThemeWatcher: MockThemeWatcher; beforeEach(function () { sinon = createSandbox(); MockCommandRegistry.install(sinon); telemetry = new MockTelemetry(); + mockThemeWatcher = new MockThemeWatcher(); const repository = new MockRepository(); const context = new MockExtensionContext(); const credentialStore = new CredentialStore(telemetry, context); - manager = new FolderRepositoryManager(0, context, repository, telemetry, new GitApiImpl(), credentialStore, new CreatePullRequestHelper()); + const repositoriesManager = new RepositoriesManager(credentialStore, telemetry); + manager = new FolderRepositoryManager(0, context, repository, telemetry, new GitApiImpl(repositoriesManager), credentialStore, new CreatePullRequestHelper(), mockThemeWatcher); }); afterEach(function () { 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); + }); + }); +}); + diff --git a/src/test/github/markdownUtils.test.ts b/src/test/github/markdownUtils.test.ts new file mode 100644 index 0000000000..3f0be975f1 --- /dev/null +++ b/src/test/github/markdownUtils.test.ts @@ -0,0 +1,40 @@ +/*--------------------------------------------------------------------------------------------- + * 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 * as marked from 'marked'; +import { PlainTextRenderer } from '../../github/markdownUtils'; + +describe('PlainTextRenderer', () => { + it('should escape inline code by default', () => { + const renderer = new PlainTextRenderer(); + const result = marked.parse('rename the `Foo` class', { renderer, smartypants: true }); + assert.strictEqual(result.trim(), 'rename the \\`Foo\\` class'); + }); + + it('should preserve inline code when allowSimpleMarkdown is true', () => { + const renderer = new PlainTextRenderer(true); + const result = marked.parse('rename the `Foo` class', { renderer, smartypants: true }); + assert.strictEqual(result.trim(), 'rename the `Foo` class'); + }); + + it('should handle multiple inline code spans', () => { + const renderer = new PlainTextRenderer(true); + const result = marked.parse('rename the `Foo` class to `Bar`', { renderer, smartypants: true }); + assert.strictEqual(result.trim(), 'rename the `Foo` class to `Bar`'); + }); + + it('should still escape when allowSimpleMarkdown is false', () => { + const renderer = new PlainTextRenderer(false); + const result = marked.parse('rename the `Foo` class to `Bar`', { renderer, smartypants: true }); + assert.strictEqual(result.trim(), 'rename the \\`Foo\\` class to \\`Bar\\`'); + }); + + it('should strip all formatting by default', () => { + const renderer = new PlainTextRenderer(false); + const result = marked.parse('rename the `Foo` class to **`Bar`** and make it *italic*', { renderer, smartypants: true }); + assert.strictEqual(result.trim(), 'rename the \\`Foo\\` class to \\`Bar\\` and make it italic'); + }); +}); \ No newline at end of file diff --git a/src/test/github/prComment.test.ts b/src/test/github/prComment.test.ts new file mode 100644 index 0000000000..fff88abca4 --- /dev/null +++ b/src/test/github/prComment.test.ts @@ -0,0 +1,56 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { replaceImages } from '../../github/prComment'; + +describe('replace images', function () { + it('github.com', function () { + const markdownBody = `Test image +![image](https://github.com/user-attachments/assets/714215c1-e994-4c69-be20-2276c558f7c3) +test again +![image](https://github.com/user-attachments/assets/3f2c170a-d0c3-4ac7-a9e5-ea13bf71a5bc)`; + const htmlBody = ` +

Test image

image

+

test again

+

image

`; + const host = 'github.com'; + const replaced = replaceImages(markdownBody, htmlBody, host); + const expected = `Test image +![image](https://private-user-images.githubusercontent.com/38270282/445632993-714215c1-e994-4c69-be20-2276c558f7c3.png?jwt=TEST) +test again +![image](https://private-user-images.githubusercontent.com/38270282/445689518-3f2c170a-d0c3-4ac7-a9e5-ea13bf71a5bc.png?jwt=TEST)`; + assert.strictEqual(replaced, expected); + }); + + it('GHCE', function () { + const markdownBody = `Test image +![image](https://test.ghe.com/user-attachments/assets/d81c6ab2-52a6-4ebf-b0c8-125492bd9662)`; + const htmlBody = ` +

Test image

+

image

`; + const host = 'test.ghe.com'; + const replaced = replaceImages(markdownBody, htmlBody, host); + const expected = `Test image +![image](https://test.ghe.com/github-production-user-asset-6210df/11296/2514616-d81c6ab2-52a6-4ebf-b0c8-125492bd9662.png?TEST)`; + + assert.strictEqual(replaced, expected); + }); + + it('GHE', function () { + const markdownBody = `Test +![image](https://alexr00-my-test-instance.ghe-test.com/my-user/my-repo/assets/6/c267d6ce-fbdd-41a0-b86d-760882bd0c82) +`; + const htmlBody = `

Test
+image

`; + const host = 'alexr00-my-test-instance.ghe-test.com'; + const replaced = replaceImages(markdownBody, htmlBody, host); + const expected = `Test +![image](https://media.alexr00-my-test-instance.ghe-test.com/user/6/files/c267d6ce-fbdd-41a0-b86d-760882bd0c82?TEST) +`; + + assert.strictEqual(replaced, expected); + }); +}); diff --git a/src/test/github/pullRequestGitHelper.test.ts b/src/test/github/pullRequestGitHelper.test.ts index 5fb4eff394..590b35f0aa 100644 --- a/src/test/github/pullRequestGitHelper.test.ts +++ b/src/test/github/pullRequestGitHelper.test.ts @@ -44,6 +44,98 @@ describe('PullRequestGitHelper', function () { sinon.restore(); }); + describe('fetchAndCheckout', function () { + it('creates a unique branch when local branch exists with different commit to preserve user work', async function () { + const url = 'git@github.com:owner/name.git'; + const remote = new GitHubRemote('origin', url, new Protocol(url), GitHubServerType.GitHubDotCom); + const gitHubRepository = new MockGitHubRepository(remote, credentialStore, telemetry, sinon); + + const prItem = convertRESTPullRequestToRawPullRequest( + new PullRequestBuilder() + .number(100) + .user(u => u.login('me')) + .base(b => { + (b.repo)(r => (r).clone_url('git@github.com:owner/name.git')); + }) + .head(h => { + h.repo(r => (r).clone_url('git@github.com:owner/name.git')); + h.ref('my-branch'); + }) + .build(), + gitHubRepository, + ); + + const pullRequest = new PullRequestModel(credentialStore, telemetry, gitHubRepository, remote, prItem); + + // Setup: local branch exists with different commit than remote + await repository.createBranch('my-branch', false, 'local-commit-hash'); + + // Setup: remote branch has different commit + await repository.createBranch('refs/remotes/origin/my-branch', false, 'remote-commit-hash'); + + const remotes = [remote]; + + // Expect fetch to be called + repository.expectFetch('origin', 'my-branch'); + + await PullRequestGitHelper.fetchAndCheckout(repository, remotes, pullRequest, { report: () => undefined }); + + // Verify that the original local branch is preserved + const originalBranch = await repository.getBranch('my-branch'); + assert.strictEqual(originalBranch.commit, 'local-commit-hash', 'Original branch should be preserved'); + + // Verify that a unique branch was created with the correct commit + const uniqueBranch = await repository.getBranch('pr/me/100'); + assert.strictEqual(uniqueBranch.commit, 'remote-commit-hash', 'Unique branch should have remote commit'); + assert.strictEqual(repository.state.HEAD?.name, 'pr/me/100', 'Should check out the unique branch'); + }); + + it('creates a unique branch even when currently checked out on conflicting local branch', async function () { + const url = 'git@github.com:owner/name.git'; + const remote = new GitHubRemote('origin', url, new Protocol(url), GitHubServerType.GitHubDotCom); + const gitHubRepository = new MockGitHubRepository(remote, credentialStore, telemetry, sinon); + + const prItem = convertRESTPullRequestToRawPullRequest( + new PullRequestBuilder() + .number(100) + .user(u => u.login('me')) + .base(b => { + (b.repo)(r => (r).clone_url('git@github.com:owner/name.git')); + }) + .head(h => { + h.repo(r => (r).clone_url('git@github.com:owner/name.git')); + h.ref('my-branch'); + }) + .build(), + gitHubRepository, + ); + + const pullRequest = new PullRequestModel(credentialStore, telemetry, gitHubRepository, remote, prItem); + + // Setup: local branch exists with different commit than remote AND is currently checked out + await repository.createBranch('my-branch', true, 'local-commit-hash'); // checkout = true + + // Setup: remote branch has different commit + await repository.createBranch('refs/remotes/origin/my-branch', false, 'remote-commit-hash'); + + const remotes = [remote]; + + // Expect fetch to be called + repository.expectFetch('origin', 'my-branch'); + + await PullRequestGitHelper.fetchAndCheckout(repository, remotes, pullRequest, { report: () => undefined }); + + // Verify that the original local branch is preserved with its commit + const originalBranch = await repository.getBranch('my-branch'); + assert.strictEqual(originalBranch.commit, 'local-commit-hash', 'Original branch should be preserved'); + + // Verify that a unique branch was created and checked out + const uniqueBranch = await repository.getBranch('pr/me/100'); + assert.strictEqual(uniqueBranch.commit, 'remote-commit-hash', 'Unique branch should have remote commit'); + assert.strictEqual(repository.state.HEAD?.name, 'pr/me/100', 'Should check out the unique branch'); + }); + }); + describe('checkoutFromFork', function () { it('fetches, checks out, and configures a branch from a fork', async function () { const url = 'git@github.com:owner/name.git'; diff --git a/src/test/github/pullRequestModel.test.ts b/src/test/github/pullRequestModel.test.ts index 7a976af689..5497cde4ab 100644 --- a/src/test/github/pullRequestModel.test.ts +++ b/src/test/github/pullRequestModel.test.ts @@ -16,7 +16,6 @@ import { PullRequestBuilder } from '../builders/rest/pullRequestBuilder'; import { MockTelemetry } from '../mocks/mockTelemetry'; import { MockGitHubRepository } from '../mocks/mockGitHubRepository'; import { NetworkStatus } from 'apollo-client'; -import { Resource } from '../../common/resources'; import { MockExtensionContext } from '../mocks/mockExtensionContext'; import { GitHubServerType } from '../../common/authentication'; import { mergeQuerySchemaWithShared } from '../../github/common'; @@ -66,7 +65,6 @@ describe('PullRequestModel', function () { context = new MockExtensionContext(); credentials = new CredentialStore(telemetry, context); repo = new MockGitHubRepository(remote, credentials, telemetry, sinon); - Resource.initialize(context); }); afterEach(function () { diff --git a/src/test/github/pullRequestOverview.test.ts b/src/test/github/pullRequestOverview.test.ts index 97b1476df6..9498318b4c 100644 --- a/src/test/github/pullRequestOverview.test.ts +++ b/src/test/github/pullRequestOverview.test.ts @@ -24,6 +24,8 @@ import { GitHubServerType } from '../../common/authentication'; import { GitHubRemote } from '../../common/remote'; import { CheckState } from '../../github/interface'; import { CreatePullRequestHelper } from '../../view/createPullRequestHelper'; +import { RepositoriesManager } from '../../github/repositoriesManager'; +import { MockThemeWatcher } from '../mocks/mockThemeWatcher'; const EXTENSION_URI = vscode.Uri.joinPath(vscode.Uri.file(__dirname), '../../..'); @@ -35,6 +37,7 @@ describe('PullRequestOverview', function () { let repo: MockGitHubRepository; let telemetry: MockTelemetry; let credentialStore: CredentialStore; + let mockThemeWatcher: MockThemeWatcher; beforeEach(async function () { sinon = createSandbox(); @@ -44,8 +47,10 @@ describe('PullRequestOverview', function () { const repository = new MockRepository(); telemetry = new MockTelemetry(); credentialStore = new CredentialStore(telemetry, context); + mockThemeWatcher = new MockThemeWatcher(); const createPrHelper = new CreatePullRequestHelper(); - pullRequestManager = new FolderRepositoryManager(0, context, repository, telemetry, new GitApiImpl(), credentialStore, createPrHelper); + const repositoriesManager = new RepositoriesManager(credentialStore, telemetry); + pullRequestManager = new FolderRepositoryManager(0, context, repository, telemetry, new GitApiImpl(repositoriesManager), credentialStore, createPrHelper, mockThemeWatcher); const url = 'https://github.com/aaa/bbb'; remote = new GitHubRemote('origin', url, new Protocol(url), GitHubServerType.GitHubDotCom); @@ -114,6 +119,7 @@ describe('PullRequestOverview', function () { const resolveStub = sinon.stub(pullRequestManager, 'resolvePullRequest').resolves(prModel0); sinon.stub(prModel0, 'getReviewRequests').resolves([]); sinon.stub(prModel0, 'getTimelineEvents').resolves([]); + sinon.stub(prModel0, 'validateDraftMode').resolves(true); sinon.stub(prModel0, 'getStatusChecks').resolves([{ state: CheckState.Success, statuses: [] }, null]); await PullRequestOverviewPanel.createOrShow(telemetry, EXTENSION_URI, pullRequestManager, prModel0); @@ -127,6 +133,7 @@ describe('PullRequestOverview', function () { resolveStub.resolves(prModel1); sinon.stub(prModel1, 'getReviewRequests').resolves([]); sinon.stub(prModel1, 'getTimelineEvents').resolves([]); + sinon.stub(prModel1, 'validateDraftMode').resolves(true); sinon.stub(prModel1, 'getStatusChecks').resolves([{ state: CheckState.Success, statuses: [] }, null]); await PullRequestOverviewPanel.createOrShow(telemetry, EXTENSION_URI, pullRequestManager, prModel1); diff --git a/src/test/index.ts b/src/test/index.ts index 677bc56144..6ded19c0c2 100644 --- a/src/test/index.ts +++ b/src/test/index.ts @@ -1,3 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// @ts-nocheck // This file is providing the test runner to use when running extension tests. import * as path from 'path'; import * as vscode from 'vscode'; @@ -6,20 +12,6 @@ import Mocha from 'mocha'; import { mockWebviewEnvironment } from './mocks/mockWebviewEnvironment'; import { EXTENSION_ID } from '../constants'; -function addTests(mocha: Mocha, root: string): Promise { - return new Promise((resolve, reject) => { - glob('**/**.test.js', { cwd: root }, (error, files) => { - if (error) { - return reject(error); - } - - for (const testFile of files) { - mocha.addFile(path.join(root, testFile)); - } - resolve(); - }); - }); -} async function runAllExtensionTests(testsRoot: string, clb: (error: Error | null, failures?: number) => void): Promise { // Ensure the dev-mode extension is activated @@ -31,10 +23,34 @@ async function runAllExtensionTests(testsRoot: string, clb: (error: Error | null ui: 'bdd', color: true }); - mocha.addFile(path.resolve(testsRoot, 'globalHooks.js')); + // Load globalHooks if it exists + try { + mocha.addFile(path.resolve(testsRoot, 'globalHooks.js')); + } catch (e) { + // globalHooks might not exist in webpack bundle, ignore + } - await addTests(mocha, testsRoot); - await addTests(mocha, path.resolve(testsRoot, '../../../webviews/')); + // Import all test files using webpack's require.context + try { + // Load tests from src/test directory only + // Webview tests are compiled separately with the webview configuration + const importAll = (r: __WebpackModuleApi.RequireContext) => r.keys().forEach(r); + importAll(require.context('./', true, /\.test$/)); + } catch (e) { + // Fallback if 'require.context' is not available (e.g., in non-webpack environments) + const files = glob.sync('**/*.test.js', { + cwd: testsRoot, + absolute: true, + // Browser/webview tests are loaded via the separate browser runner + ignore: ['browser/**'] + }); + if (!files.length) { + console.log('Fallback test discovery found no test files. Original error:', e); + } + for (const f of files) { + mocha.addFile(f); + } + } if (process.env.TEST_JUNIT_XML_PATH) { mocha.reporter('mocha-multi-reporters', { diff --git a/src/test/issues/issueTodoProvider.test.ts b/src/test/issues/issueTodoProvider.test.ts new file mode 100644 index 0000000000..1551294b82 --- /dev/null +++ b/src/test/issues/issueTodoProvider.test.ts @@ -0,0 +1,186 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { IssueTodoProvider } from '../../issues/issueTodoProvider'; +import { CopilotRemoteAgentManager } from '../../github/copilotRemoteAgent'; +import { CODING_AGENT, CREATE_ISSUE_TRIGGERS, ISSUES_SETTINGS_NAMESPACE, SHOW_CODE_LENS } from '../../common/settingKeys'; +import * as issueUtil from '../../issues/util'; + +const mockCopilotManager: Partial = { + isAvailable: () => Promise.resolve(true) +} + +describe('IssueTodoProvider', function () { + // Mock isComment + // We don't have a real 'vscode.TextDocument' in these tests, which + // causes 'vscode.languages.getTokenInformationAtPosition' to throw. + const originalIsComment = issueUtil.isComment; + before(() => { + (issueUtil as any).isComment = async (document: vscode.TextDocument, position: vscode.Position) => { + try { + const lineText = document.lineAt(position.line).text; + return lineText.trim().startsWith('//'); + } catch { + return false; + } + }; + }); + after(() => { + (issueUtil as any).isComment = originalIsComment; + }); + + it('should provide both actions when CopilotRemoteAgentManager is available', async function () { + const mockContext = { + subscriptions: [] + } as any as vscode.ExtensionContext; + + + const provider = new IssueTodoProvider(mockContext, mockCopilotManager as CopilotRemoteAgentManager); + + // Create a mock document with TODO comment + const document = { + lineAt: (line: number) => ({ text: line === 1 ? ' // TODO: Fix this' : 'function test() {' }), + lineCount: 4 + } as vscode.TextDocument; + + const range = new vscode.Range(1, 0, 1, 20); + const context = { + only: vscode.CodeActionKind.QuickFix + } as vscode.CodeActionContext; + + const actions = await provider.provideCodeActions(document, range, context, new vscode.CancellationTokenSource().token); + + assert.strictEqual(actions.length, 2); + + // Find the actions + const createIssueAction = actions.find(a => a.title === 'Create GitHub Issue'); + const startAgentAction = actions.find(a => a.title === 'Delegate to agent'); + + assert.ok(createIssueAction, 'Should have Create GitHub Issue action'); + assert.ok(startAgentAction, 'Should have Delegate to agent action'); + + assert.strictEqual(createIssueAction?.command?.command, 'issue.createIssueFromSelection'); + assert.strictEqual(startAgentAction?.command?.command, 'issue.startCodingAgentFromTodo'); + }); + + it('should provide code lenses for TODO comments', async function () { + const mockContext = { + subscriptions: [] + } as any as vscode.ExtensionContext; + + const provider = new IssueTodoProvider(mockContext, mockCopilotManager as CopilotRemoteAgentManager); + + // Create a mock document with TODO comment + const document = { + lineAt: (line: number) => ({ + text: line === 1 ? ' // TODO: Fix this' : 'function test() {}' + }), + lineCount: 4 + } as vscode.TextDocument; + + const originalGetConfiguration = vscode.workspace.getConfiguration; + vscode.workspace.getConfiguration = (section?: string) => { + if (section === ISSUES_SETTINGS_NAMESPACE) { + return { + get: (key: string, defaultValue?: any) => { + if (key === CREATE_ISSUE_TRIGGERS) { + return ['TODO', 'todo', 'BUG', 'FIXME', 'ISSUE', 'HACK']; + } + return defaultValue; + } + } as any; + } else if (section === CODING_AGENT) { + return { + get: (key: string, defaultValue?: any) => { + if (key === SHOW_CODE_LENS) { + return true; + } + return defaultValue; + } + } as any; + } + return originalGetConfiguration(section); + }; + + try { + // Update triggers to ensure the expression is set + (provider as any).updateTriggers(); + + const codeLenses = await provider.provideCodeLenses(document, new vscode.CancellationTokenSource().token); + + assert.strictEqual(codeLenses.length, 1); + + // Verify the code lenses + const startAgentLens = codeLenses.find(cl => cl.command?.title === 'Delegate to agent'); + + assert.ok(startAgentLens, 'Should have Delegate to agent CodeLens'); + + assert.strictEqual(startAgentLens?.command?.command, 'issue.startCodingAgentFromTodo'); + + // Verify the range points to the TODO text + assert.strictEqual(startAgentLens?.range.start.line, 1); + } finally { + // Restore original configuration + vscode.workspace.getConfiguration = originalGetConfiguration; + } + }); + + it('should not provide code lenses when codeLens setting is disabled', async function () { + const mockContext = { + subscriptions: [] + } as any as vscode.ExtensionContext; + + const provider = new IssueTodoProvider(mockContext, mockCopilotManager as CopilotRemoteAgentManager); + + // Create a mock document with TODO comment + const document = { + lineAt: (lineNo: number) => ({ + text: lineNo === 1 ? ' // TODO: Fix this' : 'function test() {}', + firstNonWhitespaceCharacterIndex: lineNo === 1 ? 2 : 0, + } as vscode.TextLine), + lineCount: 4, + languageId: 'javascript' + } as vscode.TextDocument; + + const originalGetConfiguration = vscode.workspace.getConfiguration; + vscode.workspace.getConfiguration = (section?: string) => { + if (section === ISSUES_SETTINGS_NAMESPACE) { + return { + get: (key: string, defaultValue?: any) => { + if (key === CREATE_ISSUE_TRIGGERS) { + return ['TODO', 'todo', 'BUG', 'FIXME', 'ISSUE', 'HACK']; + } + return defaultValue; + } + } as any; + } else if (section === CODING_AGENT) { + return { + get: (key: string, defaultValue?: any) => { + if (key === SHOW_CODE_LENS) { + return false; + } + return defaultValue; + } + } as any; + } + return originalGetConfiguration(section); + }; + + try { + // Update triggers to ensure the expression is set + (provider as any).updateTriggers(); + + const codeLenses = await provider.provideCodeLenses(document, new vscode.CancellationTokenSource().token); + + // Should return empty array when CodeLens is disabled + assert.strictEqual(codeLenses.length, 0, 'Should not provide code lenses when setting is disabled'); + } finally { + // Restore original configuration + vscode.workspace.getConfiguration = originalGetConfiguration; + } + }); +}); \ No newline at end of file diff --git a/src/test/issues/issuesUtils.test.ts b/src/test/issues/issuesUtils.test.ts index 90c45e0fa0..6b48644d14 100644 --- a/src/test/issues/issuesUtils.test.ts +++ b/src/test/issues/issuesUtils.test.ts @@ -48,5 +48,21 @@ describe('Issues utilities', function () { const notIssue = '#a4'; const notIssueParsed = parseIssueExpressionOutput(notIssue.match(ISSUE_OR_URL_EXPRESSION)); assert.strictEqual(notIssueParsed, undefined); + + // Test PR URL parsing + const prUrl = 'https://github.com/microsoft/vscode/pull/123'; + const prUrlParsed = parseIssueExpressionOutput(prUrl.match(ISSUE_OR_URL_EXPRESSION)); + assert.strictEqual(prUrlParsed?.issueNumber, 123); + assert.strictEqual(prUrlParsed?.commentNumber, undefined); + assert.strictEqual(prUrlParsed?.name, 'vscode'); + assert.strictEqual(prUrlParsed?.owner, 'microsoft'); + + // Test HTTP PR URL (without S) + const prUrlHttp = 'http://github.com/owner/repo/pull/456'; + const prUrlHttpParsed = parseIssueExpressionOutput(prUrlHttp.match(ISSUE_OR_URL_EXPRESSION)); + assert.strictEqual(prUrlHttpParsed?.issueNumber, 456); + assert.strictEqual(prUrlHttpParsed?.commentNumber, undefined); + assert.strictEqual(prUrlHttpParsed?.name, 'repo'); + assert.strictEqual(prUrlHttpParsed?.owner, 'owner'); }); }); diff --git a/src/test/issues/stateManager.test.ts b/src/test/issues/stateManager.test.ts new file mode 100644 index 0000000000..481f2687e8 --- /dev/null +++ b/src/test/issues/stateManager.test.ts @@ -0,0 +1,127 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { StateManager } from '../../issues/stateManager'; +import { CurrentIssue } from '../../issues/currentIssue'; +import { USE_BRANCH_FOR_ISSUES, ISSUES_SETTINGS_NAMESPACE } from '../../common/settingKeys'; + +// Mock classes for testing +class MockFolderRepositoryManager { + constructor(public repository: { rootUri: vscode.Uri }) { } +} + +class MockSingleRepoState { + currentIssue?: MockCurrentIssue; + constructor(public folderManager: MockFolderRepositoryManager) { } +} + +class MockCurrentIssue { + stopWorkingCalled = false; + stopWorkingCheckoutFlag = false; + issue = { number: 123 }; + + async stopWorking(checkoutDefaultBranch: boolean) { + this.stopWorkingCalled = true; + this.stopWorkingCheckoutFlag = checkoutDefaultBranch; + } +} + +describe('StateManager branch behavior with useBranchForIssues setting', function () { + let stateManager: StateManager; + let mockContext: vscode.ExtensionContext; + + beforeEach(() => { + mockContext = { + workspaceState: { + get: () => undefined, + update: () => Promise.resolve(), + }, + subscriptions: [], + } as any; + + stateManager = new StateManager(undefined as any, undefined as any, mockContext); + (stateManager as any)._singleRepoStates = new Map(); + }); + + it('should not checkout default branch when useBranchForIssues is off', async function () { + // Mock workspace configuration to return 'off' + const originalGetConfiguration = vscode.workspace.getConfiguration; + vscode.workspace.getConfiguration = (section?: string) => { + if (section === ISSUES_SETTINGS_NAMESPACE) { + return { + get: (key: string) => { + if (key === USE_BRANCH_FOR_ISSUES) { + return 'off'; + } + return undefined; + }, + } as any; + } + return originalGetConfiguration(section); + }; + + try { + // Set up test state + const mockUri = vscode.Uri.parse('file:///test'); + const mockFolderManager = new MockFolderRepositoryManager({ rootUri: mockUri }); + const mockState = new MockSingleRepoState(mockFolderManager); + const mockCurrentIssue = new MockCurrentIssue(); + mockState.currentIssue = mockCurrentIssue; + + (stateManager as any)._singleRepoStates.set(mockUri.path, mockState); + + // Call setCurrentIssue with checkoutDefaultBranch = true + await stateManager.setCurrentIssue(mockState as any, undefined, true, true); + + // Verify that stopWorking was called with false (not the original true) + assert.strictEqual(mockCurrentIssue.stopWorkingCalled, true, 'stopWorking should have been called'); + assert.strictEqual(mockCurrentIssue.stopWorkingCheckoutFlag, false, 'stopWorking should have been called with checkoutDefaultBranch=false when useBranchForIssues is off'); + } finally { + // Restore original configuration + vscode.workspace.getConfiguration = originalGetConfiguration; + } + }); + + it('should checkout default branch when useBranchForIssues is not off', async function () { + // Mock workspace configuration to return 'on' + const originalGetConfiguration = vscode.workspace.getConfiguration; + vscode.workspace.getConfiguration = (section?: string) => { + if (section === ISSUES_SETTINGS_NAMESPACE) { + return { + get: (key: string) => { + if (key === USE_BRANCH_FOR_ISSUES) { + return 'on'; + } + return undefined; + }, + } as any; + } + return originalGetConfiguration(section); + }; + + try { + // Set up test state + const mockUri = vscode.Uri.parse('file:///test'); + const mockFolderManager = new MockFolderRepositoryManager({ rootUri: mockUri }); + const mockState = new MockSingleRepoState(mockFolderManager); + const mockCurrentIssue = new MockCurrentIssue(); + mockState.currentIssue = mockCurrentIssue; + + (stateManager as any)._singleRepoStates.set(mockUri.path, mockState); + + // Call setCurrentIssue with checkoutDefaultBranch = true + await stateManager.setCurrentIssue(mockState as any, undefined, true, true); + + // Verify that stopWorking was called with true (preserving the original value) + assert.strictEqual(mockCurrentIssue.stopWorkingCalled, true, 'stopWorking should have been called'); + assert.strictEqual(mockCurrentIssue.stopWorkingCheckoutFlag, true, 'stopWorking should have been called with checkoutDefaultBranch=true when useBranchForIssues is on'); + } finally { + // Restore original configuration + vscode.workspace.getConfiguration = originalGetConfiguration; + } + }); +}); \ No newline at end of file diff --git a/src/test/lm/tools/copilotRemoteAgentTool.test.ts b/src/test/lm/tools/copilotRemoteAgentTool.test.ts new file mode 100644 index 0000000000..26440130c8 --- /dev/null +++ b/src/test/lm/tools/copilotRemoteAgentTool.test.ts @@ -0,0 +1,384 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { SinonSandbox, createSandbox } from 'sinon'; +import * as vscode from 'vscode'; +import { CopilotRemoteAgentTool, CopilotRemoteAgentToolParameters } from '../../../lm/tools/copilotRemoteAgentTool'; +import { CopilotRemoteAgentManager } from '../../../github/copilotRemoteAgent'; +import { MockTelemetry } from '../../mocks/mockTelemetry'; +import { RemoteAgentResult } from '../../../github/common'; +import { MockPrsTreeModel } from '../../mocks/mockPRsTreeModel'; +import { PrsTreeModel } from '../../../view/prsTreeModel'; +import { FolderRepositoryManager } from '../../../github/folderRepositoryManager'; + +class TestCopilotRemoteAgentTool extends CopilotRemoteAgentTool { + publicGetActivePullRequestWithSession(repoInfo?: { repo: string; owner: string; fm: FolderRepositoryManager }): Promise { + return this.getActivePullRequestWithSession(repoInfo); + } +} + +describe('CopilotRemoteAgentTool', function () { + let sinon: SinonSandbox; + let tool: TestCopilotRemoteAgentTool; + let mockManager: sinon.SinonStubbedInstance; + let telemetry: MockTelemetry; + let mockPrsTreeModel: PrsTreeModel; + + beforeEach(function () { + sinon = createSandbox(); + telemetry = new MockTelemetry(); + mockManager = sinon.createStubInstance(CopilotRemoteAgentManager); + + // Mock the VSCode Language Model API that may not be available in test environment + if (!(vscode as any).LanguageModelPartAudience) { + (vscode as any).LanguageModelPartAudience = { + Assistant: 0, + User: 1, + Extension: 2 + }; + } + mockPrsTreeModel = new MockPrsTreeModel() as unknown as PrsTreeModel; + + tool = new TestCopilotRemoteAgentTool(mockManager as any, telemetry, mockPrsTreeModel); + }); + + afterEach(function () { + sinon.restore(); + }); + + describe('toolId', function () { + it('should have the correct tool ID', function () { + assert.strictEqual(CopilotRemoteAgentTool.toolId, 'github-pull-request_copilot-coding-agent'); + }); + }); + + describe('prepareInvocation()', function () { + const mockInput: CopilotRemoteAgentToolParameters = { + title: 'Test PR Title', + body: 'Test PR body', + }; + + it('should throw error when coding agent is not available', async function () { + mockManager.isAvailable.resolves(false); + + const options = { input: mockInput } as any; + + await assert.rejects( + async () => await tool.prepareInvocation(options), + /Copilot coding agent is not available/ + ); + }); + + it('should prepare invocation for new PR when agent is available', async function () { + mockManager.isAvailable.resolves(true); + mockManager.repoInfo.resolves({ + owner: 'test', + repo: 'test-repo', + baseRef: 'main', + remote: {} as any, + repository: {} as any, + ghRepository: {} as any, + fm: {} as any + }); + sinon.stub(mockManager, 'autoCommitAndPushEnabled').get(() => true); + + const options = { input: mockInput } as any; + + const result = await tool.prepareInvocation(options); + + assert.strictEqual(result.pastTenseMessage, 'Launched coding agent'); + assert.strictEqual(result.invocationMessage, 'Launching coding agent'); + // Handle both string and MarkdownString types + const message = result.confirmationMessages?.message; + const messageText = typeof message === 'string' ? message : message?.value || ''; + assert(messageText.includes('test/test-repo')); + assert(messageText.includes('automatically pushed')); + }); + + it('should prepare invocation for existing PR', async function () { + mockManager.isAvailable.resolves(true); + mockManager.repoInfo.resolves({ + owner: 'test', + repo: 'test', + baseRef: 'main', + remote: {} as any, + repository: {} as any, + ghRepository: {} as any, + fm: {} as any + }); + + // Mock the config getter to avoid access issues + Object.defineProperty(mockManager, 'autoCommitAndPushEnabled', { + get: () => false + }); + + const inputWithExistingPR: CopilotRemoteAgentToolParameters = { + title: 'Test PR Title', + existingPullRequest: '123', + }; + + const options = { input: inputWithExistingPR } as any; + + const result = await tool.prepareInvocation(options); + + // Handle both string and MarkdownString types + const message = result.confirmationMessages?.message; + const messageText = typeof message === 'string' ? message : message?.value || ''; + assert(messageText.includes('existing pull request **#123**')); + }); + + it('should handle active PR with session', async function () { + mockManager.isAvailable.resolves(true); + mockManager.repoInfo.resolves({ + owner: 'test', + repo: 'test-repo', + baseRef: 'main', + remote: {} as any, + repository: {} as any, + ghRepository: {} as any, + fm: { + activePullRequest: { number: 123 } as any + } as any + }); + + // Mock the config getter to avoid access issues + Object.defineProperty(mockManager, 'autoCommitAndPushEnabled', { + get: () => false + }); + + const options = { input: mockInput } as any; + + const result = await tool.prepareInvocation(options); + + // Handle both string and MarkdownString types + const message = result.confirmationMessages?.message; + const messageText = typeof message === 'string' ? message : message?.value || ''; + assert(messageText.includes('existing pull request **#123**')); + }); + }); + + describe('invoke()', function () { + const mockInput: CopilotRemoteAgentToolParameters = { + title: 'Test PR Title', + body: 'Test PR body', + }; + + const mockOptions = { + input: mockInput + } as any; + + const mockToken = new vscode.CancellationTokenSource().token; + + it('should return error when no repository information is found', async function () { + mockManager.repoInfo.resolves(undefined); + + const result = await tool.invoke(mockOptions, mockToken); + + assert(result); + + // VSCode wraps the result with content array + if ((result as any).content && Array.isArray((result as any).content)) { + const content = (result as any).content; + const textValue = content[0]?.value || content[0]?.text || ''; + assert(textValue.includes('No repository information found') || textValue.includes('repository information')); + } else { + // Check that it returns a text result with error message + const resultParts = (result as any)._parts || (result as any).parts; + assert(Array.isArray(resultParts)); + assert(resultParts.length > 0); + const firstPart = resultParts[0]; + assert(firstPart.value?.includes('No repository information found') || firstPart.text?.includes('No repository information found')); + } + }); + + it('should return error for invalid existing PR number', async function () { + mockManager.repoInfo.resolves({ + owner: 'test', + repo: 'test', + baseRef: 'main', + remote: {} as any, + repository: {} as any, + ghRepository: {} as any, + fm: {} as any + }); + + const invalidInput = { + ...mockInput, + existingPullRequest: 'invalid' + }; + + const result = await tool.invoke({ input: invalidInput } as any, mockToken); + + assert(result); + + // VSCode wraps the result with content array + if ((result as any).content && Array.isArray((result as any).content)) { + const content = (result as any).content; + const textValue = content[0]?.value || content[0]?.text || ''; + assert(textValue.includes('Invalid pull request number') || textValue.includes('invalid')); + } else { + const resultParts = (result as any)._parts || (result as any).parts; + assert(Array.isArray(resultParts)); + assert(resultParts.length > 0); + const firstPart = resultParts[0]; + assert(firstPart.value?.includes('Invalid pull request number') || firstPart.text?.includes('Invalid pull request number')); + } + }); + + it('should add follow-up to existing PR', async function () { + mockManager.repoInfo.resolves({ + owner: 'test', + repo: 'test-repo', + baseRef: 'main', + remote: {} as any, + repository: {} as any, + ghRepository: {} as any, + fm: {} as any + }); + mockManager.addFollowUpToExistingPR.resolves('Follow-up added'); + + const inputWithExistingPR: CopilotRemoteAgentToolParameters = { + title: 'Test PR Title', + existingPullRequest: '123', + }; + + const optionsWithExistingPR = { + input: inputWithExistingPR + } as any; + + const result = await tool.invoke(optionsWithExistingPR, mockToken); + + assert(result); + + // VSCode wraps the result with content array + if ((result as any).content && Array.isArray((result as any).content)) { + const content = (result as any).content; + const textValue = content[0]?.value || content[0]?.text || ''; + assert(textValue.includes('Follow-up added to pull request #123') || textValue.includes('follow-up') || textValue.includes('Follow-up added')); + } else { + const resultParts = (result as any)._parts || (result as any).parts; + assert(Array.isArray(resultParts)); + const firstPart = resultParts[0]; + assert(firstPart.value?.includes('Follow-up added to pull request #123') || firstPart.text?.includes('Follow-up added to pull request #123')); + } + }); + + it('should invoke remote agent for new PR successfully', async function () { + mockManager.repoInfo.resolves({ + owner: 'test', + repo: 'test-repo', + baseRef: 'main', + remote: {} as any, + repository: {} as any, + ghRepository: {} as any, + fm: { + resolvePullRequest: sinon.stub().resolves({ + number: 789, + title: 'Test PR', + body: 'Test body', + author: { login: 'copilot-swe-agent' }, + githubRepository: { + remote: { + owner: 'test', + repositoryName: 'test-repo' + } + } + }) + } as any + }); + + const successResult: RemoteAgentResult = { + state: 'success', + number: 789, + link: 'https://github.com/test/test-repo/pull/789', + webviewUri: vscode.Uri.parse('https://example.com'), + llmDetails: 'Agent created PR successfully', + sessionId: '123-456' + }; + + mockManager.invokeRemoteAgent.resolves(successResult); + + const result = await tool.invoke(mockOptions, mockToken); + + assert(result); + const resultParts = (result as any).content; + assert(Array.isArray(resultParts)); + assert(resultParts.length >= 1); + const firstPart = resultParts[0]; + assert(firstPart.value?.includes('Agent created PR successfully') || firstPart.text?.includes('Agent created PR successfully')); + }); + + it('should throw error when invocation fails', async function () { + mockManager.repoInfo.resolves({ + owner: 'test', + repo: 'test-repo', + baseRef: 'main', + remote: {} as any, + repository: {} as any, + ghRepository: {} as any, + fm: {} as any + }); + + const errorResult: RemoteAgentResult = { + state: 'error', + error: 'Something went wrong' + }; + + mockManager.invokeRemoteAgent.resolves(errorResult); + + await assert.rejects( + async () => await tool.invoke(mockOptions, mockToken), + /Something went wrong/ + ); + }); + }); + + describe('publicGetActivePullRequestWithSession()', function () { + it('should return undefined when no repo info is provided', async function () { + const result = await tool.publicGetActivePullRequestWithSession(undefined); + assert.strictEqual(result, undefined); + }); + + it('should return undefined when no active PR exists', async function () { + const repoInfo = { + owner: 'test', + repo: 'test-repo', + fm: { + activePullRequest: undefined + } as FolderRepositoryManager + }; + + const result = await tool.publicGetActivePullRequestWithSession(repoInfo); + assert.strictEqual(result, undefined); + }); + + it('should return undefined when active PR has no copilot state', async function () { + const repoInfo = { + owner: 'test', + repo: 'test-repo', + fm: { + activePullRequest: { number: 456 } + } as FolderRepositoryManager + }; + + const result = await tool.publicGetActivePullRequestWithSession(repoInfo); + assert.strictEqual(result, undefined); + }); + + it('should return PR number when active PR has copilot state', async function () { + const repoInfo = { + owner: 'test', + repo: 'test-repo', + fm: { + activePullRequest: { number: 123 } + } as FolderRepositoryManager + }; + + const result = await tool.publicGetActivePullRequestWithSession(repoInfo); + assert.strictEqual(result, 123); + }); + }); +}); diff --git a/src/test/mocks/mockExtensionContext.ts b/src/test/mocks/mockExtensionContext.ts index 0390cf4cb7..b15c19907e 100644 --- a/src/test/mocks/mockExtensionContext.ts +++ b/src/test/mocks/mockExtensionContext.ts @@ -21,9 +21,13 @@ export class MockExtensionContext implements ExtensionContext { store(key: string, value: string): Thenable { throw new Error('Method not implemented.'); } + keys(): Thenable { + throw new Error('Method not implemented.'); + } delete(key: string): Thenable { throw new Error('Method not implemented.'); } + onDidChange!: Event; })(); subscriptions: { dispose(): any }[] = []; diff --git a/src/test/mocks/mockGitHubRepository.ts b/src/test/mocks/mockGitHubRepository.ts index 9cf42b8220..4bff09d7e9 100644 --- a/src/test/mocks/mockGitHubRepository.ts +++ b/src/test/mocks/mockGitHubRepository.ts @@ -21,6 +21,7 @@ import { MockTelemetry } from './mockTelemetry'; import { Uri } from 'vscode'; import { LoggingOctokit, RateLogger } from '../../github/loggingOctokit'; import { mergeQuerySchemaWithShared } from '../../github/common'; + const queries = mergeQuerySchemaWithShared(require('../../github/queries.gql'), require('../../github/queriesShared.gql')) as any; export class MockGitHubRepository extends GitHubRepository { @@ -33,7 +34,7 @@ export class MockGitHubRepository extends GitHubRepository { this._hub = { octokit: new LoggingOctokit(this.queryProvider.octokit, new RateLogger(new MockTelemetry(), true)), - graphql: null, + graphql: {} as any, }; this._metadata = Promise.resolve({ @@ -71,8 +72,8 @@ export class MockGitHubRepository extends GitHubRepository { block(builder); const responses = builder.build(); - const prNumber = responses.pullRequest.repository.pullRequest.number; - const headRef = responses.pullRequest.repository.pullRequest.headRef; + const prNumber = responses.pullRequest.repository!.pullRequest.number; + const headRef = responses.pullRequest.repository?.pullRequest.headRef; this.queryProvider.expectGraphQLQuery( { diff --git a/src/test/mocks/mockNotificationManager.ts b/src/test/mocks/mockNotificationManager.ts new file mode 100644 index 0000000000..8977615583 --- /dev/null +++ b/src/test/mocks/mockNotificationManager.ts @@ -0,0 +1,15 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { Event, EventEmitter } from 'vscode'; +import { PullRequestModel } from '../../github/pullRequestModel'; +import { NotificationTreeDataItem, NotificationTreeItem } from '../../notifications/notificationItem'; + +export class MockNotificationManager { + onDidChangeTreeData: Event = new EventEmitter().event; + onDidChangeNotifications: Event = new EventEmitter().event; + hasNotification(_issueModel: PullRequestModel): boolean { return false; } + markPrNotificationsAsRead(_issueModel: PullRequestModel): void { /* no-op */ } + dispose(): void { /* no-op */ } +} diff --git a/src/test/mocks/mockPRsTreeModel.ts b/src/test/mocks/mockPRsTreeModel.ts new file mode 100644 index 0000000000..26420a9a87 --- /dev/null +++ b/src/test/mocks/mockPRsTreeModel.ts @@ -0,0 +1,108 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Event, Disposable, EventEmitter } from "vscode"; +import { CopilotPRStatus } from "../../common/copilot"; +import { CodingAgentPRAndStatus, CopilotStateModel } from "../../github/copilotPrWatcher"; +import { FolderRepositoryManager, ItemsResponseResult } from "../../github/folderRepositoryManager"; +import { PullRequestChangeEvent } from "../../github/githubRepository"; +import { PullRequestModel } from "../../github/pullRequestModel"; +import { PRStatusChange, PrsTreeModel } from "../../view/prsTreeModel"; +import { TreeNode } from "../../view/treeNodes/treeNode"; + +export class MockPrsTreeModel implements Partial { + onDidChangeCopilotStates: Event = new EventEmitter().event; + onDidChangeCopilotNotifications: Event = new EventEmitter().event; + clearCopilotCaches(): false | undefined { + throw new Error("Method not implemented."); + } + async refreshCopilotStateChanges(clearCache?: boolean): Promise { + return false; + } + getCopilotPullRequests(clearCache?: boolean): Promise { + throw new Error("Method not implemented."); + } + public onDidChangePrStatus: Event = new EventEmitter().event; + public onDidChangeData: Event = new EventEmitter().event; + public onLoaded: Event; + public copilotStateModel: CopilotStateModel; + public updateExpandedQueries(element: TreeNode, isExpanded: boolean): void { + throw new Error("Method not implemented."); + } + get expandedQueries(): Set | undefined { + return new Set(['All Open']); + } + get hasLoaded(): boolean { + return true; + } + set hasLoaded(value: boolean) { + throw new Error("Method not implemented."); + } + public cachedPRStatus(identifier: string): PRStatusChange | undefined { + throw new Error("Method not implemented."); + } + public forceClearCache(): void { + throw new Error("Method not implemented."); + } + public hasPullRequest(pr: PullRequestModel): boolean { + throw new Error("Method not implemented."); + } + public clearCache(silent?: boolean): void { + throw new Error("Method not implemented."); + } + async getLocalPullRequests(folderRepoManager: FolderRepositoryManager, update?: boolean): Promise> { + return { + hasMorePages: false, + items: this._localPullRequests, + hasUnsearchedRepositories: false + }; + } + getPullRequestsForQuery(folderRepoManager: FolderRepositoryManager, fetchNextPage: boolean, query: string): Promise> { + throw new Error("Method not implemented."); + } + getAllPullRequests(folderRepoManager: FolderRepositoryManager, fetchNextPage: boolean, update?: boolean): Promise> { + throw new Error("Method not implemented."); + } + getCopilotNotificationsCount(owner: string, repo: string): number { + return 0; + } + get copilotNotificationsCount(): number { + return 0; + } + clearAllCopilotNotifications(owner?: string, repo?: string): void { + throw new Error("Method not implemented."); + } + clearCopilotNotification(owner: string, repo: string, pullRequestNumber: number): void { + throw new Error("Method not implemented."); + } + hasCopilotNotification(owner: string, repo: string, pullRequestNumber?: number): boolean { + throw new Error("Method not implemented."); + } + getCopilotStateForPR(owner: string, repo: string, prNumber: number): CopilotPRStatus { + if (prNumber === 123) { + return CopilotPRStatus.Started; + } else { + return CopilotPRStatus.None; + } + } + getCopilotCounts(owner: string, repo: string): { total: number; inProgress: number; error: number; } { + throw new Error("Method not implemented."); + } + dispose(): void { + throw new Error("Method not implemented."); + } + protected _isDisposed: boolean; + protected _register(value: T): T { + throw new Error("Method not implemented."); + } + protected get isDisposed(): boolean { + throw new Error("Method not implemented."); + } + + private _localPullRequests: PullRequestModel[] = []; + addLocalPullRequest(pr: PullRequestModel): void { + this._localPullRequests.push(pr); + } +} \ No newline at end of file diff --git a/src/test/mocks/mockRepository.ts b/src/test/mocks/mockRepository.ts index 3481977f5a..9a70408774 100644 --- a/src/test/mocks/mockRepository.ts +++ b/src/test/mocks/mockRepository.ts @@ -68,7 +68,9 @@ export class MockRepository implements Repository { } private _state: Mutable = { - HEAD: undefined, + HEAD: { + type: RefType.Head + }, refs: [], remotes: [], submodules: [], @@ -149,8 +151,12 @@ export class MockRepository implements Repository { return Promise.reject(new Error('Unexpected hashObject(...)')); } + private _hasBranch(ref: string) { + return this._branches.some(b => b.name === ref); + } + async createBranch(name: string, checkout: boolean, ref?: string | undefined): Promise { - if (this._branches.some(b => b.name === name)) { + if (this._hasBranch(name)) { throw new Error(`A branch named ${name} already exists`); } @@ -275,7 +281,7 @@ export class MockRepository implements Repository { throw new Error(`Unexpected fetch(${remoteName}, ${ref}, ${depth})`); } - if (ref) { + if (ref && !this._hasBranch(ref)) { const match = /^(?:\+?[^:]+\:)?(.*)$/.exec(ref); if (match) { const [, localRef] = match; diff --git a/src/test/mocks/mockThemeWatcher.ts b/src/test/mocks/mockThemeWatcher.ts new file mode 100644 index 0000000000..1b1eaa36f9 --- /dev/null +++ b/src/test/mocks/mockThemeWatcher.ts @@ -0,0 +1,34 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Disposable } from '../../common/lifecycle'; +import type { ThemeData } from '../../view/theme'; +import { IThemeWatcher } from '../../themeWatcher'; + +export class MockThemeWatcher extends Disposable implements IThemeWatcher { + private _themeData: ThemeData | undefined; + private _onDidChangeTheme = new vscode.EventEmitter(); + readonly onDidChangeTheme = this._onDidChangeTheme.event; + + constructor() { + super(); + this._themeData = { + colors: {}, + semanticTokenColors: [], + tokenColors: [], + type: 'dark' + }; + } + + async updateTheme(themeData?: ThemeData) { + this._themeData = themeData ?? this._themeData; + this._onDidChangeTheme.fire(this._themeData); + } + + get themeData() { + return this._themeData; + } +} diff --git a/src/test/reference-types.d.ts b/src/test/reference-types.d.ts new file mode 100644 index 0000000000..a160962af4 --- /dev/null +++ b/src/test/reference-types.d.ts @@ -0,0 +1,6 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +/// diff --git a/src/test/view/prsTree.test.ts b/src/test/view/prsTree.test.ts index 2fcfda70cc..fc98daf733 100644 --- a/src/test/view/prsTree.test.ts +++ b/src/test/view/prsTree.test.ts @@ -9,9 +9,11 @@ import { default as assert } from 'assert'; import { Octokit } from '@octokit/rest'; import { PullRequestsTreeDataProvider } from '../../view/prsTreeDataProvider'; +import { NotificationsManager } from '../../notifications/notificationsManager'; import { FolderRepositoryManager } from '../../github/folderRepositoryManager'; import { MockTelemetry } from '../mocks/mockTelemetry'; +import { MockNotificationManager } from '../mocks/mockNotificationManager'; import { MockExtensionContext } from '../mocks/mockExtensionContext'; import { MockRepository } from '../mocks/mockRepository'; import { MockCommandRegistry } from '../mocks/mockCommandRegistry'; @@ -22,7 +24,6 @@ import { GitHubRemote, Remote } from '../../common/remote'; import { Protocol } from '../../common/protocol'; import { CredentialStore, GitHub } from '../../github/credentials'; import { parseGraphQLPullRequest } from '../../github/utils'; -import { Resource } from '../../common/resources'; import { GitApiImpl } from '../../api/api1'; import { RepositoriesManager } from '../../github/repositoriesManager'; import { LoggingOctokit, RateLogger } from '../../github/loggingOctokit'; @@ -31,6 +32,10 @@ import { DataUri } from '../../common/uri'; import { IAccount, ITeam } from '../../github/interface'; import { asPromise } from '../../common/utils'; import { CreatePullRequestHelper } from '../../view/createPullRequestHelper'; +import { CopilotRemoteAgentManager } from '../../github/copilotRemoteAgent'; +import { MockThemeWatcher } from '../mocks/mockThemeWatcher'; +import { MockPrsTreeModel } from '../mocks/mockPRsTreeModel'; +import { PrsTreeModel } from '../../view/prsTreeModel'; describe('GitHub Pull Requests view', function () { let sinon: SinonSandbox; @@ -40,11 +45,16 @@ describe('GitHub Pull Requests view', function () { let credentialStore: CredentialStore; let reposManager: RepositoriesManager; let createPrHelper: CreatePullRequestHelper; - + let copilotManager: CopilotRemoteAgentManager; + let mockThemeWatcher: MockThemeWatcher; + let gitAPI: GitApiImpl; + let mockNotificationsManager: MockNotificationManager; + let prsTreeModel: PrsTreeModel; beforeEach(function () { sinon = createSandbox(); MockCommandRegistry.install(sinon); + mockThemeWatcher = new MockThemeWatcher(); context = new MockExtensionContext(); @@ -53,8 +63,12 @@ describe('GitHub Pull Requests view', function () { credentialStore, telemetry, ); - provider = new PullRequestsTreeDataProvider(telemetry, context, reposManager); + prsTreeModel = new PrsTreeModel(telemetry, reposManager, context); credentialStore = new CredentialStore(telemetry, context); + gitAPI = new GitApiImpl(reposManager); + copilotManager = new CopilotRemoteAgentManager(credentialStore, reposManager, telemetry, context, gitAPI, prsTreeModel); + provider = new PullRequestsTreeDataProvider(prsTreeModel, telemetry, context, reposManager, copilotManager); + mockNotificationsManager = new MockNotificationManager(); createPrHelper = new CreatePullRequestHelper(); // For tree view unit tests, we don't test the authentication flow, so `showSignInNotification` returns @@ -67,13 +81,11 @@ describe('GitHub Pull Requests view', function () { userAgent: 'GitHub VSCode Pull Requests', previews: ['shadow-cat-preview'], }), new RateLogger(telemetry, true)), - graphql: null, + graphql: {} as any, }; return github; }); - - Resource.initialize(context); }); afterEach(function () { @@ -101,8 +113,8 @@ describe('GitHub Pull Requests view', function () { it('has no children when repositories have not yet been initialized', async function () { const repository = new MockRepository(); repository.addRemote('origin', 'git@github.com:aaa/bbb'); - reposManager.insertFolderManager(new FolderRepositoryManager(0, context, repository, telemetry, new GitApiImpl(), credentialStore, createPrHelper)); - provider.initialize([], credentialStore); + reposManager.insertFolderManager(new FolderRepositoryManager(0, context, repository, telemetry, new GitApiImpl(reposManager), credentialStore, createPrHelper, mockThemeWatcher)); + provider.initialize([], mockNotificationsManager as NotificationsManager); const rootNodes = await provider.getChildren(); assert.strictEqual(rootNodes.length, 0); @@ -111,10 +123,12 @@ describe('GitHub Pull Requests view', function () { it('opens the viewlet and displays the default categories', async function () { const repository = new MockRepository(); repository.addRemote('origin', 'git@github.com:aaa/bbb'); - reposManager.insertFolderManager(new FolderRepositoryManager(0, context, repository, telemetry, new GitApiImpl(), credentialStore, createPrHelper)); + const folderManager = new FolderRepositoryManager(0, context, repository, telemetry, new GitApiImpl(reposManager), credentialStore, createPrHelper, mockThemeWatcher); + sinon.stub(folderManager, 'getPullRequestDefaults').returns(Promise.resolve({ owner: 'aaa', repo: 'bbb', base: 'main' })); + reposManager.insertFolderManager(folderManager); sinon.stub(credentialStore, 'isAuthenticated').returns(true); await reposManager.folderManagers[0].updateRepositories(); - provider.initialize([], credentialStore); + provider.initialize([], mockNotificationsManager as NotificationsManager); const rootNodes = await provider.getChildren(); @@ -124,7 +138,41 @@ describe('GitHub Pull Requests view', function () { assert(treeItems[treeItems.length - 1].collapsibleState === vscode.TreeItemCollapsibleState.Expanded); assert.deepStrictEqual( treeItems.map(n => n.label), - ['Local Pull Request Branches', 'Waiting For My Review', 'Assigned To Me', 'Created By Me', 'All Open'], + ['Copilot on My Behalf', 'Local Pull Request Branches', 'Waiting For My Review', 'Created By Me', 'All Open'], + ); + }); + + it('refreshes tree when GitHub repositories are discovered in existing folder manager', async function () { + const repository = new MockRepository(); + repository.addRemote('origin', 'git@github.com:aaa/bbb'); + const folderManager = new FolderRepositoryManager(0, context, repository, telemetry, new GitApiImpl(reposManager), credentialStore, createPrHelper, mockThemeWatcher); + sinon.stub(folderManager, 'getPullRequestDefaults').returns(Promise.resolve({ owner: 'aaa', repo: 'bbb', base: 'main' })); + reposManager.insertFolderManager(folderManager); + provider.initialize([], mockNotificationsManager as NotificationsManager); + + // Initially no children because no GitHub repositories are loaded yet + let rootNodes = await provider.getChildren(); + assert.strictEqual(rootNodes.length, 0); + + // Listen to the prsTreeModel's onDidChangeData event which is what actually drives the tree refresh + const onDidChangeDataSpy = sinon.spy(); + provider.prsTreeModel.onDidChangeData(onDidChangeDataSpy); + + // Simulate GitHub repositories being discovered (as happens when remotes load after activation) + sinon.stub(credentialStore, 'isAuthenticated').returns(true); + await folderManager.updateRepositories(); + + // Verify that the tree model's data change event was triggered + assert(onDidChangeDataSpy.calledWith(folderManager), + 'Tree model should fire data change event with the folder manager when GitHub repositories are discovered'); + + // Verify tree now has content + rootNodes = await provider.getChildren(); + const treeItems = await Promise.all(rootNodes.map(node => node.getTreeItem())); + assert.deepStrictEqual( + treeItems.map(n => n.label), + ['Copilot on My Behalf', 'Local Pull Request Branches', 'Waiting For My Review', 'Created By Me', 'All Open'], + 'Tree should display categories after GitHub repositories are discovered', ); }); @@ -151,7 +199,7 @@ describe('GitHub Pull Requests view', function () { ); }); }).pullRequest; - const prItem0 = parseGraphQLPullRequest(pr0.repository.pullRequest, gitHubRepository); + const prItem0 = await parseGraphQLPullRequest(pr0.repository!.pullRequest, gitHubRepository); const pullRequest0 = new PullRequestModel(credentialStore, telemetry, gitHubRepository, remote, prItem0); const pr1 = gitHubRepository.addGraphQLPullRequest(builder => { @@ -168,7 +216,7 @@ describe('GitHub Pull Requests view', function () { ); }); }).pullRequest; - const prItem1 = parseGraphQLPullRequest(pr1.repository.pullRequest, gitHubRepository); + const prItem1 = await parseGraphQLPullRequest(pr1.repository!.pullRequest, gitHubRepository); const pullRequest1 = new PullRequestModel(credentialStore, telemetry, gitHubRepository, remote, prItem1); const repository = new MockRepository(); @@ -181,7 +229,7 @@ describe('GitHub Pull Requests view', function () { await repository.createBranch('non-pr-branch', false); - const manager = new FolderRepositoryManager(0, context, repository, telemetry, new GitApiImpl(), credentialStore, createPrHelper); + const manager = new FolderRepositoryManager(0, context, repository, telemetry, new GitApiImpl(reposManager), credentialStore, createPrHelper, mockThemeWatcher); reposManager.insertFolderManager(manager); sinon.stub(manager, 'createGitHubRepository').callsFake((r, cs) => { assert.deepStrictEqual(r, remote); @@ -193,7 +241,7 @@ describe('GitHub Pull Requests view', function () { return Promise.resolve(users.map(user => user.avatarUrl ? vscode.Uri.parse(user.avatarUrl) : undefined)); }); await manager.updateRepositories(); - provider.initialize([], credentialStore); + provider.initialize([], mockNotificationsManager as NotificationsManager); manager.activePullRequest = pullRequest1; const rootNodes = await provider.getChildren(); @@ -208,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/test/view/reviewCommentController.test.ts b/src/test/view/reviewCommentController.test.ts index 6b6d742a72..ba473d2bd8 100644 --- a/src/test/view/reviewCommentController.test.ts +++ b/src/test/view/reviewCommentController.test.ts @@ -30,15 +30,18 @@ import { ReviewManager, ShowPullRequest } from '../../view/reviewManager'; import { PullRequestChangesTreeDataProvider } from '../../view/prChangesTreeDataProvider'; import { MockExtensionContext } from '../mocks/mockExtensionContext'; import { ReviewModel } from '../../view/reviewModel'; -import { Resource } from '../../common/resources'; import { RepositoriesManager } from '../../github/repositoriesManager'; import { GitFileChangeModel } from '../../view/fileChangeModel'; import { WebviewViewCoordinator } from '../../view/webviewViewCoordinator'; import { GitHubServerType } from '../../common/authentication'; import { CreatePullRequestHelper } from '../../view/createPullRequestHelper'; import { mergeQuerySchemaWithShared } from '../../github/common'; -import { GitHubRef } from '../../common/githubRef'; import { AccountType } from '../../github/interface'; +import { CopilotRemoteAgentManager } from '../../github/copilotRemoteAgent'; +import { MockThemeWatcher } from '../mocks/mockThemeWatcher'; +import { asPromise } from '../../common/utils'; +import { PrsTreeModel } from '../../view/prsTreeModel'; +import { MockPrsTreeModel } from '../mocks/mockPRsTreeModel'; const schema = mergeQuerySchemaWithShared(require('../../github/queries.gql'), require('../../github/queriesShared.gql')) as any; const protocol = new Protocol('https://github.com/github/test.git'); @@ -62,10 +65,14 @@ describe('ReviewCommentController', function () { let reviewManager: ReviewManager; let reposManager: RepositoriesManager; let gitApiImpl: GitApiImpl; + let copilotManager: CopilotRemoteAgentManager; + let mockThemeWatcher: MockThemeWatcher; + let mockPrsTreeModel: PrsTreeModel; beforeEach(async function () { sinon = createSandbox(); MockCommandRegistry.install(sinon); + mockThemeWatcher = new MockThemeWatcher(); telemetry = new MockTelemetry(); const context = new MockExtensionContext(); @@ -74,12 +81,13 @@ describe('ReviewCommentController', function () { repository = new MockRepository(); repository.addRemote('origin', 'git@github.com:aaa/bbb'); reposManager = new RepositoriesManager(credentialStore, telemetry); - provider = new PullRequestsTreeDataProvider(telemetry, context, reposManager); + gitApiImpl = new GitApiImpl(reposManager); + mockPrsTreeModel = new MockPrsTreeModel() as unknown as PrsTreeModel; + copilotManager = new CopilotRemoteAgentManager(credentialStore, reposManager, telemetry, context, gitApiImpl, mockPrsTreeModel); + provider = new PullRequestsTreeDataProvider(mockPrsTreeModel, telemetry, context, reposManager, copilotManager); const activePrViewCoordinator = new WebviewViewCoordinator(context); const createPrHelper = new CreatePullRequestHelper(); - Resource.initialize(context); - gitApiImpl = new GitApiImpl(); - manager = new FolderRepositoryManager(0, context, repository, telemetry, gitApiImpl, credentialStore, createPrHelper); + manager = new FolderRepositoryManager(0, context, repository, telemetry, gitApiImpl, credentialStore, createPrHelper, mockThemeWatcher); reposManager.insertFolderManager(manager); const tree = new PullRequestChangesTreeDataProvider(gitApiImpl, reposManager); reviewManager = new ReviewManager(0, context, repository, manager, telemetry, tree, provider, new ShowPullRequest(), activePrViewCoordinator, createPrHelper, gitApiImpl); @@ -323,8 +331,9 @@ describe('ReviewCommentController', function () { } ) + const newReviewThreadPromise = asPromise(activePullRequest.onDidChangeReviewThreads); await reviewCommentController.createOrReplyComment(thread, 'hello world', false); - + await newReviewThreadPromise; assert.strictEqual(thread.comments.length, 1); assert.strictEqual(thread.comments[0].parent, thread); diff --git a/src/themeWatcher.ts b/src/themeWatcher.ts new file mode 100644 index 0000000000..ea8d047000 --- /dev/null +++ b/src/themeWatcher.ts @@ -0,0 +1,41 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { Disposable } from './common/lifecycle'; +import { COLOR_THEME, WORKBENCH } from './common/settingKeys'; +import { loadCurrentThemeData, ThemeData } from './view/theme'; + +export interface IThemeWatcher { + readonly onDidChangeTheme: vscode.Event; + readonly themeData: ThemeData | undefined; +} + +export class ThemeWatcher extends Disposable implements IThemeWatcher { + private _themeData: ThemeData | undefined; + private _onDidChangeTheme = this._register(new vscode.EventEmitter()); + readonly onDidChangeTheme = this._onDidChangeTheme.event; + + constructor() { + super(); + this._register( + vscode.workspace.onDidChangeConfiguration(async e => { + if (e.affectsConfiguration(`${WORKBENCH}.${COLOR_THEME}`)) { + await this.updateTheme(); + } + }), + ); + this.updateTheme(); + } + + private async updateTheme() { + this._themeData = await loadCurrentThemeData(); + this._onDidChangeTheme.fire(this._themeData); + } + + get themeData() { + return this._themeData; + } +} \ No newline at end of file diff --git a/src/uriHandler.ts b/src/uriHandler.ts new file mode 100644 index 0000000000..817f6e17ee --- /dev/null +++ b/src/uriHandler.ts @@ -0,0 +1,212 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { 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'; + +interface PendingCheckoutPayload { + owner: string; + repo: string; + pullRequestNumber: number; + 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(reviewsManager: ReviewsManager, folderManager: FolderRepositoryManager, owner: string, repo: string, prNumber: number): Promise { + try { + 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); + return; + } + + const proceed = await showCheckoutPrompt(owner, repo, prNumber); + if (!proceed) { + return; + } + + 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(reviewsManager: ReviewsManager, 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 folderManager = reposManager.getManagerForRepository(pending.owner, pending.repo); + if (!folderManager) { + return false; + } + await performPullRequestCheckout(reviewsManager, folderManager, 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 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, + private readonly _reviewsManagers: ReviewsManager, + private readonly _telemetry: ITelemetry, + private readonly _context: vscode.ExtensionContext, + private readonly _git: GitApiImpl + ) { } + + async handleUri(uri: vscode.Uri): Promise { + switch (uri.path) { + case UriHandlerPaths.OpenIssueWebview: + return this._openIssueWebview(uri); + case UriHandlerPaths.OpenPullRequestWebview: + return this._openPullRequestWebview(uri); + case UriHandlerPaths.CheckoutPullRequest: + // 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); + } + } + + private async _openIssueWebview(uri: vscode.Uri): Promise { + const params = fromOpenIssueWebviewUri(uri); + if (!params) { + return; + } + const folderManager = this._reposManagers.getManagerForRepository(params.owner, params.repo) ?? this._reposManagers.folderManagers[0]; + const issue = await folderManager.resolveIssue(params.owner, params.repo, params.issueNumber, true); + if (!issue) { + return; + } + return IssueOverviewPanel.createOrShow(this._telemetry, this._context.extensionUri, folderManager, issue); + } + + private async _openPullRequestWebview(uri: vscode.Uri): Promise { + const params = fromOpenOrCheckoutPullRequestWebviewUri(uri); + if (!params) { + return; + } + const folderManager = this._reposManagers.getManagerForRepository(params.owner, params.repo) ?? this._reposManagers.folderManagers[0]; + const pullRequest = await folderManager.resolvePullRequest(params.owner, params.repo, params.pullRequestNumber); + if (!pullRequest) { + return; + } + 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); + 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 { + const params = fromOpenOrCheckoutPullRequestWebviewUri(uri); + if (!params) { + return; + } + const folderManager = this._reposManagers.getManagerForRepository(params.owner, params.repo); + if (folderManager) { + 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) => { + 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) { + 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 { + 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); + } + } + } + +} diff --git a/src/view/commentControllBase.ts b/src/view/commentControllBase.ts index c4e16b1f6b..083b193ccd 100644 --- a/src/view/commentControllBase.ts +++ b/src/view/commentControllBase.ts @@ -6,16 +6,20 @@ import * as vscode from 'vscode'; import { Disposable } from '../common/lifecycle'; import { ITelemetry } from '../common/telemetry'; +import { Schemes } from '../common/uri'; import { FolderRepositoryManager } from '../github/folderRepositoryManager'; import { GitHubRepository } from '../github/githubRepository'; -import { PullRequestModel } from '../github/pullRequestModel'; +import { isCopilotOnMyBehalf, PullRequestModel } from '../github/pullRequestModel'; export abstract class CommentControllerBase extends Disposable { constructor( protected _folderRepoManager: FolderRepositoryManager, protected _telemetry: ITelemetry + ) { super(); + + this._register(vscode.window.onDidChangeActiveTextEditor(e => this.onDidChangeActiveTextEditor(e))); } protected _commentController: vscode.CommentController; @@ -37,4 +41,26 @@ export abstract class CommentControllerBase extends Disposable { } return githubRepositories; } -} \ No newline at end of file + + protected abstract onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined); + + protected async tryAddCopilotMention(editor: vscode.TextEditor, pullRequest: PullRequestModel) { + if (editor.document.uri.scheme !== Schemes.Comment) { + return; + } + + if (editor.document.lineCount < 1 || editor.document.lineAt(0).text.length > 0) { + return; + } + + const currentUser = await this._folderRepoManager.getCurrentUser(); + if (!await isCopilotOnMyBehalf(pullRequest, currentUser)) { + return; + } + + return editor.edit(editBuilder => { + editBuilder.insert(new vscode.Position(0, 0), '@copilot '); + }); + } +} + diff --git a/src/view/commentDecorationProvider.ts b/src/view/commentDecorationProvider.ts index 570b6ae920..86a14aa2fd 100644 --- a/src/view/commentDecorationProvider.ts +++ b/src/view/commentDecorationProvider.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { TreeDecorationProvider } from './treeDecorationProviders'; import { fromFileChangeNodeUri, Schemes } from '../common/uri'; import { FolderRepositoryManager } from '../github/folderRepositoryManager'; import { PullRequestModel } from '../github/pullRequestModel'; import { RepositoriesManager } from '../github/repositoriesManager'; -import { TreeDecorationProvider } from './treeDecorationProviders'; export class CommentDecorationProvider extends TreeDecorationProvider { @@ -36,7 +36,7 @@ export class CommentDecorationProvider extends TreeDecorationProvider { const folderManager = this._repositoriesManager.getManagerForFile(uri); if (query && folderManager) { const hasComment = folderManager.gitHubRepositories.find(repo => { - const pr = repo.pullRequestModels.get(query.prNumber); + const pr = repo.getExistingPullRequestModel(query.prNumber); if (pr?.reviewThreadsCache.find(c => c.path === query.fileName)) { return true; } diff --git a/src/view/commitsDecorationProvider.ts b/src/view/commitsDecorationProvider.ts new file mode 100644 index 0000000000..d5fc5ccad9 --- /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/compareChangesTreeDataProvider.ts b/src/view/compareChangesTreeDataProvider.ts index 339dd8f28b..edae2c9721 100644 --- a/src/view/compareChangesTreeDataProvider.ts +++ b/src/view/compareChangesTreeDataProvider.ts @@ -5,6 +5,7 @@ import * as pathLib from 'path'; import * as vscode from 'vscode'; +import { CreatePullRequestDataModel } from './createPullRequestDataModel'; import { Change, Commit } from '../api/api'; import { Status } from '../api/api1'; import { getGitChangeType } from '../common/diffHunk'; @@ -15,7 +16,6 @@ import { Schemes } from '../common/uri'; import { dateFromNow } from '../common/utils'; import { OctokitCommon } from '../github/common'; import { FolderRepositoryManager } from '../github/folderRepositoryManager'; -import { CreatePullRequestDataModel } from './createPullRequestDataModel'; import { GitHubFileChangeNode } from './treeNodes/fileChangeNode'; import { BaseTreeNode, TreeNode, TreeNodeParent } from './treeNodes/treeNode'; @@ -166,7 +166,8 @@ abstract class CompareChangesTreeProvider extends Disposable implements vscode.T return { rawFiles, rawCommits, mergeBase }; } catch (e) { - if ('name' in e && e.name === 'HttpError' && e.status === 404) { + const eWithName: Partial<{ name: string; status: number }> = e; + if (e.name && eWithName.name === 'HttpError' && eWithName.status === 404) { (this.view as vscode.TreeView2).message = new vscode.MarkdownString(vscode.l10n.t('The upstream branch `{0}` does not exist on GitHub', this.model.baseBranch)); } return {}; diff --git a/src/view/createPullRequestDataModel.ts b/src/view/createPullRequestDataModel.ts index 16392a25a5..050f83a2ae 100644 --- a/src/view/createPullRequestDataModel.ts +++ b/src/view/createPullRequestDataModel.ts @@ -4,13 +4,13 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { ChangesContentProvider, GitContentProvider, GitHubContentProvider } from './gitHubContentProvider'; import { Change, Commit } from '../api/api'; import { Disposable } from '../common/lifecycle'; import Logger from '../common/logger'; import { OctokitCommon } from '../github/common'; import { FolderRepositoryManager } from '../github/folderRepositoryManager'; import { GitHubRepository } from '../github/githubRepository'; -import { ChangesContentProvider, GitContentProvider, GitHubContentProvider } from './gitHubContentProvider'; export interface CreateModelChangeEvent { baseOwner?: string; @@ -155,6 +155,11 @@ export class CreatePullRequestDataModel extends Disposable { private async updateHasUpstream(branch: string): Promise { const compareBranch = await this.folderRepositoryManager.repository.getBranch(branch); this._compareHasUpstream = !!compareBranch.upstream; + // Check that the upstream head matches the local head + if (this._compareHasUpstream) { + const upstream = await this.gitHubRepository?.hasBranch(branch); + this._compareHasUpstream = upstream === compareBranch.commit; + } return this._compareHasUpstream; } diff --git a/src/view/createPullRequestHelper.ts b/src/view/createPullRequestHelper.ts index 16cc77feae..63339033e8 100644 --- a/src/view/createPullRequestHelper.ts +++ b/src/view/createPullRequestHelper.ts @@ -4,6 +4,8 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { CompareChanges } from './compareChangesTreeDataProvider'; +import { CreatePullRequestDataModel } from './createPullRequestDataModel'; import { Repository } from '../api/api'; import { commands } from '../common/executeCommands'; import { addDisposable, Disposable, disposeAll } from '../common/lifecycle'; @@ -12,8 +14,6 @@ import { BaseCreatePullRequestViewProvider, BasePullRequestDataModel, CreatePull import { FolderRepositoryManager, PullRequestDefaults } from '../github/folderRepositoryManager'; import { PullRequestModel } from '../github/pullRequestModel'; import { RevertPullRequestViewProvider } from '../github/revertPRViewProvider'; -import { CompareChanges } from './compareChangesTreeDataProvider'; -import { CreatePullRequestDataModel } from './createPullRequestDataModel'; export class CreatePullRequestHelper extends Disposable { private _currentDisposables: vscode.Disposable[] = []; diff --git a/src/view/emojiCompletionProvider.ts b/src/view/emojiCompletionProvider.ts new file mode 100644 index 0000000000..ffb75844cb --- /dev/null +++ b/src/view/emojiCompletionProvider.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 { ensureEmojis } from '../common/emoji'; +import { Schemes } from '../common/uri'; + +export class EmojiCompletionProvider implements vscode.CompletionItemProvider { + private _emojiCompletions: vscode.CompletionItem[] = []; + + constructor(private _context: vscode.ExtensionContext) { + void this.buildEmojiCompletions(); + } + + private async buildEmojiCompletions(): Promise { + const emojis = await ensureEmojis(this._context); + + for (const [name, emoji] of Object.entries(emojis)) { + const completionItem = new vscode.CompletionItem({ label: emoji, description: `:${name}:` }, vscode.CompletionItemKind.Text); + completionItem.filterText = `:${name}:`; + completionItem.sortText = name; + this._emojiCompletions.push(completionItem); + } + } + + provideCompletionItems( + document: vscode.TextDocument, + position: vscode.Position, + _token: vscode.CancellationToken, + context: vscode.CompletionContext + ): vscode.ProviderResult { + // Only provide completions for comment documents + if (document.uri.scheme !== Schemes.Comment) { + return []; + } + + const word = document.getWordRangeAtPosition(position, /:([-+_a-z0-9]+:?)?/i); + if (!word) { + return []; + } + + // If invoked by trigger charcter, ignore if this is the start of an emoji (single ':') and there is no preceding space + if (context.triggerKind === vscode.CompletionTriggerKind.TriggerCharacter) { + if (word.end.character - word.start.character === 1 && word.start.character > 0) { + const charBefore = document.getText(new vscode.Range(word.start.translate(0, -1), word.start)); + if (!/\s/.test(charBefore)) { + return []; + } + } + } + + // Update the range on cached items directly + for (const item of this._emojiCompletions) { + item.range = word; + } + + return new vscode.CompletionList(this._emojiCompletions, false); + } +} diff --git a/src/view/fileChangeModel.ts b/src/view/fileChangeModel.ts index 3c4c81dbcf..06661245b1 100644 --- a/src/view/fileChangeModel.ts +++ b/src/view/fileChangeModel.ts @@ -5,7 +5,7 @@ import * as vscode from 'vscode'; import { ViewedState } from '../common/comment'; -import { DiffHunk, parsePatch } from '../common/diffHunk'; +import { DiffChangeType, DiffHunk, parsePatch } from '../common/diffHunk'; import { GitChangeType, InMemFileChange, SimpleFileChange, SlimFileChange } from '../common/file'; import Logger from '../common/logger'; import { resolvePath, toPRUri, toReviewUri } from '../common/uri'; @@ -66,6 +66,28 @@ export abstract class FileChangeModel { return diffHunks; } + public async calculateChangedLinesCount(): Promise<{ added: number; removed: number }> { + try { + const diffHunks = await this.diffHunks(); + let added = 0; + let removed = 0; + + for (const hunk of diffHunks) { + for (const line of hunk.diffLines) { + if (line.type === DiffChangeType.Add) { + ++added; + } else if (line.type === DiffChangeType.Delete) { + ++removed; + } + } + } + return { added, removed }; + } catch (error) { + Logger.warn(`Failed to calculate added/removed lines for ${this.fileName}: ${error}`, FileChangeModel.ID); + return { added: 0, removed: 0 }; + } + } + constructor(public readonly pullRequest: PullRequestModel, protected readonly folderRepoManager: FolderRepositoryManager, public readonly change: SimpleFileChange, @@ -94,7 +116,7 @@ export class GitFileChangeModel extends FileChangeModel { } } - private _show: Promise + private _show: Promise; async showBase(): Promise { if (!this._show && this.change.status !== GitChangeType.ADD) { const commit = ((this.change instanceof InMemFileChange || this.change instanceof SlimFileChange) ? this.change.baseCommit : this.sha!); diff --git a/src/view/fileTypeDecorationProvider.ts b/src/view/fileTypeDecorationProvider.ts index 25e8d8e2a3..3d6c3283f5 100644 --- a/src/view/fileTypeDecorationProvider.ts +++ b/src/view/fileTypeDecorationProvider.ts @@ -5,11 +5,11 @@ import * as path from 'path'; import * as vscode from 'vscode'; +import { TreeDecorationProvider } from './treeDecorationProviders'; import { GitChangeType } from '../common/file'; import { FileChangeNodeUriParams, fromFileChangeNodeUri, fromPRUri, PRUriParams } from '../common/uri'; import { FolderRepositoryManager } from '../github/folderRepositoryManager'; import { PullRequestModel } from '../github/pullRequestModel'; -import { TreeDecorationProvider } from './treeDecorationProviders'; export class FileTypeDecorationProvider extends TreeDecorationProvider { constructor() { diff --git a/src/view/gitContentProvider.ts b/src/view/gitContentProvider.ts index bcfd5dac71..be99f25a92 100644 --- a/src/view/gitContentProvider.ts +++ b/src/view/gitContentProvider.ts @@ -6,15 +6,15 @@ import * as pathLib from 'path'; import * as vscode from 'vscode'; +import { GitFileChangeModel } from './fileChangeModel'; +import { RepositoryFileSystemProvider } from './repositoryFileSystemProvider'; +import { ReviewManager } from './reviewManager'; import { Repository } from '../api/api'; import { GitApiImpl } from '../api/api1'; import Logger from '../common/logger'; import { fromReviewUri } from '../common/uri'; import { CredentialStore } from '../github/credentials'; import { getRepositoryForFile } from '../github/utils'; -import { GitFileChangeModel } from './fileChangeModel'; -import { RepositoryFileSystemProvider } from './repositoryFileSystemProvider'; -import { ReviewManager } from './reviewManager'; import { GitFileChangeNode, RemoteFileChangeNode } from './treeNodes/fileChangeNode'; export class GitContentFileSystemProvider extends RepositoryFileSystemProvider { @@ -96,8 +96,10 @@ export class GitContentFileSystemProvider extends RepositoryFileSystemProvider { // Only show the error if we know it's not an outdated commit if (!this.getOutdatedChangeModelForFile(uri)) { vscode.window.showErrorMessage( - `We couldn't find commit ${commit} locally. You may want to sync the branch with remote. Sometimes commits can disappear after a force-push`, + vscode.l10n.t('We couldn\'t find commit {0} locally. You may want to sync the branch with remote. Sometimes commits can disappear after a force-push.', commit), ); + } else { + vscode.window.showInformationMessage(vscode.l10n.t('We couldn\'t find commit {0}. Sometimes commits can disappear after a force-push.', commit)); } } } diff --git a/src/view/gitHubContentProvider.ts b/src/view/gitHubContentProvider.ts index 40c1b2849a..ac25a7c481 100644 --- a/src/view/gitHubContentProvider.ts +++ b/src/view/gitHubContentProvider.ts @@ -59,7 +59,7 @@ export abstract class ChangesContentProvider implements Partial { } }; } - stat(_uri: any): vscode.FileStat { + async stat(_uri: any): Promise { // const params = fromGitHubURI(uri); return { @@ -141,6 +141,28 @@ export class GitContentProvider extends ChangesContentProvider implements vscode super(); } + override async stat(uri: vscode.Uri): Promise { + let mtime = 0; + const params = fromGitHubURI(uri); + if (params?.branch) { + const branch = await this.folderRepositoryManager.repository.getBranch(params?.branch); + if (branch.commit) { + const commit = await this.folderRepositoryManager.repository.getCommit(branch.commit); + if (commit) { + mtime = commit.commitDate?.getTime() ?? 0; + } + } + + } + return { + type: vscode.FileType.File, + ctime: 0, + mtime: mtime, + size: 0, + permissions: vscode.FilePermission.Readonly + }; + } + async readFile(uri: vscode.Uri): Promise { const params = fromGitHubURI(uri); if (!params || params.isEmpty) { diff --git a/src/view/githubFileContentProvider.ts b/src/view/githubFileContentProvider.ts new file mode 100644 index 0000000000..059f113840 --- /dev/null +++ b/src/view/githubFileContentProvider.ts @@ -0,0 +1,39 @@ +/*--------------------------------------------------------------------------------------------- + * 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 { RepositoryFileSystemProvider } from './repositoryFileSystemProvider'; +import { GitApiImpl } from '../api/api1'; +import { fromGitHubCommitUri } from '../common/uri'; +import { CredentialStore } from '../github/credentials'; +import { RepositoriesManager } from '../github/repositoriesManager'; + +export class GitHubCommitFileSystemProvider extends RepositoryFileSystemProvider { + constructor(private readonly repos: RepositoriesManager, gitAPI: GitApiImpl, credentialStore: CredentialStore) { + super(gitAPI, credentialStore); + } + + override async readFile(uri: vscode.Uri): Promise { + await this.waitForAuth(); + await this.waitForAnyGitHubRepos(this.repos); + + const params = fromGitHubCommitUri(uri); + if (!params) { + throw new Error(`Invalid GitHub commit URI: ${uri.toString()}`); + } + + const folderManager = this.repos.getManagerForRepository(params.owner, params.repo); + if (!folderManager) { + throw new Error(`Repository not found for owner: ${params.owner}, repo: ${params.repo}`); + } + + const githubRepo = await folderManager.createGitHubRepositoryFromOwnerName(params.owner, params.repo); + if (!githubRepo) { + throw new Error(`GitHub repository not found for owner: ${params.owner}, repo: ${params.repo}`); + } + + return githubRepo.getFile(uri.path, params.commit); + } +} \ No newline at end of file diff --git a/src/view/inMemPRContentProvider.ts b/src/view/inMemPRContentProvider.ts index 80403cced0..ddd7e92a69 100644 --- a/src/view/inMemPRContentProvider.ts +++ b/src/view/inMemPRContentProvider.ts @@ -5,6 +5,8 @@ 'use strict'; import * as vscode from 'vscode'; +import { FileChangeModel, InMemFileChangeModel, RemoteFileChangeModel } from './fileChangeModel'; +import { RepositoryFileSystemProvider } from './repositoryFileSystemProvider'; import { GitApiImpl } from '../api/api1'; import { DiffChangeType, getModifiedContentFromDiffHunk } from '../common/diffHunk'; import { GitChangeType, InMemFileChange, SlimFileChange } from '../common/file'; @@ -14,8 +16,6 @@ import { CredentialStore } from '../github/credentials'; import { FolderRepositoryManager, ReposManagerState } from '../github/folderRepositoryManager'; import { IResolvedPullRequestModel, PullRequestModel } from '../github/pullRequestModel'; import { RepositoriesManager } from '../github/repositoriesManager'; -import { FileChangeModel, InMemFileChangeModel, RemoteFileChangeModel } from './fileChangeModel'; -import { RepositoryFileSystemProvider } from './repositoryFileSystemProvider'; export class InMemPRFileSystemProvider extends RepositoryFileSystemProvider { private _prFileChangeContentProviders: { [key: number]: (uri: vscode.Uri) => Promise } = {}; diff --git a/src/view/prChangesTreeDataProvider.ts b/src/view/prChangesTreeDataProvider.ts index 2b4089f313..1a6890114b 100644 --- a/src/view/prChangesTreeDataProvider.ts +++ b/src/view/prChangesTreeDataProvider.ts @@ -4,17 +4,17 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { ProgressHelper } from './progress'; +import { ReviewModel } from './reviewModel'; import { GitApiImpl } from '../api/api1'; import { commands, contexts } from '../common/executeCommands'; import { Disposable } from '../common/lifecycle'; import Logger, { PR_TREE } from '../common/logger'; -import { FILE_LIST_LAYOUT, GIT, OPEN_DIFF_ON_CLICK, PR_SETTINGS_NAMESPACE } from '../common/settingKeys'; +import { FILE_LIST_LAYOUT, GIT, HIDE_VIEWED_FILES, OPEN_DIFF_ON_CLICK, PR_SETTINGS_NAMESPACE } from '../common/settingKeys'; import { isDescendant } from '../common/utils'; import { FolderRepositoryManager } from '../github/folderRepositoryManager'; import { PullRequestModel } from '../github/pullRequestModel'; import { RepositoriesManager } from '../github/repositoriesManager'; -import { ProgressHelper } from './progress'; -import { ReviewModel } from './reviewModel'; import { GitFileChangeNode } from './treeNodes/fileChangeNode'; import { RepositoryChangesNode } from './treeNodes/repositoryChangesNode'; import { BaseTreeNode, TreeNode } from './treeNodes/treeNode'; @@ -50,6 +50,8 @@ export class PullRequestChangesTreeDataProvider extends Disposable implements vs await vscode.commands.executeCommand('setContext', 'fileListLayout:flat', layout === 'flat'); } else if (e.affectsConfiguration(`${GIT}.${OPEN_DIFF_ON_CLICK}`)) { this._onDidChangeTreeData.fire(); + } else if (e.affectsConfiguration(`${PR_SETTINGS_NAMESPACE}.${HIDE_VIEWED_FILES}`)) { + this._onDidChangeTreeData.fire(); } }), ); diff --git a/src/view/prNotificationDecorationProvider.ts b/src/view/prNotificationDecorationProvider.ts deleted file mode 100644 index 08119395d3..0000000000 --- a/src/view/prNotificationDecorationProvider.ts +++ /dev/null @@ -1,47 +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 { Disposable } from '../common/lifecycle'; -import { fromPRNodeUri } from '../common/uri'; -import { NotificationProvider } from '../github/notifications'; - -export class PRNotificationDecorationProvider extends Disposable implements vscode.FileDecorationProvider { - private _onDidChangeFileDecorations: vscode.EventEmitter = new vscode.EventEmitter< - vscode.Uri | vscode.Uri[] - >(); - onDidChangeFileDecorations: vscode.Event = this._onDidChangeFileDecorations.event; - - - constructor(private readonly _notificationProvider: NotificationProvider) { - super(); - this._register(vscode.window.registerFileDecorationProvider(this)); - this._register( - this._notificationProvider.onDidChangeNotifications(PRNodeUris => this._onDidChangeFileDecorations.fire(PRNodeUris)) - ); - } - - provideFileDecoration( - uri: vscode.Uri, - _token: vscode.CancellationToken, - ): vscode.ProviderResult { - if (!uri.query) { - return; - } - - const prNodeParams = fromPRNodeUri(uri); - - if (prNodeParams && this._notificationProvider.hasNotification(prNodeParams.prIdentifier)) { - return { - propagate: false, - color: new vscode.ThemeColor('pullRequests.notification'), - badge: '●', - tooltip: 'unread notification' - }; - } - - return undefined; - } -} diff --git a/src/view/prStatusDecorationProvider.ts b/src/view/prStatusDecorationProvider.ts index f73209fa8a..c8f44b1eba 100644 --- a/src/view/prStatusDecorationProvider.ts +++ b/src/view/prStatusDecorationProvider.ts @@ -4,10 +4,16 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { PrsTreeModel } from './prsTreeModel'; import { Disposable } from '../common/lifecycle'; -import { createPRNodeUri, fromPRNodeUri, Schemes } from '../common/uri'; +import { Protocol } from '../common/protocol'; +import { NOTIFICATION_SETTING, NotificationVariants, PR_SETTINGS_NAMESPACE } from '../common/settingKeys'; +import { EventType } from '../common/timelineEvent'; +import { createPRNodeUri, fromPRNodeUri, fromQueryUri, parsePRNodeIdentifier, PRNodeUriParams, Schemes, toQueryUri } from '../common/uri'; +import { CopilotRemoteAgentManager } from '../github/copilotRemoteAgent'; import { getStatusDecoration } from '../github/markdownUtils'; -import { PrsTreeModel } from './prsTreeModel'; +import { PullRequestModel } from '../github/pullRequestModel'; +import { NotificationsManager } from '../notifications/notificationsManager'; export class PRStatusDecorationProvider extends Disposable implements vscode.FileDecorationProvider { @@ -16,7 +22,7 @@ export class PRStatusDecorationProvider extends Disposable implements vscode.Fil >(); onDidChangeFileDecorations: vscode.Event = this._onDidChangeFileDecorations.event; - constructor(private readonly _prsTreeModel: PrsTreeModel) { + constructor(private readonly _prsTreeModel: PrsTreeModel, private readonly _copilotManager: CopilotRemoteAgentManager, private readonly _notificationProvider: NotificationsManager) { super(); this._register(vscode.window.registerFileDecorationProvider(this)); this._register( @@ -24,12 +30,61 @@ export class PRStatusDecorationProvider extends Disposable implements vscode.Fil this._onDidChangeFileDecorations.fire(identifiers.map(id => createPRNodeUri(id))); }) ); + + this._register(this._copilotManager.onDidChangeNotifications(items => { + const repoItems = new Set(); + const uris: vscode.Uri[] = []; + for (const item of items) { + const queryUri = toQueryUri({ remote: { owner: item.remote.owner, repositoryName: item.remote.repositoryName }, isCopilot: true }); + if (!repoItems.has(queryUri.toString())) { + repoItems.add(queryUri.toString()); + uris.push(queryUri); + } + uris.push(createPRNodeUri(item, true)); + } + this._onDidChangeFileDecorations.fire(uris); + })); + + const addUriForRefresh = (uris: vscode.Uri[], pullRequest: unknown) => { + if (pullRequest instanceof PullRequestModel) { + uris.push(createPRNodeUri(pullRequest)); + if (pullRequest.timelineEvents?.some(t => t.event === EventType.CopilotStarted)) { + // The pr nodes in the Copilot category have a different uri so we need to refresh those too + uris.push(createPRNodeUri(pullRequest, true)); + } + } + }; + + this._register( + this._notificationProvider.onDidChangeNotifications(notifications => { + let uris: vscode.Uri[] = []; + for (const notification of notifications) { + addUriForRefresh(uris, notification.model); + } + this._onDidChangeFileDecorations.fire(uris); + }) + ); + + // if the notification setting changes, refresh the decorations for the nodes with notifications + this._register(vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(`${PR_SETTINGS_NAMESPACE}.${NOTIFICATION_SETTING}`)) { + const uris: vscode.Uri[] = []; + for (const pr of this._notificationProvider.getAllNotifications()) { + addUriForRefresh(uris, pr.model); + } + this._onDidChangeFileDecorations.fire(uris); + } + })); } provideFileDecoration( uri: vscode.Uri, _token: vscode.CancellationToken, ): vscode.ProviderResult { + if (uri.scheme === Schemes.PRQuery) { + return this._queryDecoration(uri); + } + if (uri.scheme !== Schemes.PRNode) { return; } @@ -37,11 +92,80 @@ export class PRStatusDecorationProvider extends Disposable implements vscode.Fil if (!params) { return; } + + const copilotDecoration = this._getCopilotDecoration(params); + if (copilotDecoration) { + return copilotDecoration; + } + + const notificationDecoration = this._getNotificationDecoration(params); + if (notificationDecoration) { + return notificationDecoration; + } + const status = this._prsTreeModel.cachedPRStatus(params.prIdentifier); if (!status) { return; } - return getStatusDecoration(status.status) as vscode.FileDecoration; + const decoration = getStatusDecoration(status.status) as vscode.FileDecoration; + return decoration; + } + + private _getCopilotDecoration(params: PRNodeUriParams): vscode.FileDecoration | undefined { + if (!params.showCopilot) { + return; + } + const idParts = parsePRNodeIdentifier(params.prIdentifier); + if (!idParts) { + return; + } + const protocol = new Protocol(idParts.remote); + if (this._prsTreeModel.hasCopilotNotification(protocol.owner, protocol.repositoryName, idParts.prNumber)) { + return { + badge: new vscode.ThemeIcon('copilot') as unknown as string, + color: new vscode.ThemeColor('pullRequests.notification') + }; + } + } + + private _queryDecoration(uri: vscode.Uri): vscode.ProviderResult { + const params = fromQueryUri(uri); + if (!params?.isCopilot || !params.remote) { + return; + } + const counts = this._prsTreeModel.getCopilotNotificationsCount(params.remote.owner, params.remote.repositoryName); + if (counts === 0) { + return; + } + + return { + tooltip: vscode.l10n.t('Coding agent has made changes'), + badge: new vscode.ThemeIcon('copilot') as unknown as string, + color: new vscode.ThemeColor('pullRequests.notification'), + }; + } + + private _getNotificationDecoration(params: PRNodeUriParams): vscode.FileDecoration | undefined { + if (!this.notificationSettingValue()) { + return; + } + const idParts = parsePRNodeIdentifier(params.prIdentifier); + if (!idParts) { + return; + } + const protocol = new Protocol(idParts.remote); + if (this._notificationProvider.hasNotification({ owner: protocol.owner, repo: protocol.repositoryName, number: idParts.prNumber })) { + return { + propagate: false, + color: new vscode.ThemeColor('pullRequests.notification'), + badge: '●', + tooltip: 'unread notification' + }; + } + } + + private notificationSettingValue(): boolean { + return vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(NOTIFICATION_SETTING, 'off') === 'pullRequests'; } } \ No newline at end of file diff --git a/src/view/prsTreeDataProvider.ts b/src/view/prsTreeDataProvider.ts index 7e2773cce7..e433636fca 100644 --- a/src/view/prsTreeDataProvider.ts +++ b/src/view/prsTreeDataProvider.ts @@ -4,33 +4,36 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { PRStatusDecorationProvider } from './prStatusDecorationProvider'; +import { PrsTreeModel } from './prsTreeModel'; +import { ReviewModel } from './reviewModel'; import { AuthProvider } from '../common/authentication'; import { commands, contexts } from '../common/executeCommands'; import { Disposable } from '../common/lifecycle'; +import Logger from '../common/logger'; import { FILE_LIST_LAYOUT, PR_SETTINGS_NAMESPACE, QUERIES, REMOTES } from '../common/settingKeys'; import { ITelemetry } from '../common/telemetry'; import { createPRNodeIdentifier } from '../common/uri'; import { EXTENSION_ID } from '../constants'; -import { CredentialStore } from '../github/credentials'; +import { CopilotRemoteAgentManager } from '../github/copilotRemoteAgent'; import { FolderRepositoryManager, ReposManagerState } from '../github/folderRepositoryManager'; +import { PullRequestChangeEvent } from '../github/githubRepository'; import { PRType } from '../github/interface'; import { issueMarkdown } from '../github/markdownUtils'; -import { NotificationProvider } from '../github/notifications'; import { PullRequestModel } from '../github/pullRequestModel'; +import { PullRequestOverviewPanel } from '../github/pullRequestOverview'; import { RepositoriesManager } from '../github/repositoriesManager'; import { findDotComAndEnterpriseRemotes } from '../github/utils'; -import { PRStatusDecorationProvider } from './prStatusDecorationProvider'; -import { PrsTreeModel } from './prsTreeModel'; -import { ReviewModel } from './reviewModel'; import { CategoryTreeNode, PRCategoryActionNode, PRCategoryActionType } from './treeNodes/categoryNode'; import { InMemFileChangeNode } from './treeNodes/fileChangeNode'; import { PRNode } from './treeNodes/pullRequestNode'; import { BaseTreeNode, TreeNode } from './treeNodes/treeNode'; import { TreeUtils } from './treeNodes/treeUtils'; import { WorkspaceFolderNode } from './treeNodes/workspaceFolderNode'; +import { NotificationsManager } from '../notifications/notificationsManager'; export class PullRequestsTreeDataProvider extends Disposable implements vscode.TreeDataProvider, BaseTreeNode { - private _onDidChangeTreeData = new vscode.EventEmitter(); + private _onDidChangeTreeData = new vscode.EventEmitter(); readonly onDidChangeTreeData = this._onDidChangeTreeData.event; private _onDidChange = new vscode.EventEmitter(); get onDidChange(): vscode.Event { @@ -40,27 +43,34 @@ export class PullRequestsTreeDataProvider extends Disposable implements vscode.T get children() { return this._children; } - private _view: vscode.TreeView; + private readonly _view: vscode.TreeView; private _initialized: boolean = false; - public notificationProvider: NotificationProvider; - public readonly prsTreeModel: PrsTreeModel; + private _notificationsProvider?: NotificationsManager; + private _notificationClearTimeout: NodeJS.Timeout | undefined; get view(): vscode.TreeView { return this._view; } - constructor(private readonly _telemetry: ITelemetry, private readonly _context: vscode.ExtensionContext, private readonly _reposManager: RepositoriesManager) { + constructor(public readonly prsTreeModel: PrsTreeModel, private readonly _telemetry: ITelemetry, private readonly _context: vscode.ExtensionContext, private readonly _reposManager: RepositoriesManager, private readonly _copilotManager: CopilotRemoteAgentManager) { super(); - this.prsTreeModel = this._register(new PrsTreeModel(this._telemetry, this._reposManager, _context)); - this._register(this.prsTreeModel.onDidChangeData(folderManager => folderManager ? this.refreshRepo(folderManager) : this.refresh())); - this._register(new PRStatusDecorationProvider(this.prsTreeModel)); + this._register(this.prsTreeModel.onDidChangeData(e => { + if (e instanceof FolderRepositoryManager) { + this.refreshRepo(e); + } else if (Array.isArray(e)) { + this.refreshPullRequests(e); + } else { + this.refreshAllQueryResults(true); + } + })); this._register(vscode.commands.registerCommand('pr.refreshList', _ => { - this.refresh(undefined, true); + this.prsTreeModel.forceClearCache(); + this.refreshAllQueryResults(true); })); this._register(vscode.commands.registerCommand('pr.loadMore', (node: CategoryTreeNode) => { node.fetchNextPage = true; - this._onDidChangeTreeData.fire(node); + this.refresh(node); })); this._view = this._register(vscode.window.createTreeView('pr:github', { @@ -68,25 +78,69 @@ export class PullRequestsTreeDataProvider extends Disposable implements vscode.T showCollapseAll: true, })); + this._register(this._view.onDidChangeVisibility(e => { + if (e.visible) { + // Sync with currently active PR when view becomes visible + const currentPR = PullRequestOverviewPanel.getCurrentPullRequest(); + if (currentPR) { + this.syncWithActivePullRequest(currentPR); + } + } + })); + + this._register({ + dispose: () => { + if (this._notificationClearTimeout) { + clearTimeout(this._notificationClearTimeout); + this._notificationClearTimeout = undefined; + } + } + }); + + this._register(this._copilotManager.onDidChangeStates(() => { + this.refreshAllQueryResults(); + })); + + this._register(this._copilotManager.onDidChangeNotifications(() => { + this.updateBadge(); + })); + this.updateBadge(); + + this._register(this._copilotManager.onDidCreatePullRequest(() => this.refreshAllQueryResults(true))); + + // Listen for PR overview panel changes to sync the tree view + this._register(PullRequestOverviewPanel.onVisible(pullRequest => { + // Only sync if view is already visible (don't open the view) + if (this._view.visible) { + this.syncWithActivePullRequest(pullRequest); + } + })); + this._children = []; this._register(vscode.commands.registerCommand('pr.configurePRViewlet', async () => { const configuration = await vscode.window.showQuickPick([ 'Configure Remotes...', - 'Configure Queries...' + 'Configure Queries...', + 'Configure All Pull Request Settings...' ]); switch (configuration) { case 'Configure Queries...': return vscode.commands.executeCommand( 'workbench.action.openSettings', - `@ext:${EXTENSION_ID} queries`, + `@ext:${EXTENSION_ID} pull request queries`, ); case 'Configure Remotes...': return vscode.commands.executeCommand( 'workbench.action.openSettings', `@ext:${EXTENSION_ID} remotes`, ); + case 'Configure All Pull Request Settings...': + return vscode.commands.executeCommand( + 'workbench.action.openSettings', + `@ext:${EXTENSION_ID} pull request`, + ); default: return; } @@ -94,7 +148,7 @@ export class PullRequestsTreeDataProvider extends Disposable implements vscode.T this._register(vscode.workspace.onDidChangeConfiguration(e => { if (e.affectsConfiguration(`${PR_SETTINGS_NAMESPACE}.${FILE_LIST_LAYOUT}`)) { - this._onDidChangeTreeData.fire(); + this.refreshAll(); } })); @@ -108,6 +162,80 @@ export class PullRequestsTreeDataProvider extends Disposable implements vscode.T })); } + private filterNotificationsToKnown(notifications: PullRequestModel[]): PullRequestModel[] { + return notifications.filter(notification => { + if (!this.prsTreeModel.hasPullRequest(notification)) { + return false; + } + return !this.prsTreeModel.hasCopilotNotification(notification.remote.owner, notification.remote.repositoryName, notification.number); + }); + } + + private updateBadge() { + const isPRNotificationsOn = this._notificationsProvider?.isPRNotificationsOn(); + + const prNotificationsCount = isPRNotificationsOn ? this.filterNotificationsToKnown(this._notificationsProvider!.prNotifications).length : 0; + const copilotCount = this.prsTreeModel.copilotNotificationsCount; + const totalCount = prNotificationsCount + copilotCount; + + if (totalCount === 0) { + this._view.badge = undefined; + return; + } + + if (prNotificationsCount > 0 && copilotCount > 0) { + if (copilotCount === 1) { + if (prNotificationsCount === 1) { + this._view.badge = { + tooltip: vscode.l10n.t('Coding agent has 1 pull request to view, plus 1 other pull request notification'), + value: totalCount + }; + } else { + this._view.badge = { + tooltip: vscode.l10n.t(`Coding agent has 1 pull request to view, plus {0} other pull request notifications`, prNotificationsCount), + value: totalCount + }; + } + } else { + if (prNotificationsCount === 1) { + this._view.badge = { + tooltip: vscode.l10n.t('Coding agent has {0} pull requests to view, plus 1 other pull request notification', copilotCount), + value: totalCount + }; + } else { + this._view.badge = { + tooltip: vscode.l10n.t(`Coding agent has {0} pull requests to view, plus {1} other pull request notifications`, copilotCount, prNotificationsCount), + value: totalCount + }; + } + } + } else if (copilotCount > 0) { + if (copilotCount === 1) { + this._view.badge = { + tooltip: vscode.l10n.t(`Coding agent has 1 pull request to view`), + value: totalCount + }; + } else { + this._view.badge = { + tooltip: vscode.l10n.t(`Coding agent has {0} pull requests to view`, copilotCount), + value: totalCount + }; + } + } else if (prNotificationsCount > 0) { + if (prNotificationsCount === 1) { + this._view.badge = { + tooltip: vscode.l10n.t(`1 pull request notification`), + value: totalCount + }; + } else { + this._view.badge = { + tooltip: vscode.l10n.t(`{0} pull request notifications`, prNotificationsCount), + value: totalCount + }; + } + } + } + public async expandPullRequest(pullRequest: PullRequestModel) { if (this._children.length === 0) { await this.getChildren(); @@ -129,57 +257,239 @@ export class PullRequestsTreeDataProvider extends Disposable implements vscode.T return this._view.reveal(element, options); } - initialize(reviewModels: ReviewModel[], credentialStore: CredentialStore) { + /** + * Sync the tree view with the currently active PR overview + */ + private async syncWithActivePullRequest(pullRequest: PullRequestModel): Promise { + const alreadySelected = this._view.selection.find(child => child instanceof PRNode && (child.pullRequestModel.number === pullRequest.number) && (child.pullRequestModel.remote.owner === pullRequest.remote.owner) && (child.pullRequestModel.remote.repositoryName === pullRequest.remote.repositoryName)); + if (alreadySelected) { + return; + } + try { + // Find the PR node in the tree and reveal it + const prNode = await this.findPRNode(pullRequest); + if (prNode) { + await this.reveal(prNode, { select: true, focus: false, expand: false }); + } + } catch (error) { + // Silently ignore errors to avoid disrupting the user experience + Logger.warn(`Failed to sync tree view with active PR: ${error}`); + } + } + + /** + * Find a PR node in the tree structure + */ + private async findPRNode(pullRequest: PullRequestModel): Promise { + if (this._children.length === 0) { + await this.getChildren(); + } + + for (const child of this._children) { + if (child instanceof WorkspaceFolderNode) { + const found = await this.findPRNodeInWorkspaceFolder(child, pullRequest); + if (found) return found; + } else if (child instanceof CategoryTreeNode) { + const found = await this.findPRNodeInCategory(child, pullRequest); + if (found) return found; + } + } + return undefined; + } + + /** + * Search for PR node within a workspace folder node + */ + private async findPRNodeInWorkspaceFolder(workspaceNode: WorkspaceFolderNode, pullRequest: PullRequestModel): Promise { + const children = await workspaceNode.getChildren(false); + for (const child of children) { + if (child instanceof CategoryTreeNode) { + const found = await this.findPRNodeInCategory(child, pullRequest); + if (found) return found; + } + } + return undefined; + } + + /** + * Search for PR node within a category node + */ + private async findPRNodeInCategory(categoryNode: CategoryTreeNode, pullRequest: PullRequestModel): Promise { + if (categoryNode.collapsibleState !== vscode.TreeItemCollapsibleState.Expanded) { + return; + } + const children = await categoryNode.getChildren(false); + for (const child of children) { + if (child instanceof PRNode && (child.pullRequestModel.number === pullRequest.number) && (child.pullRequestModel.remote.owner === pullRequest.remote.owner) && (child.pullRequestModel.remote.repositoryName === pullRequest.remote.repositoryName)) { + return child; + } + } + return undefined; + } + + initialize(reviewModels: ReviewModel[], notificationsManager: NotificationsManager) { if (this._initialized) { throw new Error('Tree has already been initialized!'); } this._initialized = true; - this._register( - this._reposManager.onDidChangeState(() => { - this.refresh(); - }), - ); + this._register(this._reposManager.onDidChangeState(() => { + this.refreshAll(); + })); + this._register(this._reposManager.onDidLoadAnyRepositories(() => { + this.refreshAll(); + })); for (const model of reviewModels) { - this._register(model.onDidChangeLocalFileChanges(_ => { this.refresh(); })); + this._register(model.onDidChangeLocalFileChanges(_ => { this.refreshAllQueryResults(); })); } - this.notificationProvider = this._register(new NotificationProvider(this, credentialStore, this._reposManager)); + this._notificationsProvider = notificationsManager; + this._register(this._notificationsProvider.onDidChangeNotifications(() => { + this.updateBadge(); + })); + this._register(new PRStatusDecorationProvider(this.prsTreeModel, this._copilotManager, this._notificationsProvider)); this.initializeCategories(); - this.refresh(); + this.refreshAll(); } private async initializeCategories() { this._register(vscode.workspace.onDidChangeConfiguration(async e => { if (e.affectsConfiguration(`${PR_SETTINGS_NAMESPACE}.${QUERIES}`)) { - this.refresh(); + this.refreshAll(); } })); } - refresh(node?: TreeNode, reset?: boolean): void { + refreshAll(reset?: boolean) { + this.tryReset(!!reset); + this._onDidChangeTreeData.fire(); + } + + private tryReset(reset: boolean) { if (reset) { - this.prsTreeModel.clearCache(); + this.prsTreeModel.clearCache(true); + } + } + + private refreshAllQueryResults(reset?: boolean) { + this.tryReset(!!reset); + + if (!this._children || this._children.length === 0) { + this._onDidChangeTreeData.fire(); + return; + } + + if (this._children[0] instanceof WorkspaceFolderNode) { + (this._children as WorkspaceFolderNode[]).forEach(folderNode => this.refreshQueryResultsForFolder(folderNode)); + return; } - return node ? this._onDidChangeTreeData.fire(node) : this._onDidChangeTreeData.fire(); + this.refreshQueryResultsForFolder(); + } + + private refreshQueryResultsForFolder(manager?: WorkspaceFolderNode, reset?: boolean) { + if (!manager && this._children[0] instanceof WorkspaceFolderNode) { + // Not permitted. There're multiple folder nodes, therefore must specify which one to refresh + throw new Error('Must specify a folder node to refresh when there are multiple folder nodes'); + } + + if (!this._children || this._children.length === 0) { + this._onDidChangeTreeData.fire(); + return; + } + const queries = manager?.children ?? this._children; + this.tryReset(!!reset); + + this._onDidChangeTreeData.fire([...queries]); + } + + refresh(node: TreeNode, reset?: boolean): void { + this.tryReset(!!reset); + return this._onDidChangeTreeData.fire(node); } private refreshRepo(manager: FolderRepositoryManager): void { - if (this._children.length === 0) { - return this.refresh(); + if ((this._children.length === 0) || (this._children[0] instanceof CategoryTreeNode && this._children[0].folderRepoManager === manager)) { + return this.refreshQueryResultsForFolder(undefined, true); } if (this._children[0] instanceof WorkspaceFolderNode) { const children: WorkspaceFolderNode[] = this._children as WorkspaceFolderNode[]; const node = children.find(node => node.folderManager === manager); if (node) { - this._onDidChangeTreeData.fire(node); + this.refreshQueryResultsForFolder(node); return; } } } + private refreshPullRequests(pullRequests: PullRequestChangeEvent[]): void { + if (!this._children?.length || !pullRequests?.length) { + return; + } + const prNodesToRefresh: TreeNode[] = []; + const prsWithStateChange = new Set(); + const prNumbers = new Set(); + + for (const prChange of pullRequests) { + prNumbers.add(prChange.model.number); + if (prChange.event.state) { + prsWithStateChange.add(prChange.model.number); + } + } + + const hasPRNode = (node: TreeNode) => { + const prNodes = node.children ?? []; + for (const prNode of prNodes) { + if (prNode instanceof PRNode && prsWithStateChange.has(prNode.pullRequestModel.number)) { + return true; + } + } + return false; + }; + + const categoriesToRefresh: Set = new Set(); + // First find the categories to refresh, since if we refresh a category we don't need to specifically refresh its children + for (const child of this._children) { + if (child instanceof WorkspaceFolderNode) { + const categories = child.children ?? []; + for (const category of categories) { + if (category instanceof CategoryTreeNode && !categoriesToRefresh.has(category) && hasPRNode(category)) { + categoriesToRefresh.add(category); + } + } + } else if (child instanceof CategoryTreeNode && !categoriesToRefresh.has(child) && hasPRNode(child)) { + categoriesToRefresh.add(child); + } + } + + // Yes, multiple PRs can exist in different repos with the same number, but at worst we'll refresh all the duplicate numbers, which shouldn't be many. + const collectPRNodes = (node: TreeNode) => { + const prNodes = node.children ?? []; + for (const prNode of prNodes) { + if (prNode instanceof PRNode && prNumbers.has(prNode.pullRequestModel.number)) { + prNodesToRefresh.push(prNode); + } + } + }; + + for (const child of this._children) { + if (child instanceof WorkspaceFolderNode) { + const categories = child.children ?? []; + for (const category of categories) { + if (category instanceof CategoryTreeNode && !categoriesToRefresh.has(category)) { + collectPRNodes(category); + } + } + } else if (child instanceof CategoryTreeNode && !categoriesToRefresh.has(child)) { + collectPRNodes(child); + } + } + if (prNodesToRefresh.length || categoriesToRefresh.size > 0) { + this._onDidChangeTreeData.fire([...Array.from(categoriesToRefresh), ...prNodesToRefresh]); + } + } + getTreeItem(element: TreeNode): vscode.TreeItem | Promise { return element.getTreeItem(); } @@ -249,13 +559,14 @@ export class PullRequestsTreeDataProvider extends Disposable implements vscode.T let result: WorkspaceFolderNode[] | CategoryTreeNode[]; if (gitHubFolderManagers.length === 1) { - result = WorkspaceFolderNode.getCategoryTreeNodes( + result = await WorkspaceFolderNode.getCategoryTreeNodes( gitHubFolderManagers[0], this._telemetry, this, - this.notificationProvider, + this._notificationsProvider!, this._context, - this.prsTreeModel + this.prsTreeModel, + this._copilotManager, ); } else { result = gitHubFolderManagers.map( @@ -265,9 +576,10 @@ export class PullRequestsTreeDataProvider extends Disposable implements vscode.T folderManager.repository.rootUri, folderManager, this._telemetry, - this.notificationProvider, + this._notificationsProvider!, this._context, - this.prsTreeModel + this.prsTreeModel, + this._copilotManager ), ); } diff --git a/src/view/prsTreeModel.ts b/src/view/prsTreeModel.ts index 99b1c2dbaa..8d25580ccb 100644 --- a/src/view/prsTreeModel.ts +++ b/src/view/prsTreeModel.ts @@ -4,32 +4,46 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { RemoteInfo } from '../../common/types'; +import { COPILOT_ACCOUNTS } from '../common/comment'; +import { copilotEventToStatus, CopilotPRStatus } from '../common/copilot'; import { Disposable, disposeAll } from '../common/lifecycle'; +import Logger from '../common/logger'; import { getReviewMode } from '../common/settingsUtils'; import { ITelemetry } from '../common/telemetry'; import { createPRNodeIdentifier } from '../common/uri'; import { FolderRepositoryManager, ItemsResponseResult } from '../github/folderRepositoryManager'; +import { PullRequestChangeEvent } from '../github/githubRepository'; import { CheckState, PRType, PullRequestChecks, PullRequestReviewRequirement } from '../github/interface'; import { PullRequestModel } from '../github/pullRequestModel'; import { RepositoriesManager } from '../github/repositoriesManager'; -import { UnsatisfiedChecks } from '../github/utils'; +import { extractRepoFromQuery, UnsatisfiedChecks } from '../github/utils'; import { CategoryTreeNode } from './treeNodes/categoryNode'; import { TreeNode } from './treeNodes/treeNode'; +import { CodingAgentPRAndStatus, CopilotStateModel, getCopilotQuery } from '../github/copilotPrWatcher'; export const EXPANDED_QUERIES_STATE = 'expandedQueries'; -interface PRStatusChange { +export interface PRStatusChange { pullRequest: PullRequestModel; status: UnsatisfiedChecks; } +interface CachedPRs { + clearRequested: boolean; + maxKnownPR: number | undefined; // used to determine if there have been new PRs created since last query + items: ItemsResponseResult; +} + export class PrsTreeModel extends Disposable { + private static readonly ID = 'PrsTreeModel'; + private _activePRDisposables: Map = new Map(); private readonly _onDidChangePrStatus: vscode.EventEmitter = this._register(new vscode.EventEmitter()); public readonly onDidChangePrStatus = this._onDidChangePrStatus.event; - private readonly _onDidChangeData: vscode.EventEmitter = this._register(new vscode.EventEmitter()); + private readonly _onDidChangeData: vscode.EventEmitter = this._register(new vscode.EventEmitter()); public readonly onDidChangeData = this._onDidChangeData.event; - private _expandedQueries: Set = new Set(); + private _expandedQueries: Set | undefined; private _hasLoaded: boolean = false; private _onLoaded: vscode.EventEmitter = this._register(new vscode.EventEmitter()); public readonly onLoaded = this._onLoaded.event; @@ -37,30 +51,82 @@ export class PrsTreeModel extends Disposable { // Key is identifier from createPRNodeUri private readonly _queriedPullRequests: Map = new Map(); - private _cachedPRs: Map>> = new Map(); + private _cachedPRs: Map> = new Map(); + // For ease of finding which PRs we know about + private _allCachedPRs: Set = new Set(); + + private readonly _repoEvents: Map = new Map(); + private _getPullRequestsForQueryLock: Promise = Promise.resolve(); + private _sentNoRepoTelemetry: boolean = false; + + public readonly copilotStateModel: CopilotStateModel; + private readonly _onDidChangeCopilotStates = this._register(new vscode.EventEmitter()); + readonly onDidChangeCopilotStates = this._onDidChangeCopilotStates.event; + private readonly _onDidChangeCopilotNotifications = this._register(new vscode.EventEmitter()); + readonly onDidChangeCopilotNotifications = this._onDidChangeCopilotNotifications.event; constructor(private _telemetry: ITelemetry, private readonly _reposManager: RepositoriesManager, private readonly _context: vscode.ExtensionContext) { super(); + this.copilotStateModel = new CopilotStateModel(); + this._register(this.copilotStateModel.onDidChangeCopilotStates(() => this._onDidChangeCopilotStates.fire())); + this._register(this.copilotStateModel.onDidChangeCopilotNotifications((prs) => this._onDidChangeCopilotNotifications.fire(prs))); + const repoEvents = (manager: FolderRepositoryManager) => { - this._register(manager.onDidChangeActivePullRequest(() => { - this.clearRepo(manager); + if (this._repoEvents.has(manager)) { + disposeAll(this._repoEvents.get(manager)!); + } else { + this._repoEvents.set(manager, []); + } + + this._repoEvents.get(manager)!.push(manager.onDidChangeActivePullRequest(e => { + const prs: PullRequestChangeEvent[] = []; + if (e.old) { + prs.push({ model: e.old, event: {} }); + } + if (e.new) { + prs.push({ model: e.new, event: {} }); + } + this._onDidChangeData.fire(prs); + if (this._activePRDisposables.has(manager)) { disposeAll(this._activePRDisposables.get(manager)!); this._activePRDisposables.delete(manager); } if (manager.activePullRequest) { this._activePRDisposables.set(manager, [ - manager.activePullRequest.onDidChangeComments(() => { - this.clearRepo(manager); + manager.activePullRequest.onDidChange(e => { + if (e.comments && manager.activePullRequest) { + this._onDidChangeData.fire([{ model: manager.activePullRequest, event: e }]); + } })]); } })); }; - + this._register({ dispose: () => this._repoEvents.forEach((disposables) => disposeAll(disposables)) }); for (const manager of this._reposManager.folderManagers) { repoEvents(manager); } + + this._register(this._reposManager.onDidChangeAnyPullRequests((prs) => { + const stateChanged: PullRequestChangeEvent[] = []; + const needsRefresh: PullRequestChangeEvent[] = []; + for (const pr of prs) { + if (pr.event.state) { + stateChanged.push(pr); + } + needsRefresh.push(pr); + } + this.forceClearQueriesContainingPullRequests(stateChanged); + this._onDidChangeData.fire(needsRefresh); + })); + + this._register(this._reposManager.onDidAddPullRequest(() => { + if (this._hasLoaded) { + this._onDidChangeData.fire(); + } + })); + this._register(this._reposManager.onDidChangeFolderRepositories((changed) => { if (changed.added) { repoEvents(changed.added); @@ -68,10 +134,20 @@ export class PrsTreeModel extends Disposable { } })); - this._expandedQueries = new Set(this._context.workspaceState.get(EXPANDED_QUERIES_STATE, [] as string[])); + this._register(this._reposManager.onDidChangeAnyGitHubRepository((folderManager) => { + this._onDidChangeData.fire(folderManager); + })); + + const expandedQueries = this._context.workspaceState.get(EXPANDED_QUERIES_STATE, undefined); + if (expandedQueries) { + this._expandedQueries = new Set(expandedQueries); + } } public updateExpandedQueries(element: TreeNode, isExpanded: boolean) { + if (!this._expandedQueries) { + this._expandedQueries = new Set(); + } if ((element instanceof CategoryTreeNode) && element.id) { if (isExpanded) { this._expandedQueries.add(element.id); @@ -82,8 +158,8 @@ export class PrsTreeModel extends Disposable { } } - get expandedQueries(): Set { - if (this._reposManager.folderManagers.length > 3 && this._expandedQueries.size > 0) { + get expandedQueries(): Set | undefined { + if (this._reposManager.folderManagers.length > 3 && this._expandedQueries && this._expandedQueries.size > 0) { return new Set(); } return this._expandedQueries; @@ -102,14 +178,43 @@ export class PrsTreeModel extends Disposable { return this._queriedPullRequests.get(identifier); } - public clearCache() { + public forceClearCache() { this._cachedPRs.clear(); + this._allCachedPRs.clear(); this._onDidChangeData.fire(); } - public clearRepo(folderRepoManager: FolderRepositoryManager) { - this._cachedPRs.delete(folderRepoManager); - this._onDidChangeData.fire(folderRepoManager); + public hasPullRequest(pr: PullRequestModel): boolean { + return this._allCachedPRs.has(pr); + } + + public clearCache(silent: boolean = false) { + if (this._cachedPRs.size === 0) { + return; + } + + // Instead of clearing the entire cache, mark each cached query as requiring refresh. + for (const queries of this._cachedPRs.values()) { + for (const [, cachedPRs] of queries.entries()) { + if (cachedPRs) { + cachedPRs.clearRequested = true; + } + } + } + + if (!silent) { + this._onDidChangeData.fire(); + } + } + + private _clearOneCache(folderRepoManager: FolderRepositoryManager, query: string | PRType.LocalPullRequest | PRType.All) { + const cache = this.getFolderCache(folderRepoManager); + if (cache.has(query)) { + const cachedForQuery = cache.get(query); + if (cachedForQuery) { + cachedForQuery.clearRequested = true; + } + } } private async _getChecks(pullRequests: PullRequestModel[]) { @@ -164,7 +269,7 @@ export class PrsTreeModel extends Disposable { this._onDidChangePrStatus.fire(changedStatuses); } - private getFolderCache(folderRepoManager: FolderRepositoryManager): Map> { + private getFolderCache(folderRepoManager: FolderRepositoryManager): Map { let cache = this._cachedPRs.get(folderRepoManager); if (!cache) { cache = new Map(); @@ -173,17 +278,23 @@ export class PrsTreeModel extends Disposable { return cache; } - async getLocalPullRequests(folderRepoManager: FolderRepositoryManager, update?: boolean) { + async getLocalPullRequests(folderRepoManager: FolderRepositoryManager, update?: boolean): Promise> { const cache = this.getFolderCache(folderRepoManager); if (!update && cache.has(PRType.LocalPullRequest)) { - return cache.get(PRType.LocalPullRequest)!; + return cache.get(PRType.LocalPullRequest)!.items; } const useReviewConfiguration = getReviewMode(); const prs = (await folderRepoManager.getLocalPullRequests()) .filter(pr => pr.isOpen || (pr.isClosed && useReviewConfiguration.closed) || (pr.isMerged && useReviewConfiguration.merged)); - cache.set(PRType.LocalPullRequest, { hasMorePages: false, hasUnsearchedRepositories: false, items: prs, totalCount: prs.length }); + const toCache: CachedPRs = { + clearRequested: false, + maxKnownPR: undefined, + items: { hasMorePages: false, hasUnsearchedRepositories: false, items: prs, totalCount: prs.length } + }; + cache.set(PRType.LocalPullRequest, toCache); + prs.forEach(pr => this._allCachedPRs.add(pr)); /* __GDPR__ "pr.expand.local" : {} @@ -195,40 +306,112 @@ export class PrsTreeModel extends Disposable { return { hasMorePages: false, hasUnsearchedRepositories: false, items: prs }; } - async getPullRequestsForQuery(folderRepoManager: FolderRepositoryManager, fetchNextPage: boolean, query: string): Promise> { - const cache = this.getFolderCache(folderRepoManager); - if (!fetchNextPage && cache.has(query)) { - return cache.get(query)!; + private async _testIfRefreshNeeded(cached: CachedPRs, query: string, folderManager: FolderRepositoryManager): Promise { + if (!cached.clearRequested) { + return false; } - const prs = await folderRepoManager.getPullRequests( - PRType.Query, - { fetchNextPage }, - query, - ); - cache.set(query, prs); + const repoInfo = await extractRepoFromQuery(folderManager, query); + if (!repoInfo) { + // Query doesn't specify a repo or org, so always refresh + // Send telemetry once indicating we couldn't find a repo in the query. + if (!this._sentNoRepoTelemetry) { + /* __GDPR__ + "pr.expand.noRepo" : {} + */ + this._telemetry.sendTelemetryEvent('pr.expand.noRepo'); + this._sentNoRepoTelemetry = true; + } + return true; + } - /* __GDPR__ - "pr.expand.query" : {} - */ - this._telemetry.sendTelemetryEvent('pr.expand.query'); - // Don't await this._getChecks. It fires an event that will be listened to. - this._getChecks(prs.items); - this.hasLoaded = true; - return prs; + const currentMax = await this._getMaxKnownPR(repoInfo); + if (currentMax !== cached.maxKnownPR) { + cached.maxKnownPR = currentMax; + return true; + } + return false; + } + + private async _getMaxKnownPR(repoInfo: RemoteInfo): Promise { + const manager = this._reposManager.getManagerForRepository(repoInfo.owner, repoInfo.repositoryName); + if (!manager) { + return; + } + const repo = manager.findExistingGitHubRepository({ owner: repoInfo.owner, repositoryName: repoInfo.repositoryName }); + if (!repo) { + return; + } + return repo.getMaxPullRequest(); + } + + async getPullRequestsForQuery(folderRepoManager: FolderRepositoryManager, fetchNextPage: boolean, query: string, fetchOnePagePerRepo: boolean = false): Promise> { + let release: () => void; + const lock = new Promise(resolve => { release = resolve; }); + const prev = this._getPullRequestsForQueryLock; + this._getPullRequestsForQueryLock = prev.then(() => lock); + await prev; + + 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); + maxKnownPR = cachedPRs.maxKnownPR; + if (!shouldRefresh) { + cachedPRs.clearRequested = false; + return cachedPRs.items; + } + } + + if (!maxKnownPR) { + const repoInfo = await extractRepoFromQuery(folderRepoManager, query); + if (repoInfo) { + maxKnownPR = await this._getMaxKnownPR(repoInfo); + } + } + + const prs = await folderRepoManager.getPullRequests( + PRType.Query, + { 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)); + + /* __GDPR__ + "pr.expand.query" : {} + */ + this._telemetry.sendTelemetryEvent('pr.expand.query'); + // Don't await this._getChecks. It fires an event that will be listened to. + this._getChecks(prs.items); + this.hasLoaded = true; + return prs; + } finally { + release!(); + } } async getAllPullRequests(folderRepoManager: FolderRepositoryManager, fetchNextPage: boolean, update?: boolean): Promise> { const cache = this.getFolderCache(folderRepoManager); - if (!update && cache.has(PRType.All) && !fetchNextPage) { - return cache.get(PRType.All)!; + const allCache = cache.get(PRType.All); + if (!update && allCache && !allCache.clearRequested && !fetchNextPage) { + return allCache.items; } const prs = await folderRepoManager.getPullRequests( PRType.All, { fetchNextPage } ); - cache.set(PRType.All, prs); + 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)); /* __GDPR__ "pr.expand.all" : {} @@ -240,6 +423,156 @@ export class PrsTreeModel extends Disposable { return prs; } + private forceClearQueriesContainingPullRequests(pullRequests: PullRequestChangeEvent[]): void { + const withStateChange = pullRequests.filter(prChange => prChange.event.state); + if (!withStateChange || withStateChange.length === 0) { + return; + } + for (const [, queries] of this._cachedPRs.entries()) { + for (const [queryKey, cachedPRs] of queries.entries()) { + if (!cachedPRs || !cachedPRs.items.items || cachedPRs.items.items.length === 0) { + continue; + } + const hasPR = withStateChange.some(prChange => + cachedPRs.items.items.some(item => item === prChange.model) + ); + if (hasPR) { + const cachedForQuery = queries.get(queryKey); + if (cachedForQuery) { + cachedForQuery.items.items.forEach(item => this._allCachedPRs.delete(item)); + } + queries.delete(queryKey); + } + } + } + } + + getCopilotNotificationsCount(owner: string, repo: string): number { + return this.copilotStateModel.getNotificationsCount(owner, repo); + } + + get copilotNotificationsCount(): number { + return this.copilotStateModel.notifications.size; + } + + clearAllCopilotNotifications(owner?: string, repo?: string): void { + this.copilotStateModel.clearAllNotifications(owner, repo); + } + + clearCopilotNotification(owner: string, repo: string, pullRequestNumber: number): void { + this.copilotStateModel.clearNotification(owner, repo, pullRequestNumber); + } + + hasCopilotNotification(owner: string, repo: string, pullRequestNumber?: number): boolean { + if (pullRequestNumber !== undefined) { + const key = this.copilotStateModel.makeKey(owner, repo, pullRequestNumber); + return this.copilotStateModel.notifications.has(key); + } else { + const partialKey = this.copilotStateModel.makeKey(owner, repo); + return Array.from(this.copilotStateModel.notifications.keys()).some(key => { + return key.startsWith(partialKey); + }); + } + } + + getCopilotStateForPR(owner: string, repo: string, prNumber: number): CopilotPRStatus { + return this.copilotStateModel.get(owner, repo, prNumber); + } + + getCopilotCounts(owner: string, repo: string): { total: number; inProgress: number; error: number } { + return this.copilotStateModel.getCounts(owner, repo); + } + + clearCopilotCaches() { + const copilotQuery = getCopilotQuery(); + if (!copilotQuery) { + return false; + } + for (const folderManager of this._reposManager.folderManagers) { + this._clearOneCache(folderManager, copilotQuery); + } + } + + private _getStateChangesPromise: Promise | undefined; + async refreshCopilotStateChanges(clearCache: boolean = false): Promise { + // Return the existing in-flight promise if one exists + if (this._getStateChangesPromise) { + return this._getStateChangesPromise; + } + + if (clearCache) { + this.clearCopilotCaches(); + } + + // Create and store the in-flight promise, and ensure it's cleared when done + this._getStateChangesPromise = (async () => { + try { + const unseenKeys: Set = new Set(this.copilotStateModel.keys()); + let initialized = 0; + + const copilotQuery = getCopilotQuery(); + if (!copilotQuery) { + return false; + } + + const changes: CodingAgentPRAndStatus[] = []; + for (const folderManager of this._reposManager.folderManagers) { + initialized++; + const items: PullRequestModel[] = []; + let hasMore = true; + do { + const prs = await this.getPullRequestsForQuery(folderManager, !this.copilotStateModel.isInitialized, copilotQuery, true); + items.push(...prs.items); + hasMore = prs.hasMorePages; + } while (hasMore); + + for (const pr of items) { + unseenKeys.delete(this.copilotStateModel.makeKey(pr.remote.owner, pr.remote.repositoryName, pr.number)); + const copilotEvents = await pr.getCopilotTimelineEvents(false, !this.copilotStateModel.isInitialized); + let latestEvent = copilotEventToStatus(copilotEvents[copilotEvents.length - 1]); + if (latestEvent === CopilotPRStatus.None) { + if (!COPILOT_ACCOUNTS[pr.author.login]) { + continue; + } + latestEvent = CopilotPRStatus.Started; + } + const lastStatus = this.copilotStateModel.get(pr.remote.owner, pr.remote.repositoryName, pr.number) ?? CopilotPRStatus.None; + if (latestEvent !== lastStatus) { + changes.push({ item: pr, status: latestEvent }); + } + } + } + for (const key of unseenKeys) { + this.copilotStateModel.deleteKey(key); + } + this.copilotStateModel.set(changes); + if (!this.copilotStateModel.isInitialized) { + if ((initialized === this._reposManager.folderManagers.length) && (this._reposManager.folderManagers.length > 0)) { + Logger.debug(`Copilot PR state initialized with ${this.copilotStateModel.keys().length} PRs`, PrsTreeModel.ID); + this.copilotStateModel.setInitialized(); + } + return true; + } else { + return true; + } + } finally { + // Ensure the stored promise is cleared so subsequent calls start a new run + this._getStateChangesPromise = undefined; + } + })(); + + return this._getStateChangesPromise; + } + + async getCopilotPullRequests(clearCache: boolean = false): Promise { + if (clearCache) { + this.clearCopilotCaches(); + } + + await this.refreshCopilotStateChanges(clearCache); + return this.copilotStateModel.all; + } + override dispose() { super.dispose(); disposeAll(Array.from(this._activePRDisposables.values()).flat()); diff --git a/src/view/pullRequestCommentController.ts b/src/view/pullRequestCommentController.ts index 69b4daa4ec..e60677266d 100644 --- a/src/view/pullRequestCommentController.ts +++ b/src/view/pullRequestCommentController.ts @@ -6,12 +6,13 @@ import { v4 as uuid } from 'uuid'; import * as vscode from 'vscode'; import { CommentHandler, registerCommentHandler, unregisterCommentHandler } from '../commentHandlerResolver'; +import { CommentControllerBase } from './commentControllBase'; import { DiffSide, IComment, SubjectType } from '../common/comment'; import { disposeAll } from '../common/lifecycle'; import Logger from '../common/logger'; import { ITelemetry } from '../common/telemetry'; import { fromPRUri, Schemes } from '../common/uri'; -import { groupBy } from '../common/utils'; +import { formatError, groupBy } from '../common/utils'; import { PULL_REQUEST_OVERVIEW_VIEW_TYPE } from '../common/webview'; import { FolderRepositoryManager } from '../github/folderRepositoryManager'; import { GitHubRepository } from '../github/githubRepository'; @@ -21,16 +22,18 @@ import { PullRequestOverviewPanel } from '../github/pullRequestOverview'; import { CommentReactionHandler, createVSCodeCommentThreadForReviewThread, + setReplyAuthor, threadRange, updateCommentReviewState, updateCommentThreadLabel, updateThread, updateThreadWithRange, } from '../github/utils'; -import { CommentControllerBase } from './commentControllBase'; export class PullRequestCommentController extends CommentControllerBase implements CommentHandler, CommentReactionHandler { private static ID = 'PullRequestCommentController'; + static readonly PREFIX = 'github-browse'; + private _pendingCommentThreadAdds: GHPRCommentThread[] = []; private _commentHandlerId: string; private _commentThreadCache: { [key: string]: GHPRCommentThread[] } = {}; @@ -224,8 +227,8 @@ export class PullRequestCommentController extends CommentControllerBase implemen } } - private onDidChangeReviewThreads(e: ReviewThreadChangeEvent): void { - e.added.forEach(async (thread) => { + private async onDidChangeReviewThreads(e: ReviewThreadChangeEvent): Promise { + for (const thread of e.added) { const fileName = thread.path; const index = this._pendingCommentThreadAdds.findIndex(t => { const samePath = this._folderRepoManager.gitRelativeRootPath(t.uri.path) === thread.path; @@ -275,18 +278,18 @@ export class PullRequestCommentController extends CommentControllerBase implemen } else { this._commentThreadCache[key] = [newThread]; } - }); + } - e.changed.forEach(thread => { + for (const thread of e.changed) { const key = this.getCommentThreadCacheKey(thread.path, thread.diffSide === DiffSide.LEFT); const index = this._commentThreadCache[key] ? this._commentThreadCache[key].findIndex(t => t.gitHubThreadId === thread.id) : -1; if (index > -1) { const matchingThread = this._commentThreadCache[key][index]; updateThread(this._context, matchingThread, thread, this._githubRepositories); } - }); + } - e.removed.forEach(async thread => { + for (const thread of e.removed) { const key = this.getCommentThreadCacheKey(thread.path, thread.diffSide === DiffSide.LEFT); const index = this._commentThreadCache[key].findIndex(t => t.gitHubThreadId === thread.id); if (index > -1) { @@ -294,7 +297,23 @@ export class PullRequestCommentController extends CommentControllerBase implemen this._commentThreadCache[key].splice(index, 1); matchingThread.dispose(); } - }); + } + } + + protected override onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) { + const activeTab = vscode.window.tabGroups.activeTabGroup.activeTab; + const activeUri = activeTab?.input instanceof vscode.TabInputText ? activeTab.input.uri : (activeTab?.input instanceof vscode.TabInputTextDiff ? activeTab.input.original : undefined); + + if (editor === undefined || !editor.document.uri.authority.startsWith(PullRequestCommentController.PREFIX) || !activeUri || (activeUri.scheme !== Schemes.Pr)) { + return; + } + + const params = fromPRUri(activeUri); + if (!params || params.prNumber !== this.pullRequestModel.number) { + return; + } + + return this.tryAddCopilotMention(editor, this.pullRequestModel); } hasCommentThread(thread: GHPRCommentThread): boolean { @@ -337,14 +356,15 @@ export class PullRequestCommentController extends CommentControllerBase implemen const fileName = this._folderRepoManager.gitRelativeRootPath(thread.uri.path); const side = this.getCommentSide(thread); this._pendingCommentThreadAdds.push(thread); - await this.pullRequestModel.createReviewThread( + await Promise.all([this.pullRequestModel.createReviewThread( input, fileName, thread.range ? (thread.range.start.line + 1) : undefined, thread.range ? (thread.range.end.line + 1) : undefined, side, isSingleComment, - ); + ), + setReplyAuthor(thread, await this._folderRepoManager.getCurrentUser(this.pullRequestModel.githubRepository), this._context)]); } if (isSingleComment) { @@ -354,7 +374,7 @@ export class PullRequestCommentController extends CommentControllerBase implemen if (e.graphQLErrors?.length && e.graphQLErrors[0].type === 'NOT_FOUND') { vscode.window.showWarningMessage('The comment that you\'re replying to was deleted. Refresh to update.', 'Refresh').then(result => { if (result === 'Refresh') { - this.pullRequestModel.invalidate(); + this.pullRequestModel.githubRepository.getPullRequest(this.pullRequestModel.number); } }); } else { @@ -526,14 +546,25 @@ export class PullRequestCommentController extends CommentControllerBase implemen return; } - if ( - comment.reactions && - !comment.reactions.find(ret => ret.label === reaction.label && !!ret.authorHasReacted) - ) { - // add reaction - await this.pullRequestModel.addCommentReaction(comment.rawComment.graphNodeId, reaction); - } else { - await this.pullRequestModel.deleteCommentReaction(comment.rawComment.graphNodeId, reaction); + try { + if ( + comment.reactions && + !comment.reactions.find(ret => ret.label === reaction.label && !!ret.authorHasReacted) + ) { + // add reaction + await this.pullRequestModel.addCommentReaction(comment.rawComment.graphNodeId, reaction); + } else { + await this.pullRequestModel.deleteCommentReaction(comment.rawComment.graphNodeId, reaction); + } + } catch (e) { + // Ignore permission errors when removing reactions due to race conditions + // See: https://github.com/microsoft/vscode/issues/69321 + const errorMessage = formatError(e); + if (errorMessage.includes('does not have the correct permissions to execute `RemoveReaction`')) { + // Silently ignore this error - it occurs when quickly toggling reactions + return; + } + throw new Error(errorMessage); } } diff --git a/src/view/pullRequestCommentControllerRegistry.ts b/src/view/pullRequestCommentControllerRegistry.ts index 2afe7c2b1f..e4eeb6ad12 100644 --- a/src/view/pullRequestCommentControllerRegistry.ts +++ b/src/view/pullRequestCommentControllerRegistry.ts @@ -5,6 +5,7 @@ 'use strict'; import * as vscode from 'vscode'; +import { PullRequestCommentController } from './pullRequestCommentController'; import { Disposable } from '../common/lifecycle'; import { ITelemetry } from '../common/telemetry'; import { fromPRUri, Schemes } from '../common/uri'; @@ -12,17 +13,20 @@ import { FolderRepositoryManager } from '../github/folderRepositoryManager'; import { GHPRComment } from '../github/prComment'; import { PullRequestModel } from '../github/pullRequestModel'; import { CommentReactionHandler } from '../github/utils'; -import { PullRequestCommentController } from './pullRequestCommentController'; -interface PullRequestCommentHandlerInfo { +interface PullRequestCommentHandlerInfo extends vscode.Disposable { handler: PullRequestCommentController & CommentReactionHandler; refCount: number; - dispose: () => void; +} + +interface PRCommentingRangeProviderInfo extends vscode.Disposable { + provider: vscode.CommentingRangeProvider2; + refCount: number; } 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 +44,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); @@ -82,7 +86,7 @@ export class PRCommentControllerRegistry extends Disposable implements vscode.Co if (!this._activeChangeListeners.has(folderRepositoryManager)) { this._activeChangeListeners.set(folderRepositoryManager, folderRepositoryManager.onDidChangeActivePullRequest(e => { if (e.old) { - this._prCommentHandlers[e.old]?.dispose(); + this._prCommentHandlers[e.old.number]?.dispose(); } })); } @@ -108,13 +112,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() { diff --git a/src/view/repositoryFileSystemProvider.ts b/src/view/repositoryFileSystemProvider.ts index 01c15d9d1c..78ab8a849a 100644 --- a/src/view/repositoryFileSystemProvider.ts +++ b/src/view/repositoryFileSystemProvider.ts @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { ReadonlyFileSystemProvider } from './readonlyFileSystemProvider'; import { GitApiImpl } from '../api/api1'; import Logger from '../common/logger'; import { CredentialStore } from '../github/credentials'; -import { ReadonlyFileSystemProvider } from './readonlyFileSystemProvider'; +import { RepositoriesManager } from '../github/repositoriesManager'; export abstract class RepositoryFileSystemProvider extends ReadonlyFileSystemProvider { constructor(protected gitAPI: GitApiImpl, protected credentialStore: CredentialStore) { @@ -47,4 +48,20 @@ export abstract class RepositoryFileSystemProvider extends ReadonlyFileSystemPro } return new Promise(resolve => this.credentialStore.onDidGetSession(() => resolve())); } + + protected async waitForAnyGitHubRepos(reposManager: RepositoriesManager): Promise { + // Check if any folder manager already has GitHub repositories + if (reposManager.folderManagers.some(manager => manager.gitHubRepositories.length > 0)) { + return; + } + + Logger.appendLine('Waiting for GitHub repositories.', 'RepositoryFileSystemProvider'); + return new Promise(resolve => { + const disposable = reposManager.onDidChangeAnyGitHubRepository(() => { + Logger.appendLine('Found GitHub repositories.', 'RepositoryFileSystemProvider'); + disposable.dispose(); + resolve(); + }); + }); + } } \ No newline at end of file diff --git a/src/view/reviewCommentController.ts b/src/view/reviewCommentController.ts index f014c4e9dc..4c4c7fe090 100644 --- a/src/view/reviewCommentController.ts +++ b/src/view/reviewCommentController.ts @@ -9,6 +9,10 @@ import * as vscode from 'vscode'; import { Repository } from '../api/api'; import { GitApiImpl } from '../api/api1'; import { CommentHandler, registerCommentHandler, unregisterCommentHandler } from '../commentHandlerResolver'; +import { CommentControllerBase } from './commentControllBase'; +import { RemoteFileChangeModel } from './fileChangeModel'; +import { ReviewManager } from './reviewManager'; +import { ReviewModel } from './reviewModel'; import { DiffSide, IReviewThread, SubjectType } from '../common/comment'; import { getCommentingRanges } from '../common/commentingRanges'; import { mapNewPositionToOld, mapOldPositionToNew } from '../common/diffPositionMapping'; @@ -19,7 +23,7 @@ import Logger from '../common/logger'; import { PR_SETTINGS_NAMESPACE, PULL_BRANCH, PULL_PR_BRANCH_BEFORE_CHECKOUT, PullPRBranchVariants } from '../common/settingKeys'; import { ITelemetry } from '../common/telemetry'; import { fromReviewUri, ReviewUriParams, Schemes, toReviewUri } from '../common/uri'; -import { formatError, groupBy, uniqBy } from '../common/utils'; +import { arrayFindIndexAsync, formatError, groupBy, uniqBy } from '../common/utils'; import { FolderRepositoryManager } from '../github/folderRepositoryManager'; import { GHPRComment, GHPRCommentThread, TemporaryComment } from '../github/prComment'; import { PullRequestOverviewPanel } from '../github/pullRequestOverview'; @@ -28,16 +32,13 @@ import { createVSCodeCommentThreadForReviewThread, getRepositoryForFile, isFileInRepo, + setReplyAuthor, threadRange, updateCommentReviewState, updateCommentThreadLabel, updateThread, updateThreadWithRange, } from '../github/utils'; -import { CommentControllerBase } from './commentControllBase'; -import { RemoteFileChangeModel } from './fileChangeModel'; -import { ReviewManager } from './reviewManager'; -import { ReviewModel } from './reviewModel'; import { GitFileChangeNode, gitFileChangeNodeFilter, RemoteFileChangeNode } from './treeNodes/fileChangeNode'; export interface SuggestionInformation { @@ -48,6 +49,7 @@ export interface SuggestionInformation { export class ReviewCommentController extends CommentControllerBase implements CommentHandler, vscode.CommentingRangeProvider2, CommentReactionHandler { private static readonly ID = 'ReviewCommentController'; + private static readonly PREFIX = 'github-review'; private _commentHandlerId: string; // Note: marked as protected so that tests can verify caches have been updated correctly without breaking type safety @@ -72,7 +74,7 @@ export class ReviewCommentController extends CommentControllerBase implements Co super(folderRepoManager, telemetry); this._context = this._folderRepoManager.context; this._commentController = this._register(vscode.comments.createCommentController( - `github-review-${folderRepoManager.activePullRequest?.remote.owner}-${folderRepoManager.activePullRequest?.remote.owner}-${folderRepoManager.activePullRequest!.number}`, + `${ReviewCommentController.PREFIX}-${folderRepoManager.activePullRequest?.remote.owner}-${folderRepoManager.activePullRequest?.remote.owner}-${folderRepoManager.activePullRequest!.number}`, vscode.l10n.t('Pull Request ({0})', folderRepoManager.activePullRequest!.title), )); this._commentController.commentingRangeProvider = this as vscode.CommentingRangeProvider; @@ -273,12 +275,12 @@ export class ReviewCommentController extends CommentControllerBase implements Co ); this._register( - activePullRequest.onDidChangeReviewThreads(e => { + activePullRequest.onDidChangeReviewThreads(async e => { const githubRepositories = this.githubReposForPullRequest(this._folderRepoManager.activePullRequest); - e.added.forEach(async thread => { + for (const thread of e.added) { const { path } = thread; - const index = this._pendingCommentThreadAdds.findIndex(async t => { + const index = await arrayFindIndexAsync(this._pendingCommentThreadAdds, async t => { const fileName = this._folderRepoManager.gitRelativeRootPath(t.uri.path); if (fileName !== thread.path) { return false; @@ -322,29 +324,28 @@ export class ReviewCommentController extends CommentControllerBase implements Co } else { threadMap[path] = [newThread]; } - }); + } - e.changed.forEach(thread => { + for (const thread of e.changed) { const match = this._findMatchingThread(thread); if (match.index > -1) { const matchingThread = match.threadMap[thread.path][match.index]; updateThread(this._context, matchingThread, thread, githubRepositories); } - }); + } - e.removed.forEach(thread => { + for (const thread of e.removed) { const match = this._findMatchingThread(thread); if (match.index > -1) { const matchingThread = match.threadMap[thread.path][match.index]; match.threadMap[thread.path].splice(match.index, 1); matchingThread.dispose(); } - }); + } this.updateResourcesWithCommentingRanges(); }), ); - this._register(vscode.window.onDidChangeActiveTextEditor(e => this.onDidChangeActiveTextEditor(e))); } private _findMatchingThread(thread: IReviewThread): { threadMap: { [key: string]: GHPRCommentThread[] }, index: number } { @@ -370,9 +371,19 @@ export class ReviewCommentController extends CommentControllerBase implements Co } private _commentContentChangedListener: vscode.Disposable | undefined; - private onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) { + protected onDidChangeActiveTextEditor(editor: vscode.TextEditor | undefined) { this._commentContentChangedListener?.dispose(); this._commentContentChangedListener = undefined; + + const activeTab = vscode.window.tabGroups.activeTabGroup.activeTab; + const activeUri = activeTab?.input instanceof vscode.TabInputText ? activeTab.input.uri : (activeTab?.input instanceof vscode.TabInputTextDiff ? activeTab.input.modified : undefined); + + if (editor && activeUri && editor.document.uri.authority.startsWith(ReviewCommentController.PREFIX) && (activeUri.scheme === Schemes.File)) { + if (this._folderRepoManager.activePullRequest && activeUri.toString().startsWith(this._repository.rootUri.toString())) { + this.tryAddCopilotMention(editor, this._folderRepoManager.activePullRequest); + } + } + if (editor?.document.uri.scheme !== Schemes.Comment) { return; } @@ -682,7 +693,9 @@ export class ReviewCommentController extends CommentControllerBase implements Co endLine++; } - await this._folderRepoManager.activePullRequest!.createReviewThread(input, fileName, startLine, endLine, side); + await Promise.all([this._folderRepoManager.activePullRequest!.createReviewThread(input, fileName, startLine, endLine, side), + setReplyAuthor(thread, await this._folderRepoManager.getCurrentUser(this._folderRepoManager.activePullRequest!.githubRepository), this._context) + ]); } else { const comment = thread.comments[0]; if (comment instanceof GHPRComment) { @@ -787,14 +800,17 @@ export class ReviewCommentController extends CommentControllerBase implements Co startLine++; endLine++; } - await this._folderRepoManager.activePullRequest.createReviewThread( - input, - fileName, - startLine, - endLine, - side, - isSingleComment, - ); + await Promise.all([ + this._folderRepoManager.activePullRequest.createReviewThread( + input, + fileName, + startLine, + endLine, + side, + isSingleComment, + ), + setReplyAuthor(thread, await this._folderRepoManager.getCurrentUser(this._folderRepoManager.activePullRequest.githubRepository), this._context) + ]); } else { const comment = thread.comments[0]; if (comment instanceof GHPRComment) { @@ -969,7 +985,14 @@ ${suggestionInformation.suggestionContent} ); } } catch (e) { - throw new Error(formatError(e)); + // Ignore permission errors when removing reactions due to race conditions + // See: https://github.com/microsoft/vscode/issues/69321 + const errorMessage = formatError(e); + if (errorMessage.includes('does not have the correct permissions to execute `RemoveReaction`')) { + // Silently ignore this error - it occurs when quickly toggling reactions + return; + } + throw new Error(errorMessage); } } diff --git a/src/view/reviewManager.ts b/src/view/reviewManager.ts index 85c192ca0d..b9c31fac14 100644 --- a/src/view/reviewManager.ts +++ b/src/view/reviewManager.ts @@ -8,6 +8,14 @@ import * as vscode from 'vscode'; import type { Branch, Change, Repository } from '../api/api'; import { GitApiImpl, GitErrorCodes, Status } from '../api/api1'; import { openDescription } from '../commands'; +import { CreatePullRequestHelper } from './createPullRequestHelper'; +import { GitFileChangeModel, InMemFileChangeModel, RemoteFileChangeModel } from './fileChangeModel'; +import { getInMemPRFileSystemProvider, provideDocumentContentForChangeModel } from './inMemPRContentProvider'; +import { PullRequestChangesTreeDataProvider } from './prChangesTreeDataProvider'; +import { ProgressHelper } from './progress'; +import { PullRequestsTreeDataProvider } from './prsTreeDataProvider'; +import { ReviewCommentController, SuggestionInformation } from './reviewCommentController'; +import { ReviewModel } from './reviewModel'; import { DiffChangeType, DiffHunk, parsePatch, splitIntoSmallerHunks } from '../common/diffHunk'; import { commands } from '../common/executeCommands'; import { GitChangeType, InMemFileChange, SlimFileChange } from '../common/file'; @@ -37,14 +45,6 @@ import { GitHubRepository } from '../github/githubRepository'; import { GithubItemStateEnum } from '../github/interface'; import { PullRequestGitHelper, PullRequestMetadata } from '../github/pullRequestGitHelper'; import { IResolvedPullRequestModel, PullRequestModel } from '../github/pullRequestModel'; -import { CreatePullRequestHelper } from './createPullRequestHelper'; -import { GitFileChangeModel, InMemFileChangeModel, RemoteFileChangeModel } from './fileChangeModel'; -import { getInMemPRFileSystemProvider, provideDocumentContentForChangeModel } from './inMemPRContentProvider'; -import { PullRequestChangesTreeDataProvider } from './prChangesTreeDataProvider'; -import { ProgressHelper } from './progress'; -import { PullRequestsTreeDataProvider } from './prsTreeDataProvider'; -import { ReviewCommentController, SuggestionInformation } from './reviewCommentController'; -import { ReviewModel } from './reviewModel'; import { GitFileChangeNode, gitFileChangeNodeFilter, RemoteFileChangeNode } from './treeNodes/fileChangeNode'; import { WebviewViewCoordinator } from './webviewViewCoordinator'; @@ -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; @@ -357,33 +362,37 @@ export class ReviewManager extends Disposable { } private async checkGitHubForPrBranch(branch: Branch): Promise<(PullRequestMetadata & { model: PullRequestModel }) | undefined> { - - let branchToCheck: Branch; - if (this._repository.state.HEAD && (branch.name === this._repository.state.HEAD.name)) { - branchToCheck = this._repository.state.HEAD; - } else { - branchToCheck = branch; - } - const { remoteUrl: url, upstreamBranchName, remoteName } = await this.getUpstreamUrlAndName(branchToCheck); - const metadataFromGithub = await this._folderRepoManager.getMatchingPullRequestMetadataFromGitHub(branchToCheck, remoteName, url, upstreamBranchName); - if (metadataFromGithub) { - Logger.appendLine(`Found matching pull request metadata on GitHub for current branch ${branch.name}. Repo: ${metadataFromGithub.owner}/${metadataFromGithub.repositoryName} PR: ${metadataFromGithub.prNumber}`, this.id); - await PullRequestGitHelper.associateBranchWithPullRequest( - this._repository, - metadataFromGithub.model, - branch.name!, - ); - return metadataFromGithub; + try { + let branchToCheck: Branch; + if (this._repository.state.HEAD && (branch.name === this._repository.state.HEAD.name)) { + branchToCheck = this._repository.state.HEAD; + } else { + branchToCheck = branch; + } + const { remoteUrl: url, upstreamBranchName, remoteName } = await this.getUpstreamUrlAndName(branchToCheck); + const metadataFromGithub = await this._folderRepoManager.getMatchingPullRequestMetadataFromGitHub(branchToCheck, remoteName, url, upstreamBranchName); + if (metadataFromGithub) { + Logger.appendLine(`Found matching pull request metadata on GitHub for current branch ${branch.name}. Repo: ${metadataFromGithub.owner}/${metadataFromGithub.repositoryName} PR: ${metadataFromGithub.prNumber}`, this.id); + await PullRequestGitHelper.associateBranchWithPullRequest( + this._repository, + metadataFromGithub.model, + branch.name!, + ); + return metadataFromGithub; + } + } catch (e) { + Logger.warn(`Failed to check GitHub for PR branch: ${e.message}`, this.id); + return undefined; } } - 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); @@ -441,19 +450,14 @@ export class ReviewManager extends Disposable { } Logger.appendLine(`Found matching pull request metadata for current branch ${branch.name}. Repo: ${matchingPullRequestMetadata.owner}/${matchingPullRequestMetadata.repositoryName} PR: ${matchingPullRequestMetadata.prNumber}`, this.id); - const remote = branch.upstream ? branch.upstream.remote : null; - if (!remote) { - Logger.appendLine(`Current branch ${this._repository.state.HEAD.name} hasn't setup remote yet`, this.id); - await this.clear(true); - return; - } - // we switch to another PR, let's clean up first. Logger.appendLine( `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; @@ -464,7 +468,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; } @@ -472,7 +476,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; @@ -482,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; @@ -537,11 +544,16 @@ export class ReviewManager extends Disposable { this.changesInPrDataProvider.refresh(); await this.updateComments(); await this.reopenNewReviewDiffs(); + if (pr) { + PullRequestModel.openChanges(this._folderRepoManager, pr); + } this._changesSinceLastReviewProgress.endProgress(); }) ); 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 = { @@ -623,6 +635,18 @@ export class ReviewManager extends Disposable { } } + private _openFirstDiff() { + if (this._reviewModel.localFileChanges.length) { + this.openDiff(); + } else { + const localFileChangesDisposable = this._reviewModel.onDidChangeLocalFileChanges(() => { + localFileChangesDisposable.dispose(); + this.openDiff(); + }); + } + } + + private _doFocusShow(pr: PullRequestModel, updateLayout: boolean) { // Respect the setting 'comments.openView' when it's 'never'. const shouldShowCommentsView = vscode.workspace.getConfiguration(COMMENTS).get<'never' | string>(OPEN_VIEW); @@ -632,19 +656,21 @@ export class ReviewManager extends Disposable { this._activePrViewCoordinator.show(pr); if (updateLayout) { const focusedMode = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<'firstDiff' | 'overview' | 'multiDiff' | false>(FOCUSED_MODE); + if (focusedMode && pr.fileChanges.size === 0) { + // If there are no file changes, the only useful thing to show is the overview + return this.openDescription(); + } + if (focusedMode === 'firstDiff') { - if (this._reviewModel.localFileChanges.length) { - this.openDiff(); - } else { - const localFileChangesDisposable = this._reviewModel.onDidChangeLocalFileChanges(() => { - localFileChangesDisposable.dispose(); - this.openDiff(); - }); - } + return this._openFirstDiff(); } else if (focusedMode === 'overview') { return this.openDescription(); } else if (focusedMode === 'multiDiff') { - return PullRequestModel.openChanges(this._folderRepoManager, pr); + if (pr.fileChanges.size < 400) { + return PullRequestModel.openChanges(this._folderRepoManager, pr); + } else { + return this._openFirstDiff(); + } } } } @@ -1072,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) => { @@ -1225,6 +1252,7 @@ export class ReviewManager extends Disposable { this._prNumber = undefined; this._folderRepoManager.activePullRequest = undefined; + this._switchedToPullRequest = undefined; if (this._statusBarItem) { this._statusBarItem.hide(); diff --git a/src/view/reviewsManager.ts b/src/view/reviewsManager.ts index 407f5954fe..2995e0c8a8 100644 --- a/src/view/reviewsManager.ts +++ b/src/view/reviewsManager.ts @@ -4,18 +4,24 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { GitContentFileSystemProvider } from './gitContentProvider'; +import { PullRequestChangesTreeDataProvider } from './prChangesTreeDataProvider'; +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 { GitContentFileSystemProvider } from './gitContentProvider'; -import { PullRequestChangesTreeDataProvider } from './prChangesTreeDataProvider'; -import { PullRequestsTreeDataProvider } from './prsTreeDataProvider'; -import { ReviewManager } from './reviewManager'; +import { NotificationsManager } from '../notifications/notificationsManager'; export class ReviewsManager extends Disposable { public static ID = 'Reviews'; @@ -24,11 +30,14 @@ export class ReviewsManager extends Disposable { private _context: vscode.ExtensionContext, private _reposManager: RepositoriesManager, private _reviewManagers: ReviewManager[], + private _prsTreeModel: PrsTreeModel, private _prsTreeDataProvider: PullRequestsTreeDataProvider, private _prFileChangesProvider: PullRequestChangesTreeDataProvider, private _telemetry: ITelemetry, private _credentialStore: CredentialStore, private _gitApi: GitApiImpl, + private _copilotManager: CopilotRemoteAgentManager, + private _notificationsManager: NotificationsManager, ) { super(); const gitContentProvider = new GitContentFileSystemProvider(_gitApi, _credentialStore, () => this._reviewManagers); @@ -55,8 +64,8 @@ export class ReviewsManager extends Disposable { } this._prsTreeDataProvider.dispose(); - this._prsTreeDataProvider = this._register(new PullRequestsTreeDataProvider(this._telemetry, this._context, this._reposManager)); - this._prsTreeDataProvider.initialize(this._reviewManagers.map(manager => manager.reviewModel), this._credentialStore); + this._prsTreeDataProvider = this._register(new PullRequestsTreeDataProvider(this._prsTreeModel, this._telemetry, this._context, this._reposManager, this._copilotManager)); + this._prsTreeDataProvider.initialize(this._reviewManagers.map(manager => manager.reviewModel), this._notificationsManager); } })); } @@ -98,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; + } } + diff --git a/src/view/theme.ts b/src/view/theme.ts new file mode 100644 index 0000000000..825395f094 --- /dev/null +++ b/src/view/theme.ts @@ -0,0 +1,90 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ +import { parse } from 'jsonc-parser'; +import * as vscode from 'vscode'; +import { COLOR_THEME, WORKBENCH } from '../common/settingKeys'; + +export async function loadCurrentThemeData(): Promise { + let themeData: any = null; + const currentThemeName = vscode.workspace.getConfiguration(WORKBENCH).get(COLOR_THEME); + if (currentThemeName) { + const path = getCurrentThemePaths(currentThemeName); + if (path) { + themeData = await loadThemeFromFile(path); + } + } + return themeData; +} + +export interface ThemeData { + type: string, + colors?: { [key: string]: string } + // eslint-disable-next-line rulesdir/no-any-except-union-method-signature + tokenColors: any[], + // eslint-disable-next-line rulesdir/no-any-except-union-method-signature + semanticTokenColors: any[] +} + +async function loadThemeFromFile(path: vscode.Uri): Promise { + const decoder = new TextDecoder(); + const decoded = decoder.decode(await vscode.workspace.fs.readFile(path)); + let themeData = parse(decoded); + + // Also load the include file if specified + if (themeData.include) { + try { + const includePath = vscode.Uri.joinPath(path, '..', themeData.include); + const includeData = await loadThemeFromFile(includePath); + themeData = { + ...themeData, + colors: { + ...(includeData.colors || {}), + ...(themeData.colors || {}), + }, + tokenColors: [ + ...(includeData.tokenColors || []), + ...(themeData.tokenColors || []), + ], + semanticTokenColors: { + ...(includeData.semanticTokenColors || {}), + ...(themeData.semanticTokenColors || {}), + }, + }; + } catch (error) { + console.warn(`Failed to load theme include file: ${error}`); + } + } + + return themeData; +} + +function getCurrentThemePaths(themeName: string): vscode.Uri | undefined { + for (const ext of vscode.extensions.all) { + const themes = ext.packageJSON.contributes && ext.packageJSON.contributes.themes; + if (!themes) { + continue; + } + const theme = themes.find(theme => theme.label === themeName || theme.id === themeName); + if (theme) { + return vscode.Uri.joinPath(ext.extensionUri, theme.path); + } + } +} + +export function getIconForeground(themeData: ThemeData | undefined, kind: 'light' | 'dark'): string { + return themeData?.colors?.['icon.foreground'] ?? (kind === 'dark' ? '#C5C5C5' : '#424242'); +} + +export function getListWarningForeground(themeData: ThemeData | undefined, kind: 'light' | 'dark'): string { + return themeData?.colors?.['list.warningForeground'] ?? (kind === 'dark' ? '#CCA700' : '#855F00'); +} + +export function getListErrorForeground(themeData: ThemeData | undefined, kind: 'light' | 'dark'): string { + return themeData?.colors?.['list.errorForeground'] ?? (kind === 'dark' ? '#F88070' : '#B01011'); +} + +export function getNotebookStatusSuccessIconForeground(themeData: ThemeData | undefined, kind: 'light' | 'dark'): string { + return themeData?.colors?.['notebookStatusSuccessIcon.foreground'] ?? (kind === 'dark' ? '#89D185' : '#388A34'); +} diff --git a/src/view/treeDecorationProviders.ts b/src/view/treeDecorationProviders.ts index 0a42838e35..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; @@ -63,7 +63,7 @@ export class TreeDecorationProviders extends Disposable { this._pullRequestListeners.push(gitHubRepo.onDidAddPullRequest(model => { this._pullRequestPropertyChangeListeners.push(...this._registerPullRequestPropertyListeners(folderManager, model)); })); - const models = Array.from(gitHubRepo.pullRequestModels.values()); + const models = gitHubRepo.pullRequestModels; const listeners = models.map(model => { return this._registerPullRequestPropertyListeners(folderManager, model); }).flat(); diff --git a/src/view/treeNodes/categoryNode.ts b/src/view/treeNodes/categoryNode.ts index cc6d559f4a..e2585463e4 100644 --- a/src/view/treeNodes/categoryNode.ts +++ b/src/view/treeNodes/categoryNode.ts @@ -4,17 +4,22 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { RemoteInfo } from '../../../common/types'; import { AuthenticationError } from '../../common/authentication'; -import { PR_SETTINGS_NAMESPACE, QUERIES } from '../../common/settingKeys'; import { ITelemetry } from '../../common/telemetry'; +import { toQueryUri } from '../../common/uri'; import { formatError } from '../../common/utils'; +import { isCopilotQuery } from '../../github/copilotPrWatcher'; +import { CopilotRemoteAgentManager } from '../../github/copilotRemoteAgent'; import { FolderRepositoryManager, ItemsResponseResult } from '../../github/folderRepositoryManager'; import { PRType } from '../../github/interface'; -import { NotificationProvider } from '../../github/notifications'; import { PullRequestModel } from '../../github/pullRequestModel'; +import { extractRepoFromQuery } from '../../github/utils'; import { PrsTreeModel } from '../prsTreeModel'; import { PRNode } from './pullRequestNode'; import { TreeNode, TreeNodeParent } from './treeNode'; +import { IQueryInfo } from './workspaceFolderNode'; +import { NotificationsManager } from '../../notifications/notificationsManager'; export enum PRCategoryActionType { Empty, @@ -27,22 +32,9 @@ export enum PRCategoryActionType { ConfigureRemotes, } -interface QueryInspect { - key: string; - defaultValue?: { label: string; query: string }[]; - globalValue?: { label: string; query: string }[]; - workspaceValue?: { label: string; query: string }[]; - workspaceFolderValue?: { label: string; query: string }[]; - defaultLanguageValue?: { label: string; query: string }[]; - globalLanguageValue?: { label: string; query: string }[]; - workspaceLanguageValue?: { label: string; query: string }[]; - workspaceFolderLanguageValue?: { label: string; query: string }[]; - languageIds?: string[] -} - export class PRCategoryActionNode extends TreeNode implements vscode.TreeItem { public collapsibleState: vscode.TreeItemCollapsibleState; - public iconPath?: { light: string | vscode.Uri; dark: string | vscode.Uri }; + public iconPath?: { light: vscode.Uri; dark: vscode.Uri }; public type: PRCategoryActionType; public command?: vscode.Command; @@ -115,26 +107,50 @@ interface PageInformation { hasMorePages: boolean; } +export namespace DefaultQueries { + export namespace Queries { + export const LOCAL = 'Local Pull Request Branches'; + export const ALL = 'All Open'; + } + export namespace Values { + export const DEFAULT = 'default'; + } +} + +export function isLocalQuery(queryInfo: IQueryInfo): boolean { + return queryInfo.label === DefaultQueries.Queries.LOCAL && queryInfo.query === DefaultQueries.Values.DEFAULT; +} + +export function isAllQuery(queryInfo: IQueryInfo): boolean { + return queryInfo.label === DefaultQueries.Queries.ALL && queryInfo.query === DefaultQueries.Values.DEFAULT; +} + export class CategoryTreeNode extends TreeNode implements vscode.TreeItem { public collapsibleState: vscode.TreeItemCollapsibleState; public prs: Map; public fetchNextPage: boolean = false; public repositoryPageInformation: Map = new Map(); public contextValue: string; + public resourceUri: vscode.Uri; + public tooltip?: string | vscode.MarkdownString | undefined; + readonly isCopilot: boolean; + private _repo: RemoteInfo | undefined; constructor( parent: TreeNodeParent, - private _folderRepoManager: FolderRepositoryManager, + readonly folderRepoManager: FolderRepositoryManager, private _telemetry: ITelemetry, public readonly type: PRType, - private _notificationProvider: NotificationProvider, + private _notificationProvider: NotificationsManager, private _prsTreeModel: PrsTreeModel, + private _copilotManager: CopilotRemoteAgentManager, _categoryLabel?: string, private _categoryQuery?: string, ) { super(parent); this.prs = new Map(); + this.isCopilot = !!_categoryQuery && isCopilotQuery(_categoryQuery); switch (this.type) { case PRType.All: @@ -153,102 +169,63 @@ export class CategoryTreeNode extends TreeNode implements vscode.TreeItem { this.id = parent instanceof TreeNode ? `${parent.id ?? parent.label}/${this.label}` : this.label; - if ((this._prsTreeModel.expandedQueries.size === 0) && (this.type === PRType.All)) { + if ((this._prsTreeModel.expandedQueries === undefined) && (this.type === PRType.All)) { this.collapsibleState = vscode.TreeItemCollapsibleState.Expanded; } else { this.collapsibleState = - this._prsTreeModel.expandedQueries.has(this.id) + this._prsTreeModel.expandedQueries?.has(this.id) ? vscode.TreeItemCollapsibleState.Expanded : vscode.TreeItemCollapsibleState.Collapsed; } if (this._categoryQuery) { - this.contextValue = 'query'; + this.contextValue = this.isCopilot ? 'copilot-query' : 'query'; } - } - private async addNewQuery(config: vscode.WorkspaceConfiguration, inspect: QueryInspect | undefined, startingValue: string) { - const inputBox = vscode.window.createInputBox(); - inputBox.title = vscode.l10n.t('Enter the title of the new query'); - inputBox.placeholder = vscode.l10n.t('Title'); - inputBox.step = 1; - inputBox.totalSteps = 2; - inputBox.show(); - let title: string | undefined; - inputBox.onDidAccept(async () => { - inputBox.validationMessage = ''; - if (inputBox.step === 1) { - if (!inputBox.value) { - inputBox.validationMessage = vscode.l10n.t('Title is required'); - return; - } - - title = inputBox.value; - inputBox.value = startingValue; - inputBox.title = vscode.l10n.t('Enter the GitHub search query'); - inputBox.step++; - } else { - if (!inputBox.value) { - inputBox.validationMessage = vscode.l10n.t('Query is required'); - return; - } - inputBox.busy = true; - if (inputBox.value && title) { - if (inspect?.workspaceValue) { - inspect.workspaceValue.push({ label: title, query: inputBox.value }); - await config.update(QUERIES, inspect.workspaceValue, vscode.ConfigurationTarget.Workspace); - } else { - const value = config.get<{ label: string; query: string }[]>(QUERIES); - value?.push({ label: title, query: inputBox.value }); - await config.update(QUERIES, value, vscode.ConfigurationTarget.Global); - } - } - inputBox.dispose(); - } - }); - inputBox.onDidHide(() => inputBox.dispose()); + if (this.isCopilot) { + this.tooltip = vscode.l10n.t('Pull requests you asked the coding agent to create'); + } else if (this.type === PRType.LocalPullRequest) { + this.tooltip = vscode.l10n.t('Pull requests for branches you have locally'); + } else if (this.type === PRType.All) { + this.tooltip = vscode.l10n.t('All open pull requests in the current repository'); + } else if (_categoryQuery) { + this.tooltip = new vscode.MarkdownString(vscode.l10n.t('Pull requests for query: `{0}`', _categoryQuery)); + } else { + this.tooltip = this.label; + } } - private updateQuery(queries: { label: string; query: string }[], queryToUpdate: { label: string; query: string }) { - for (const query of queries) { - if (query.label === queryToUpdate.label) { - query.query = queryToUpdate.query; - return; - } - } + get repo(): RemoteInfo | undefined { + return this._repo; } - private async openSettings(config: vscode.WorkspaceConfiguration, inspect: QueryInspect | undefined) { - let command: string; - if (inspect?.workspaceValue) { - command = 'workbench.action.openWorkspaceSettingsFile'; - } else { - const value = config.get<{ label: string; query: string }[]>(QUERIES); - if (inspect?.defaultValue && JSON.stringify(inspect?.defaultValue) === JSON.stringify(value)) { - await config.update(QUERIES, inspect.defaultValue, vscode.ConfigurationTarget.Global); - } - command = 'workbench.action.openSettingsJson'; + private _getDescription(): string | undefined { + if (!this.isCopilot || !this._repo) { + return undefined; } - await vscode.commands.executeCommand(command); - const editor = vscode.window.activeTextEditor; - if (editor) { - const text = editor.document.getText(); - const search = text.search(this.label!); - if (search >= 0) { - const position = editor.document.positionAt(search); - editor.revealRange(new vscode.Range(position, position)); - editor.selection = new vscode.Selection(position, position); + const counts = this._prsTreeModel.getCopilotCounts(this._repo.owner, this._repo.repositoryName); + if (counts.total === 0) { + return undefined; + } else if (counts.error > 0) { + if (counts.inProgress > 0) { + return vscode.l10n.t('{0} in progress, {1} with errors', counts.inProgress, counts.error); + } else { + return vscode.l10n.t('{0} with errors', counts.error); } + } else if (counts.inProgress > 0) { + return vscode.l10n.t('{0} in progress', counts.inProgress); + } else { + return vscode.l10n.t('done working on {0}', counts.total); } } public async expandPullRequest(pullRequest: PullRequestModel, retry: boolean = true): Promise { - if (!this.children && retry) { + if (!this._children && retry) { await this.getChildren(); retry = false; } - if (this.children) { - for (const child of this.children) { + if (this._children) { + for (const child of this._children) { if (child instanceof PRNode) { if (child.pullRequestModel.equals(pullRequest)) { this.reveal(child, { expand: true, select: true }); @@ -265,43 +242,11 @@ export class CategoryTreeNode extends TreeNode implements vscode.TreeItem { return false; } - async editQuery() { - const config = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE); - const inspect = config.inspect<{ label: string; query: string }[]>(QUERIES); - - const inputBox = vscode.window.createQuickPick(); - inputBox.title = vscode.l10n.t('Edit Pull Request Query "{0}"', this.label ?? ''); - inputBox.value = this._categoryQuery ?? ''; - inputBox.items = [{ iconPath: new vscode.ThemeIcon('pencil'), label: vscode.l10n.t('Save edits'), alwaysShow: true }, { iconPath: new vscode.ThemeIcon('add'), label: vscode.l10n.t('Add new query'), alwaysShow: true }, { iconPath: new vscode.ThemeIcon('settings'), label: vscode.l10n.t('Edit in settings.json'), alwaysShow: true }]; - inputBox.activeItems = []; - inputBox.selectedItems = []; - inputBox.onDidAccept(async () => { - inputBox.busy = true; - if (inputBox.selectedItems[0] === inputBox.items[0]) { - const newQuery = inputBox.value; - if (newQuery !== this._categoryQuery && this.label) { - if (inspect?.workspaceValue) { - this.updateQuery(inspect.workspaceValue, { label: this.label, query: newQuery }); - await config.update(QUERIES, inspect.workspaceValue, vscode.ConfigurationTarget.Workspace); - } else { - const value = config.get<{ label: string; query: string }[]>(QUERIES) ?? inspect!.defaultValue!; - this.updateQuery(value, { label: this.label, query: newQuery }); - await config.update(QUERIES, value, vscode.ConfigurationTarget.Global); - } - } - } else if (inputBox.selectedItems[0] === inputBox.items[1]) { - this.addNewQuery(config, inspect, inputBox.value); - } else if (inputBox.selectedItems[0] === inputBox.items[2]) { - this.openSettings(config, inspect); - } - inputBox.dispose(); - }); - inputBox.onDidHide(() => inputBox.dispose()); - inputBox.show(); - } - - override async getChildren(): Promise { - await super.getChildren(); + override async getChildren(shouldDispose: boolean = true): Promise { + await super.getChildren(shouldDispose); + if (!shouldDispose && this._children) { + return this._children; + } const isFirstLoad = !this._firstLoad; if (isFirstLoad) { this._firstLoad = this.doGetChildren(); @@ -323,7 +268,7 @@ export class CategoryTreeNode extends TreeNode implements vscode.TreeItem { if (this.type === PRType.LocalPullRequest) { try { this.prs.clear(); - (await this._prsTreeModel.getLocalPullRequests(this._folderRepoManager)).items.forEach(item => this.prs.set(item.id, item)); + (await this._prsTreeModel.getLocalPullRequests(this.folderRepoManager)).items.forEach(item => this.prs.set(item.id, item)); } catch (e) { vscode.window.showErrorMessage(vscode.l10n.t('Fetching local pull requests failed: {0}', formatError(e))); needLogin = e instanceof AuthenticationError; @@ -333,10 +278,10 @@ export class CategoryTreeNode extends TreeNode implements vscode.TreeItem { let response: ItemsResponseResult; switch (this.type) { case PRType.All: - response = await this._prsTreeModel.getAllPullRequests(this._folderRepoManager, fetchNextPage); + response = await this._prsTreeModel.getAllPullRequests(this.folderRepoManager, fetchNextPage); break; case PRType.Query: - response = await this._prsTreeModel.getPullRequestsForQuery(this._folderRepoManager, fetchNextPage, this._categoryQuery!); + response = await this._prsTreeModel.getPullRequestsForQuery(this.folderRepoManager, fetchNextPage, this._categoryQuery!); break; } if (!fetchNextPage) { @@ -346,23 +291,27 @@ export class CategoryTreeNode extends TreeNode implements vscode.TreeItem { hasMorePages = response.hasMorePages; hasUnsearchedRepositories = response.hasUnsearchedRepositories; } catch (e) { - const error = formatError(e); - const actions: string[] = []; - if (error.includes('Bad credentials')) { - actions.push(vscode.l10n.t('Login again')); - } - vscode.window.showErrorMessage(vscode.l10n.t('Fetching pull requests failed: {0}', formatError(e)), ...actions).then(action => { - if (action && action === actions[0]) { - this._folderRepoManager.credentialStore.recreate(vscode.l10n.t('Your login session is no longer valid.')); + if (this.isCopilot && (e.response.status === 422) && e.message.includes('the users do not exist')) { + // Skip it, it's copilot and the repo doesn't have copilot + } else { + const error = formatError(e); + const actions: string[] = []; + if (error.includes('Bad credentials')) { + actions.push(vscode.l10n.t('Login again')); } - }); - needLogin = e instanceof AuthenticationError; + vscode.window.showErrorMessage(vscode.l10n.t('Fetching pull requests failed: {0}', formatError(e)), ...actions).then(action => { + if (action && action === actions[0]) { + this.folderRepoManager.credentialStore.recreate(vscode.l10n.t('Your login session is no longer valid.')); + } + }); + needLogin = e instanceof AuthenticationError; + } } } if (this.prs.size > 0) { const nodes: (PRNode | PRCategoryActionNode)[] = Array.from(this.prs.values()).map( - prItem => new PRNode(this, this._folderRepoManager, prItem, this.type === PRType.LocalPullRequest, this._notificationProvider), + prItem => new PRNode(this, this.folderRepoManager, prItem, this.type === PRType.LocalPullRequest, this._notificationProvider, this._prsTreeModel), ); if (hasMorePages) { nodes.push(new PRCategoryActionNode(this, PRCategoryActionType.More, this)); @@ -370,18 +319,32 @@ export class CategoryTreeNode extends TreeNode implements vscode.TreeItem { nodes.push(new PRCategoryActionNode(this, PRCategoryActionType.TryOtherRemotes, this)); } - this.children = nodes; + this._children = nodes; return nodes; } else { const category = needLogin ? PRCategoryActionType.Login : PRCategoryActionType.Empty; const result = [new PRCategoryActionNode(this, category)]; - this.children = result; + this._children = result; return result; } } - getTreeItem(): vscode.TreeItem { + async getTreeItem(): Promise { + this.description = this._getDescription(); + if (!this._repo) { + this._repo = await extractRepoFromQuery(this.folderRepoManager, this._categoryQuery); + } + this.resourceUri = toQueryUri({ remote: this._repo, isCopilot: this.isCopilot }); + + // Update contextValue based on current notification state + if (this._categoryQuery) { + const hasNotifications = this.isCopilot && this._repo && this._prsTreeModel.getCopilotNotificationsCount(this._repo.owner, this._repo.repositoryName) > 0; + this.contextValue = this.isCopilot ? + (hasNotifications ? 'copilot-query-with-notifications' : 'copilot-query') : + 'query'; + } + return this; } } diff --git a/src/view/treeNodes/commitNode.ts b/src/view/treeNodes/commitNode.ts index 5b27bcdfca..6c4a4e202e 100644 --- a/src/view/treeNodes/commitNode.ts +++ b/src/view/treeNodes/commitNode.ts @@ -3,11 +3,10 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as path from 'path'; import * as vscode from 'vscode'; import { getGitChangeType } from '../../common/diffHunk'; import { FILE_LIST_LAYOUT, PR_SETTINGS_NAMESPACE } from '../../common/settingKeys'; -import { DataUri, toReviewUri } from '../../common/uri'; +import { DataUri, reviewPath, toReviewUri } from '../../common/uri'; import { dateFromNow } from '../../common/utils'; import { OctokitCommon } from '../../github/common'; import { FolderRepositoryManager } from '../../github/folderRepositoryManager'; @@ -23,7 +22,6 @@ export class CommitNode extends TreeNode implements vscode.TreeItem { public collapsibleState: vscode.TreeItemCollapsibleState; public iconPath: vscode.Uri | undefined; public contextValue?: string; - public description: string | undefined; constructor( parent: TreeNodeParent, @@ -58,7 +56,7 @@ export class CommitNode extends TreeNode implements vscode.TreeItem { const fileChangeNodes = fileChanges.map(change => { const fileName = change.filename!; - const uri = vscode.Uri.parse(path.posix.join(`commit~${this.commit.sha.substr(0, 8)}`, fileName)); + const uri = reviewPath(fileName, this.commit.sha); const changeModel = new GitFileChangeModel( this.pullRequestManager, this.pullRequest, @@ -108,7 +106,7 @@ export class CommitNode extends TreeNode implements vscode.TreeItem { dirNode.finalize(); if (dirNode.label === '') { // nothing on the root changed, pull children to parent - result.push(...dirNode.children); + result.push(...dirNode._children); } else { result.push(dirNode); } @@ -116,7 +114,7 @@ export class CommitNode extends TreeNode implements vscode.TreeItem { // flat view result = fileChangeNodes; } - this.children = result; + this._children = result; return result; } } diff --git a/src/view/treeNodes/commitsCategoryNode.ts b/src/view/treeNodes/commitsCategoryNode.ts index 7c39d1684b..7a0fb197ca 100644 --- a/src/view/treeNodes/commitsCategoryNode.ts +++ b/src/view/treeNodes/commitsCategoryNode.ts @@ -4,16 +4,19 @@ *--------------------------------------------------------------------------------------------*/ 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'; -import { CommitNode } from './commitNode'; -import { TreeNode, TreeNodeParent } from './treeNode'; 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,15 +28,19 @@ 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(() => { Logger.appendLine(`Review threads have changed, refreshing Commits node`, PR_TREE); this.refresh(this); })); - this.childrenDisposables.push(this._pr.onDidChangeComments(() => { - Logger.appendLine(`Comments have changed, refreshing Commits node`, PR_TREE); - this.refresh(this); + this.childrenDisposables.push(this._pr.onDidChange(e => { + if (e.comments) { + Logger.appendLine(`Comments have changed, refreshing Commits node`, PR_TREE); + this.refresh(this); + } })); } @@ -46,11 +53,11 @@ export class CommitsNode extends TreeNode implements vscode.TreeItem { try { Logger.appendLine(`Getting children for Commits node`, PR_TREE); const commits = await this._pr.getCommits(); - this.children = commits.map( + this._children = commits.map( (commit, index) => new CommitNode(this, this._folderRepoManager, this._pr, commit, (index === commits.length - 1) && (this._folderRepoManager.repository.state.HEAD?.commit === commit.sha)), ); Logger.appendLine(`Got all children for Commits node`, PR_TREE); - return this.children; + return this._children; } catch (e) { return []; } diff --git a/src/view/treeNodes/directoryTreeNode.ts b/src/view/treeNodes/directoryTreeNode.ts index 78179fdbb6..97098a15ca 100644 --- a/src/view/treeNodes/directoryTreeNode.ts +++ b/src/view/treeNodes/directoryTreeNode.ts @@ -9,7 +9,7 @@ import { TreeNode, TreeNodeParent } from './treeNode'; export class DirectoryTreeNode extends TreeNode implements vscode.TreeItem { public collapsibleState: vscode.TreeItemCollapsibleState; - public override children: (RemoteFileChangeNode | InMemFileChangeNode | GitFileChangeNode | DirectoryTreeNode)[] = []; + public override _children: (RemoteFileChangeNode | InMemFileChangeNode | GitFileChangeNode | DirectoryTreeNode)[] = []; private pathToChild: Map = new Map(); public checkboxState?: { state: vscode.TreeItemCheckboxState, tooltip: string, accessibilityInformation: vscode.AccessibilityInformation }; @@ -20,7 +20,7 @@ export class DirectoryTreeNode extends TreeNode implements vscode.TreeItem { } override async getChildren(): Promise { - return this.children; + return this._children; } public finalize(): void { @@ -29,11 +29,11 @@ export class DirectoryTreeNode extends TreeNode implements vscode.TreeItem { } private trimTree(): void { - if (this.children.length === 0) { + if (this._children.length === 0) { return; } - this.children.forEach(n => { + this._children.forEach(n => { if (n instanceof DirectoryTreeNode) { n.trimTree(); // recursive } @@ -46,10 +46,10 @@ export class DirectoryTreeNode extends TreeNode implements vscode.TreeItem { // becomes: // - a/b // - c - if (this.children.length !== 1) { + if (this._children.length !== 1) { return; } - const child = this.children[0]; + const child = this._children[0]; if (!(child instanceof DirectoryTreeNode)) { return; } @@ -59,12 +59,12 @@ export class DirectoryTreeNode extends TreeNode implements vscode.TreeItem { if (this.label.startsWith('/')) { this.label = this.label.substr(1); } - this.children = child.children; - this.children.forEach(child => { child.parent = this; }); + this._children = child._children; + this._children.forEach(child => { child.parent = this; }); } private sort(): void { - if (this.children.length <= 1) { + if (this._children.length <= 1) { return; } @@ -72,7 +72,7 @@ export class DirectoryTreeNode extends TreeNode implements vscode.TreeItem { const files: (RemoteFileChangeNode | InMemFileChangeNode | GitFileChangeNode)[] = []; // process directory - this.children.forEach(node => { + this._children.forEach(node => { if (node instanceof DirectoryTreeNode) { node.sort(); // recc dirs.push(node); @@ -86,7 +86,7 @@ export class DirectoryTreeNode extends TreeNode implements vscode.TreeItem { dirs.sort((a, b) => (a.label! < b.label! ? -1 : 1)); files.sort((a, b) => (a.label! < b.label! ? -1 : 1)); - this.children = [...dirs, ...files]; + this._children = [...dirs, ...files]; } public addFile(file: GitFileChangeNode | RemoteFileChangeNode | InMemFileChangeNode): void { @@ -101,7 +101,7 @@ export class DirectoryTreeNode extends TreeNode implements vscode.TreeItem { if (paths.length === 1) { file.parent = this; - this.children.push(file); + this._children.push(file); return; } @@ -112,14 +112,14 @@ export class DirectoryTreeNode extends TreeNode implements vscode.TreeItem { if (!node) { node = new DirectoryTreeNode(this, dir); this.pathToChild.set(dir, node); - this.children.push(node); + this._children.push(node); } node.addPathRecc(tail, file); } public allChildrenViewed(): boolean { - for (const child of this.children) { + for (const child of this._children) { if (child instanceof DirectoryTreeNode) { if (!child.allChildrenViewed()) { return false; diff --git a/src/view/treeNodes/fileChangeNode.ts b/src/view/treeNodes/fileChangeNode.ts index 19633480ce..350471690b 100644 --- a/src/view/treeNodes/fileChangeNode.ts +++ b/src/view/treeNodes/fileChangeNode.ts @@ -57,7 +57,7 @@ async function openDiffCommand( return { command: 'vscode.diff', arguments: [parentURI, headURI, `${pathSegments[pathSegments.length - 1]} (Pull Request)`, opts], - title: 'Open Changed File in PR', + title: 'Open Changed File in pull request', }; } @@ -68,7 +68,7 @@ export class FileChangeNode extends TreeNode implements vscode.TreeItem { public iconPath?: | string | vscode.Uri - | { light: string | vscode.Uri; dark: string | vscode.Uri } + | { light: vscode.Uri; dark: vscode.Uri } | vscode.ThemeIcon; public fileChangeResourceUri: vscode.Uri; public contextValue: string; @@ -139,13 +139,14 @@ export class FileChangeNode extends TreeNode implements vscode.TreeItem { ); this.accessibilityInformation = { label: `${this.label} pull request diff`, role: 'link' }; + this.description = this._getDescription(); } get resourceUri(): vscode.Uri { return this.changeModel.filePath.with({ query: this.fileChangeResourceUri.query }); } - get description(): string | true { + protected _getDescription(): string | true { const layout = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(FILE_LIST_LAYOUT); if (layout === 'flat') { return true; @@ -221,7 +222,7 @@ export class FileChangeNode extends TreeNode implements vscode.TreeItem { * File change node whose content can not be resolved locally and we direct users to GitHub. */ export class RemoteFileChangeNode extends FileChangeNode implements vscode.TreeItem { - override get description(): string { + protected override _getDescription(): string { let description = vscode.workspace.asRelativePath(path.dirname(this.changeModel.fileName), false); if (description === '.') { description = ''; @@ -406,7 +407,6 @@ export class GitFileChangeNode extends FileChangeNode implements vscode.TreeItem * File change node whose content is resolved from GitHub. For files not yet associated with a pull request. */ export class GitHubFileChangeNode extends TreeNode implements vscode.TreeItem { - public description: string; public iconPath: vscode.ThemeIcon; public fileChangeResourceUri: vscode.Uri; public readonly tooltip: string; diff --git a/src/view/treeNodes/filesCategoryNode.ts b/src/view/treeNodes/filesCategoryNode.ts index 41c1767d39..ec414574b9 100644 --- a/src/view/treeNodes/filesCategoryNode.ts +++ b/src/view/treeNodes/filesCategoryNode.ts @@ -4,8 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { ViewedState } from '../../common/comment'; import Logger, { PR_TREE } from '../../common/logger'; -import { FILE_LIST_LAYOUT, PR_SETTINGS_NAMESPACE } from '../../common/settingKeys'; +import { FILE_LIST_LAYOUT, HIDE_VIEWED_FILES, PR_SETTINGS_NAMESPACE } from '../../common/settingKeys'; import { compareIgnoreCase } from '../../common/utils'; import { PullRequestModel } from '../../github/pullRequestModel'; import { ReviewModel } from '../reviewModel'; @@ -33,10 +34,22 @@ export class FilesCategoryNode extends TreeNode implements vscode.TreeItem { Logger.appendLine(`Review threads have changed, refreshing Files node`, PR_TREE); this.refresh(this); })); - this.childrenDisposables.push(_pullRequestModel.onDidChangeComments(() => { - Logger.appendLine(`Comments have changed, refreshing Files node`, PR_TREE); + this.childrenDisposables.push(_pullRequestModel.onDidChange(e => { + if (e.comments) { + Logger.appendLine(`Comments have changed, refreshing Files node`, PR_TREE); + this.refresh(this); + } + })); + this.childrenDisposables.push(_pullRequestModel.onDidChangeFileViewedState(() => { + Logger.appendLine(`File viewed state has changed, refreshing Files node`, PR_TREE); this.refresh(this); })); + this.childrenDisposables.push(vscode.workspace.onDidChangeConfiguration(e => { + if (e.affectsConfiguration(`${PR_SETTINGS_NAMESPACE}.${HIDE_VIEWED_FILES}`)) { + Logger.appendLine(`Hide viewed files setting has changed, refreshing Files node`, PR_TREE); + this.refresh(this); + } + })); } getTreeItem(): vscode.TreeItem { @@ -63,13 +76,23 @@ export class FilesCategoryNode extends TreeNode implements vscode.TreeItem { let nodes: TreeNode[]; const layout = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(FILE_LIST_LAYOUT); + const hideViewedFiles = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get(HIDE_VIEWED_FILES, false); + + // Filter files based on hideViewedFiles setting + const filesToShow = hideViewedFiles + ? this._reviewModel.localFileChanges.filter(f => f.changeModel.viewed !== ViewedState.VIEWED) + : this._reviewModel.localFileChanges; + + if (filesToShow.length === 0 && hideViewedFiles) { + return [new LabelOnlyNode(this, vscode.l10n.t('All files viewed'))]; + } const dirNode = new DirectoryTreeNode(this, ''); - this._reviewModel.localFileChanges.forEach(f => dirNode.addFile(f)); + filesToShow.forEach(f => dirNode.addFile(f)); dirNode.finalize(); if (dirNode.label === '') { // nothing on the root changed, pull children to parent - this.directories = dirNode.children; + this.directories = dirNode._children; } else { this.directories = [dirNode]; } @@ -77,12 +100,12 @@ export class FilesCategoryNode extends TreeNode implements vscode.TreeItem { if (layout === 'tree') { nodes = this.directories; } else { - const fileNodes = [...this._reviewModel.localFileChanges]; + const fileNodes = [...filesToShow]; fileNodes.sort((a, b) => compareIgnoreCase(a.fileChangeResourceUri.toString(), b.fileChangeResourceUri.toString())); nodes = fileNodes; } Logger.appendLine(`Got all children for Files node`, PR_TREE); - this.children = nodes; + this._children = nodes; return nodes; } } diff --git a/src/view/treeNodes/pullRequestNode.ts b/src/view/treeNodes/pullRequestNode.ts index ff5d50fbad..3d8f8337c1 100644 --- a/src/view/treeNodes/pullRequestNode.ts +++ b/src/view/treeNodes/pullRequestNode.ts @@ -4,20 +4,25 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { CategoryTreeNode } from './categoryNode'; import { Repository } from '../../api/api'; +import { COPILOT_ACCOUNTS } from '../../common/comment'; import { getCommentingRanges } from '../../common/commentingRanges'; import { InMemFileChange, SlimFileChange } from '../../common/file'; import Logger from '../../common/logger'; import { FILE_LIST_LAYOUT, PR_SETTINGS_NAMESPACE, SHOW_PULL_REQUEST_NUMBER_IN_TREE } from '../../common/settingKeys'; import { createPRNodeUri, DataUri, fromPRUri, Schemes } from '../../common/uri'; import { FolderRepositoryManager } from '../../github/folderRepositoryManager'; -import { NotificationProvider } from '../../github/notifications'; +import { CopilotWorkingStatus } from '../../github/githubRepository'; import { IResolvedPullRequestModel, PullRequestModel } from '../../github/pullRequestModel'; import { InMemFileChangeModel, RemoteFileChangeModel } from '../fileChangeModel'; import { getInMemPRFileSystemProvider, provideDocumentContentForChangeModel } from '../inMemPRContentProvider'; +import { getIconForeground, getListErrorForeground, getListWarningForeground, getNotebookStatusSuccessIconForeground } from '../theme'; import { DirectoryTreeNode } from './directoryTreeNode'; import { InMemFileChangeNode, RemoteFileChangeNode } from './fileChangeNode'; import { TreeNode, TreeNodeParent } from './treeNode'; +import { NotificationsManager } from '../../notifications/notificationsManager'; +import { PrsTreeModel } from '../prsTreeModel'; export class PRNode extends TreeNode implements vscode.CommentingRangeProvider2 { static ID = 'PRNode'; @@ -46,17 +51,21 @@ export class PRNode extends TreeNode implements vscode.CommentingRangeProvider2 private _folderReposManager: FolderRepositoryManager, public pullRequestModel: PullRequestModel, private _isLocal: boolean, - private _notificationProvider: NotificationProvider + private _notificationProvider: NotificationsManager, + private _prsTreeModel: PrsTreeModel, ) { super(parent); this.registerSinceReviewChange(); this.registerConfigurationChange(); - this._register(this.pullRequestModel.onDidInvalidate(() => this.refresh(this))); this._register(this._folderReposManager.onDidChangeActivePullRequest(e => { - if (e.new === this.pullRequestModel.number || e.old === this.pullRequestModel.number) { + if (e.new?.number === this.pullRequestModel.number || e.old?.number === this.pullRequestModel.number) { this.refresh(this); } })); + this._register(this._folderReposManager.themeWatcher.onDidChangeTheme(() => { + this.refresh(this); + })); + this.resolvePRCommentController(); } // #region Tree @@ -95,7 +104,7 @@ export class PRNode extends TreeNode implements vscode.CommentingRangeProvider2 dirNode.finalize(); if (dirNode.label === '') { // nothing on the root changed, pull children to parent - result.push(...dirNode.children); + result.push(...dirNode._children); } else { result.push(dirNode); } @@ -108,7 +117,7 @@ export class PRNode extends TreeNode implements vscode.CommentingRangeProvider2 this.reopenNewPrDiffs(this.pullRequestModel); } - this.children = result; + this._children = result; // Kick off review thread initialization but don't await it. // Events will be fired later that will cause the tree to update when this is ready. @@ -259,27 +268,74 @@ export class PRNode extends TreeNode implements vscode.CommentingRangeProvider2 }); } - async getTreeItem(): Promise { - const currentBranchIsForThisPR = this.pullRequestModel.equals(this._folderReposManager.activePullRequest); - - const { title, number, author, isDraft, html_url } = this.pullRequestModel; - const labelTitle = this.pullRequestModel.title.length > 50 ? `${this.pullRequestModel.title.substring(0, 50)}...` : this.pullRequestModel.title; - const { login } = author; + private async _getIcon(): Promise { + const copilotWorkingStatus = await this.pullRequestModel.copilotWorkingStatus(); + const theme = this._folderReposManager.themeWatcher.themeData; + if (copilotWorkingStatus === CopilotWorkingStatus.NotCopilotIssue) { + return (await DataUri.avatarCirclesAsImageDataUris(this._folderReposManager.context, [this.pullRequestModel.author], 16, 16))[0] + ?? new vscode.ThemeIcon('github'); + } + switch (copilotWorkingStatus) { + case CopilotWorkingStatus.InProgress: + return { + light: DataUri.copilotInProgressAsImageDataURI(getIconForeground(theme, 'light'), getListWarningForeground(theme, 'light')), + dark: DataUri.copilotInProgressAsImageDataURI(getIconForeground(theme, 'dark'), getListWarningForeground(theme, 'dark')) + }; + case CopilotWorkingStatus.Done: + return { + light: DataUri.copilotSuccessAsImageDataURI(getIconForeground(theme, 'light'), getNotebookStatusSuccessIconForeground(theme, 'light')), + dark: DataUri.copilotSuccessAsImageDataURI(getIconForeground(theme, 'dark'), getNotebookStatusSuccessIconForeground(theme, 'dark')) + }; + case CopilotWorkingStatus.Error: + return { + light: DataUri.copilotErrorAsImageDataURI(getIconForeground(theme, 'light'), getListErrorForeground(theme, 'light')), + dark: DataUri.copilotErrorAsImageDataURI(getIconForeground(theme, 'dark'), getListErrorForeground(theme, 'dark')) + }; + default: + return (await DataUri.avatarCirclesAsImageDataUris(this._folderReposManager.context, [this.pullRequestModel.author], 16, 16))[0] + ?? new vscode.ThemeIcon('github'); + } + } - const hasNotification = this._notificationProvider.hasNotification(this.pullRequestModel); + private _getLabel(): string { + const currentBranchIsForThisPR = this.pullRequestModel.equals(this._folderReposManager.activePullRequest); + const { title, number, author, isDraft } = this.pullRequestModel; + let label = ''; - const formattedPRNumber = number.toString(); - let labelPrefix = currentBranchIsForThisPR ? '✓ ' : ''; + if (currentBranchIsForThisPR) { + label += '$(check) '; + } if ( vscode.workspace .getConfiguration(PR_SETTINGS_NAMESPACE) .get(SHOW_PULL_REQUEST_NUMBER_IN_TREE, false) ) { - labelPrefix += `#${formattedPRNumber}: `; + label += `#${number}: `; } - const label = `${labelPrefix}${isDraft ? '[DRAFT] ' : ''}${labelTitle}`; + 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, '\\$&'); + + 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'), @@ -288,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, @@ -298,12 +354,11 @@ export class PRNode extends TreeNode implements vscode.CommentingRangeProvider2 (currentBranchIsForThisPR ? ':active' : ':nonactive') + (hasNotification ? ':notification' : '') + (((this.pullRequestModel.item.isRemoteHeadDeleted && !this._isLocal) || !this._folderReposManager.isPullRequestAssociatedWithOpenRepository(this.pullRequestModel)) ? '' : ':hasHeadRef'), - iconPath: (await DataUri.avatarCirclesAsImageDataUris(this._folderReposManager.context, [this.pullRequestModel.author], 16, 16))[0] - ?? new vscode.ThemeIcon('github'), + 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), + resourceUri: createPRNodeUri(this.pullRequestModel, this.parent instanceof CategoryTreeNode && this.parent.isCopilot ? true : undefined), command }; } diff --git a/src/view/treeNodes/repositoryChangesNode.ts b/src/view/treeNodes/repositoryChangesNode.ts index 9dbf131abf..f633f21a19 100644 --- a/src/view/treeNodes/repositoryChangesNode.ts +++ b/src/view/treeNodes/repositoryChangesNode.ts @@ -23,7 +23,6 @@ export class RepositoryChangesNode extends TreeNode implements vscode.TreeItem { public contextValue?: string; public tooltip: string; public iconPath: vscode.ThemeIcon | vscode.Uri | undefined; - public description?: string; readonly collapsibleState = vscode.TreeItemCollapsibleState.Expanded; private isLocal: boolean; public readonly repository: Repository; @@ -71,8 +70,10 @@ export class RepositoryChangesNode extends TreeNode implements vscode.TreeItem { this.revealActiveEditorInTree(activeEditorUri); })); - this._register(this.pullRequestModel.onDidInvalidate(() => { - this.refresh(); + this._register(this.pullRequestModel.onDidChange(e => { + if (e.title || e.state) { + this.refresh(); + } })); } @@ -96,8 +97,8 @@ export class RepositoryChangesNode extends TreeNode implements vscode.TreeItem { this.pullRequestModel, ); } - this.children = [this._filesCategoryNode, this._commitsCategoryNode]; - return this.children; + this._children = [this._filesCategoryNode, this._commitsCategoryNode]; + return this._children; } private setLabel() { diff --git a/src/view/treeNodes/treeNode.ts b/src/view/treeNodes/treeNode.ts index 8ff18f7b5c..399cbc1dca 100644 --- a/src/view/treeNodes/treeNode.ts +++ b/src/view/treeNodes/treeNode.ts @@ -10,18 +10,19 @@ import Logger from '../../common/logger'; export interface BaseTreeNode { reveal(element: TreeNode, options?: { select?: boolean; focus?: boolean; expand?: boolean | number }): Thenable; refresh(treeNode?: TreeNode): void; - children: TreeNode[] | undefined; + children: readonly TreeNode[] | undefined; view: vscode.TreeView; } export type TreeNodeParent = TreeNode | BaseTreeNode; export abstract class TreeNode extends Disposable { - protected children: TreeNode[] | undefined; + protected _children: TreeNode[] | undefined; childrenDisposables: vscode.Disposable[] = []; label?: string; accessibilityInformation?: vscode.AccessibilityInformation; id?: string; + description?: string | boolean; constructor(public parent: TreeNodeParent) { super(); @@ -34,6 +35,13 @@ export abstract class TreeNode extends Disposable { } } + get children(): readonly TreeNode[] | undefined { + if (this._children && this._children.length) { + return this._children; + } + return undefined; + } + async reveal( treeNode: TreeNode, options?: { select?: boolean; focus?: boolean; expand?: boolean | number }, @@ -50,15 +58,15 @@ export abstract class TreeNode extends Disposable { } async cachedChildren(): Promise { - if (this.children && this.children.length) { - return this.children; + if (this._children && this._children.length) { + return this._children; } return this.getChildren(); } async getChildren(shouldDispose: boolean = true): Promise { - if (this.children && this.children.length && shouldDispose) { - disposeAll(this.children); + if (this._children && this._children.length && shouldDispose) { + disposeAll(this._children); } return []; } diff --git a/src/view/treeNodes/workspaceFolderNode.ts b/src/view/treeNodes/workspaceFolderNode.ts index 228685a400..7963f3b3ee 100644 --- a/src/view/treeNodes/workspaceFolderNode.ts +++ b/src/view/treeNodes/workspaceFolderNode.ts @@ -7,13 +7,14 @@ import * as path from 'path'; import * as vscode from 'vscode'; import { PR_SETTINGS_NAMESPACE, QUERIES } from '../../common/settingKeys'; import { ITelemetry } from '../../common/telemetry'; +import { CopilotRemoteAgentManager } from '../../github/copilotRemoteAgent'; import { FolderRepositoryManager } from '../../github/folderRepositoryManager'; import { PRType } from '../../github/interface'; -import { NotificationProvider } from '../../github/notifications'; import { PullRequestModel } from '../../github/pullRequestModel'; import { PrsTreeModel } from '../prsTreeModel'; -import { CategoryTreeNode } from './categoryNode'; +import { CategoryTreeNode, isAllQuery, isLocalQuery } from './categoryNode'; import { TreeNode, TreeNodeParent } from './treeNode'; +import { NotificationsManager } from '../../notifications/notificationsManager'; export interface IQueryInfo { label: string; @@ -21,18 +22,19 @@ export interface IQueryInfo { } export class WorkspaceFolderNode extends TreeNode implements vscode.TreeItem { - protected override children: CategoryTreeNode[] | undefined = undefined; + protected override _children: CategoryTreeNode[] | undefined = undefined; public collapsibleState: vscode.TreeItemCollapsibleState; - public iconPath?: { light: string | vscode.Uri; dark: string | vscode.Uri }; + public iconPath?: { light: vscode.Uri; dark: vscode.Uri }; constructor( parent: TreeNodeParent, uri: vscode.Uri, public readonly folderManager: FolderRepositoryManager, private telemetry: ITelemetry, - private notificationProvider: NotificationProvider, + private notificationProvider: NotificationsManager, private context: vscode.ExtensionContext, private readonly _prsTreeModel: PrsTreeModel, + private readonly _copilotMananger: CopilotRemoteAgentManager ) { super(parent); this.collapsibleState = vscode.TreeItemCollapsibleState.Expanded; @@ -41,8 +43,8 @@ export class WorkspaceFolderNode extends TreeNode implements vscode.TreeItem { } public async expandPullRequest(pullRequest: PullRequestModel): Promise { - if (this.children) { - for (const child of this.children) { + if (this._children) { + for (const child of this._children) { if (child.type === PRType.All) { return child.expandPullRequest(pullRequest); } @@ -51,40 +53,46 @@ export class WorkspaceFolderNode extends TreeNode implements vscode.TreeItem { return false; } - private static getQueries(folderManager: FolderRepositoryManager): IQueryInfo[] { - return ( - vscode.workspace - .getConfiguration(PR_SETTINGS_NAMESPACE, folderManager.repository.rootUri) - .get(QUERIES) || [] - ); + private static async getQueries(folderManager: FolderRepositoryManager): Promise { + const configuration = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE, folderManager.repository.rootUri); + const queries = (configuration.get(QUERIES) ?? []); + return queries; } getTreeItem(): vscode.TreeItem { return this; } - override async getChildren(): Promise { - super.getChildren(); - this.children = WorkspaceFolderNode.getCategoryTreeNodes(this.folderManager, this.telemetry, this, this.notificationProvider, this.context, this._prsTreeModel); - return this.children; + override async getChildren(shouldDispose: boolean = true): Promise { + super.getChildren(shouldDispose); + if (!shouldDispose && this._children) { + return this._children; + } + this._children = await WorkspaceFolderNode.getCategoryTreeNodes(this.folderManager, this.telemetry, this, this.notificationProvider, this.context, this._prsTreeModel, this._copilotMananger); + return this._children; } - public static getCategoryTreeNodes( + public static async getCategoryTreeNodes( folderManager: FolderRepositoryManager, telemetry: ITelemetry, parent: TreeNodeParent, - notificationProvider: NotificationProvider, + notificationProvider: NotificationsManager, context: vscode.ExtensionContext, prsTreeModel: PrsTreeModel, + copilotManager: CopilotRemoteAgentManager ) { - const queryCategories = WorkspaceFolderNode.getQueries(folderManager).map( - queryInfo => - new CategoryTreeNode(parent, folderManager, telemetry, PRType.Query, notificationProvider, prsTreeModel, queryInfo.label, queryInfo.query), - ); - return [ - new CategoryTreeNode(parent, folderManager, telemetry, PRType.LocalPullRequest, notificationProvider, prsTreeModel), - ...queryCategories, - new CategoryTreeNode(parent, folderManager, telemetry, PRType.All, notificationProvider, prsTreeModel), - ]; + const queries = await WorkspaceFolderNode.getQueries(folderManager); + const queryCategories: Map = new Map(); + for (const queryInfo of queries) { + if (isLocalQuery(queryInfo)) { + queryCategories.set(queryInfo.label, new CategoryTreeNode(parent, folderManager, telemetry, PRType.LocalPullRequest, notificationProvider, prsTreeModel, copilotManager)); + } else if (isAllQuery(queryInfo)) { + queryCategories.set(queryInfo.label, new CategoryTreeNode(parent, folderManager, telemetry, PRType.All, notificationProvider, prsTreeModel, copilotManager)); + } else { + queryCategories.set(queryInfo.label, new CategoryTreeNode(parent, folderManager, telemetry, PRType.Query, notificationProvider, prsTreeModel, copilotManager, queryInfo.label, queryInfo.query)); + } + } + + return Array.from(queryCategories.values()); } } diff --git a/src/view/webviewViewCoordinator.ts b/src/view/webviewViewCoordinator.ts index 9b4f608eab..da40fc4b3e 100644 --- a/src/view/webviewViewCoordinator.ts +++ b/src/view/webviewViewCoordinator.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as vscode from 'vscode'; +import { ReviewManager } from './reviewManager'; import { addDisposable, Disposable, disposeAll } from '../common/lifecycle'; import { PullRequestViewProvider } from '../github/activityBarViewProvider'; import { FolderRepositoryManager } from '../github/folderRepositoryManager'; import { PullRequestModel } from '../github/pullRequestModel'; -import { ReviewManager } from './reviewManager'; export class WebviewViewCoordinator extends Disposable { private _webviewViewProvider?: PullRequestViewProvider; diff --git a/tsconfig.eslint.json b/tsconfig.eslint.json deleted file mode 100644 index 186ca56d57..0000000000 --- a/tsconfig.eslint.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "extends": "./tsconfig.base.json", - "exclude": ["node_modules"] -} diff --git a/tsconfig.json b/tsconfig.json index b60d27e54d..c84f7118b4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -12,7 +12,6 @@ }, "exclude": [ "node_modules", - "src/test", "webviews" ] } \ No newline at end of file diff --git a/tsconfig.scripts.json b/tsconfig.scripts.json new file mode 100644 index 0000000000..09a50b720f --- /dev/null +++ b/tsconfig.scripts.json @@ -0,0 +1,14 @@ +{ + "extends": "./tsconfig.base.json", + "compilerOptions": { + "module": "commonjs", + "moduleResolution": "node", + "types": [ + "node" + ], + "noEmit": true + }, + "include": [ + "build/**/*.ts" + ] +} \ No newline at end of file diff --git a/webpack.config.js b/webpack.config.js index cd9b5bb307..250d4933f8 100644 --- a/webpack.config.js +++ b/webpack.config.js @@ -58,11 +58,6 @@ async function getWebviewConfig(mode, env, entry) { }), new ForkTsCheckerPlugin({ async: false, - eslint: { - enabled: true, - files: path.join(basePath, '**', '*.ts'), - options: { cache: true, configFile: path.join(__dirname, '.eslintrc.webviews.json') }, - }, formatter: 'basic', typescript: { configFile: path.join(__dirname, 'tsconfig.webviews.json'), @@ -79,6 +74,9 @@ async function getWebviewConfig(mode, env, entry) { output: { filename: '[name].js', path: path.resolve(__dirname, 'dist'), + // Use absolute paths (file:///) in source maps instead of the default webpack:// scheme + devtoolModuleFilenameTemplate: info => 'file:///' + info.absoluteResourcePath.replace(/\\/g, '/'), + devtoolFallbackModuleFilenameTemplate: 'file:///[absolute-resource-path]' }, optimization: { minimizer: [ @@ -107,7 +105,7 @@ async function getWebviewConfig(mode, env, entry) { rules: [ { exclude: /node_modules/, - include: [basePath, path.join(__dirname, 'src')], + include: [basePath, path.join(__dirname, 'src'), path.join(__dirname, 'common')], test: /\.tsx?$/, use: env.esbuild ? { @@ -134,6 +132,10 @@ async function getWebviewConfig(mode, env, entry) { test: /\.svg/, use: ['svg-inline-loader'], }, + { + test: /\.ttf$/, + type: 'asset/resource' + }, ], }, resolve: { @@ -157,34 +159,59 @@ async function getWebviewConfig(mode, env, entry) { */ async function getExtensionConfig(target, mode, env) { const basePath = path.join(__dirname, 'src'); + const glob = require('glob'); /** * @type WebpackConfig['plugins'] | any */ const plugins = [ - new webpack.optimize.LimitChunkCountPlugin({ - maxChunks: 1 - }), new ForkTsCheckerPlugin({ async: false, - eslint: { - enabled: true, - files: path.join(basePath, '**', '*.ts'), - options: { - cache: true, - configFile: path.join( - __dirname, - target === 'webworker' ? '.eslintrc.browser.json' : '.eslintrc.node.json', - ), - }, - }, formatter: 'basic', typescript: { configFile: path.join(__dirname, target === 'webworker' ? 'tsconfig.browser.json' : 'tsconfig.json'), }, - }) + }), + new webpack.ContextReplacementPlugin(/mocha/, /^$/) ]; + // Add fixtures copying plugin for node target (which has individual test files) + if (target === 'node') { + const fs = require('fs'); + const srcRoot = 'src'; + class CopyFixturesPlugin { + apply(compiler) { + compiler.hooks.afterEmit.tap('CopyFixturesPlugin', () => { + this.copyFixtures(srcRoot, compiler.options.output.path); + }); + } + + copyFixtures(inputDir, outputDir) { + try { + const files = fs.readdirSync(inputDir); + for (const file of files) { + const filePath = path.join(inputDir, file); + const stats = fs.statSync(filePath); + if (stats.isDirectory()) { + if (file === 'fixtures') { + const outputFilePath = path.join(outputDir, inputDir.substring(srcRoot.length), file); + const inputFilePath = path.join(inputDir, file); + fs.cpSync(inputFilePath, outputFilePath, { recursive: true, force: true }); + } else { + this.copyFixtures(filePath, outputDir); + } + } + } + } catch (error) { + // Ignore errors during fixtures copying to not break the build + console.warn('Warning: Could not copy fixtures:', error.message); + } + } + } + + plugins.push(new CopyFixturesPlugin()); + } + if (target === 'webworker') { plugins.push(new webpack.ProvidePlugin({ process: path.join( @@ -193,13 +220,36 @@ async function getExtensionConfig(target, mode, env) { 'process', 'browser.js') })); + plugins.push(new webpack.ProvidePlugin({ + Buffer: ['buffer', 'Buffer'] + })); } const entry = { extension: './src/extension.ts', }; + + // Add test entry points if (target === 'webworker') { entry['test/index'] = './src/test/browser/index.ts'; + } else if (target === 'node') { + // Add main test runner + entry['test/index'] = './src/test/index.ts'; + + // Add individual test files as separate entry points + const testFiles = glob.sync('src/test/**/*.test.ts', { cwd: __dirname }); + testFiles.forEach(testFile => { + // Convert src/test/github/utils.test.ts -> test/github/utils.test + const entryName = testFile.replace('src/', '').replace('.ts', ''); + entry[entryName] = `./${testFile}`; + }); + } + + // Don't limit chunks for node target when we have individual test files + if (target !== 'node' || !('test/index' in entry && Object.keys(entry).some(key => key.endsWith('.test')))) { + plugins.unshift(new webpack.optimize.LimitChunkCountPlugin({ + maxChunks: 1 + })); } return { @@ -213,6 +263,9 @@ async function getExtensionConfig(target, mode, env) { libraryTarget: 'commonjs2', filename: '[name].js', chunkFilename: 'feature-[name].js', + // Use absolute paths (file:///) in source maps for easier debugging of tests & sources + devtoolModuleFilenameTemplate: info => 'file:///' + info.absoluteResourcePath.replace(/\\/g, '/'), + devtoolFallbackModuleFilenameTemplate: 'file:///[absolute-resource-path]', }, optimization: { minimizer: [ @@ -242,7 +295,7 @@ async function getExtensionConfig(target, mode, env) { rules: [ { exclude: /node_modules/, - include: path.join(__dirname, 'src'), + include: [path.join(__dirname, 'src'), path.join(__dirname, 'common')], test: /\.tsx?$/, use: env.esbuild ? { @@ -285,11 +338,7 @@ async function getExtensionConfig(target, mode, env) { exclude: /node_modules/, test: /\.(graphql|gql)$/, loader: 'graphql-tag/loader', - }, - // { - // test: /webview-*\.js/, - // use: 'raw-loader' - // }, + } ], }, resolve: { @@ -343,6 +392,7 @@ async function getExtensionConfig(target, mode, env) { '@opentelemetry/instrumentation': '@opentelemetry/instrumentation', '@azure/opentelemetry-instrumentation-azure-sdk': '@azure/opentelemetry-instrumentation-azure-sdk', 'fs': 'fs', + 'mocha': 'commonjs mocha', }, plugins: plugins, stats: { @@ -353,7 +403,7 @@ async function getExtensionConfig(target, mode, env) { errorsCount: true, warningsCount: true, timings: true, - }, + } }; } diff --git a/webviews/activityBarView/app.tsx b/webviews/activityBarView/app.tsx index 5d31ff9574..9d40442273 100644 --- a/webviews/activityBarView/app.tsx +++ b/webviews/activityBarView/app.tsx @@ -5,9 +5,9 @@ import React, { useContext, useEffect, useState } from 'react'; import { render } from 'react-dom'; +import { Overview } from './overview'; import { PullRequest } from '../../src/github/views'; import PullRequestContext from '../common/context'; -import { Overview } from './overview'; export function main() { render({pr => }, document.getElementById('app')); diff --git a/webviews/activityBarView/index.css b/webviews/activityBarView/index.css index 63783da3b9..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%; } @@ -104,7 +114,6 @@ form button { .select-control button, .branch-status-container button, input[type='submit'] { - min-height: 31px; white-space: normal; } @@ -185,37 +194,5 @@ button .icon svg { } button.split-left { - border-radius: 2px 0 0 2px; - flex-grow: 1; - overflow: hidden; - white-space: nowrap; - text-overflow: ellipsis; -} - - -.split { - width: 1px; height: 100%; - background-color: var(--vscode-button-background); - opacity: 0.5; -} - -button.split-right { - border-radius: 0 2px 2px 0; - cursor: pointer; - width: 24px; height: 28px; - position: relative; -} - -button.split-right:disabled { - cursor: default; -} - -button.split-right .icon { - pointer-events: none; - position: absolute; - top: 6px; right: 4px; -} - -button.split-right .icon svg path { - fill: unset; + display: block; } diff --git a/webviews/activityBarView/overview.tsx b/webviews/activityBarView/overview.tsx index f427a792ff..976a9d35fd 100644 --- a/webviews/activityBarView/overview.tsx +++ b/webviews/activityBarView/overview.tsx @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import React from 'react'; +import { ExitSection } from './exit'; import { PullRequest } from '../../src/github/views'; import { AddCommentSimple } from '../components/comment'; import { StatusChecksSection } from '../components/merge'; -import { ExitSection } from './exit'; export const Overview = (pr: PullRequest) => { return <> diff --git a/webviews/common/cache.ts b/webviews/common/cache.ts index 4ab516e9e0..f369e8770c 100644 --- a/webviews/common/cache.ts +++ b/webviews/common/cache.ts @@ -3,17 +3,17 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { PullRequest } from '../../src/github/views'; import { vscode } from './message'; +import { PullRequest } from '../../src/github/views'; export function getState(): PullRequest { return vscode.getState(); } -export function setState(pullRequest: PullRequest): void { +export function setState(pullRequest: PullRequest | undefined): void { const oldPullRequest = getState(); - if (oldPullRequest && oldPullRequest.number && oldPullRequest.number === pullRequest.number) { + if (oldPullRequest && oldPullRequest.number && oldPullRequest.number === pullRequest?.number) { pullRequest.pendingCommentText = oldPullRequest.pendingCommentText; } @@ -22,7 +22,7 @@ export function setState(pullRequest: PullRequest): void { } } -export function updateState(data: Partial): void { +export function updateState(data: Partial | undefined): void { const pullRequest = vscode.getState(); vscode.setState(Object.assign(pullRequest, data)); } diff --git a/webviews/common/common.css b/webviews/common/common.css index 87e42ed6b3..d5847177c3 100644 --- a/webviews/common/common.css +++ b/webviews/common/common.css @@ -34,7 +34,7 @@ input[type='submit'] { user-select: none; } -button:not(.icon-button), +button:not(.icon-button):not(.danger):not(.secondary), input[type='submit'] { background-color: var(--vscode-button-background); } @@ -45,6 +45,14 @@ input.select-left { button.select-right { border-radius: 0 2px 2px 0; + width: 24px; + position: relative; +} + +button.select-right span { + position: absolute; + top: 2px; + right: 4px; } button:focus, @@ -125,7 +133,8 @@ input[type='checkbox'] { outline: 1px solid var(--vscode-contrastActiveBorder); } -svg path { +:not(.copilot-icon) > svg path, +.copilot-icon svg path:first-of-type { fill: var(--vscode-foreground); } @@ -162,15 +171,18 @@ body img.avatar { } .icon-button:hover, +.title .icon-button:hover, +.title .icon-button:focus, .section .icon-button:hover, .section .icon-button:focus { background-color: var(--vscode-toolbar-hoverBackground); } .icon-button:focus, +.title .icon-button:focus, .section .icon-button:focus { outline: 1px solid var(--vscode-focusBorder); - outline-offset: unset; + outline-offset: 1px; } .label .icon-button:hover, @@ -227,6 +239,13 @@ body img.avatar { gap: 4px; } +.reviewer-icons [role='alert'] { + position: absolute; + width: 0; + height: 0; + overflow: hidden; +} + .push-right { margin-left: auto; } @@ -239,6 +258,9 @@ body img.avatar { .author-link { font-weight: 600; color: var(--vscode-editor-foreground); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } .status-item button { @@ -351,20 +373,36 @@ button.split-left { overflow: hidden; white-space: nowrap; text-overflow: ellipsis; + display: flex; } .split { - width: 1px; - height: 100%; background-color: var(--vscode-button-background); - opacity: 0.5; + border-top: 1px solid var(--vscode-button-border); + border-bottom: 1px solid var(--vscode-button-border); + padding: 4px 0; +} + +.split .separator { + height: 100%; + width: 1px; + background-color: var(--vscode-button-separator); +} + +.split.disabled { + opacity: 0.4; +} + +.split.secondary { + background-color: var(--vscode-button-secondaryBackground); + border-top: 1px solid var(--vscode-button-secondaryBorder); + border-bottom: 1px solid var(--vscode-button-secondaryBorder); } button.split-right { border-radius: 0 2px 2px 0; cursor: pointer; width: 24px; - height: 28px; position: relative; } @@ -375,13 +413,14 @@ button.split-right:disabled { button.split-right .icon { pointer-events: none; position: absolute; - top: 6px; + top: 4px; right: 4px; } button.split-right .icon svg path { fill: unset; } + button.input-box { display: block; height: 24px; @@ -412,16 +451,57 @@ button.input-box:focus { .dropdown-container { display: flex; - flex-grow: 1; min-width: 0; margin: 0; +} + +.dropdown-container.spreadable { + flex-grow: 1; width: 100%; } button.inlined-dropdown { width: 100%; max-width: 150px; - margin-right: 5px; + margin-right: 8px; display: inline-block; text-align: center; +} + +button.inlined-dropdown:last-child { + margin-right: 0; +} + +.spinner { + margin-top: 5px; + margin-left: 5px; +} + +.commit-spinner-inline { + margin-left: 8px; + display: inline-flex; + align-items: center; + vertical-align: middle; + grid-column: none; +} + +.commit-spinner-before { + margin-right: 6px; + display: inline-flex; + align-items: center; + vertical-align: middle; +} + +.loading { + animation: spinner-rotate 1s linear infinite; +} + +@keyframes spinner-rotate { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } } \ No newline at end of file diff --git a/webviews/common/context.tsx b/webviews/common/context.tsx index 69dcedf8a4..d5aa38643a 100644 --- a/webviews/common/context.tsx +++ b/webviews/common/context.tsx @@ -4,17 +4,18 @@ *--------------------------------------------------------------------------------------------*/ import { createContext } from 'react'; -import { IComment } from '../../src/common/comment'; -import { EventType, ReviewEvent, TimelineEvent } from '../../src/common/timelineEvent'; -import { IProjectItem, MergeMethod, ReadyForReview, ReviewState } from '../../src/github/interface'; -import { ChangeAssigneesReply, MergeArguments, MergeResult, ProjectItemsReply, PullRequest } from '../../src/github/views'; import { getState, setState, updateState } from './cache'; import { getMessageHandler, MessageHandler } from './message'; +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, ReadyForReviewReply, SubmitReviewReply } from '../../src/github/views'; export class PRContext { constructor( - public pr: PullRequest = getState(), - public onchange: ((ctx: PullRequest) => void) | null = null, + public pr: PullRequest | undefined = getState(), + public onchange: ((ctx: PullRequest | undefined) => void) | null = null, private _handler: MessageHandler | null = null, ) { if (!_handler) { @@ -32,10 +33,14 @@ export class PRContext { public checkout = () => this.postMessage({ command: 'pr.checkout' }); + public openChanges = (openToTheSide?: boolean) => this.postMessage({ command: 'pr.open-changes', args: { openToTheSide } }); + public copyPrLink = () => this.postMessage({ command: 'pr.copy-prlink' }); public copyVscodeDevLink = () => this.postMessage({ command: 'pr.copy-vscodedevlink' }); + public cancelCodingAgent = (event: TimelineEvent): Promise => this.postMessage({ command: 'pr.cancel-coding-agent', args: event }); + public exitReviewMode = async () => { if (!this.pr) { return; @@ -48,7 +53,17 @@ export class PRContext { public gotoChangesSinceReview = () => this.postMessage({ command: 'pr.gotoChangesSinceReview' }); - public refresh = () => this.postMessage({ command: 'pr.refresh' }); + public refresh = async () =>{ + if (this.pr) { + this.pr.busy = true; + } + this.updatePR(this.pr); + await this.postMessage({ command: 'pr.refresh' }); + if (this.pr) { + this.pr.busy = false; + } + this.updatePR(this.pr); + }; public checkMergeability = () => this.postMessage({ command: 'pr.checkMergeability' }); @@ -60,7 +75,7 @@ export class PRContext { public merge = async (args: MergeArguments): Promise => { const result: MergeResult = await this.postMessage({ command: 'pr.merge', args }); return result; - } + }; public openOnGitHub = () => this.postMessage({ command: 'pr.openOnGitHub' }); @@ -74,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 }); @@ -88,6 +105,9 @@ export class PRContext { public deleteComment = async (args: { id: number; pullRequestReviewId?: number }) => { await this.postMessage({ command: 'pr.delete-comment', args }); const { pr } = this; + if (!pr) { + throw new Error('Unexpectedly no pull request when trying to delete comment'); + } const { id, pullRequestReviewId } = args; if (!pullRequestReviewId) { this.updatePR({ @@ -105,11 +125,11 @@ export class PRContext { console.error('No comments to delete for review:', pullRequestReviewId, review); return; } - this.pr.events.splice(index, 1, { + pr.events.splice(index, 1, { ...review, comments: review.comments.filter(c => c.id !== id), }); - this.updatePR(this.pr); + this.updatePR(pr); }; public editComment = (args: { comment: IComment; text: string }) => @@ -125,23 +145,63 @@ export class PRContext { this.updatePR({ pendingCommentDrafts: pendingCommentDrafts }); }; - public requestChanges = async (body: string) => - this.appendReview(await this.postMessage({ command: 'pr.request-changes', args: body })); + private async submitReviewCommand(command: string, body: string) { + try { + const result: SubmitReviewReply = await this.postMessage({ command, args: body }); + return this.appendReview(result); + } catch (error) { + return this.updatePR({ busy: false }); + } + } + + public requestChanges = (body: string) => this.submitReviewCommand('pr.request-changes', body); - public approve = async (body: string) => - this.appendReview(await this.postMessage({ command: 'pr.approve', args: body })); + public approve = (body: string) => this.submitReviewCommand('pr.approve', body); - public submit = async (body: string) => - this.appendReview(await this.postMessage({ command: 'pr.submit', args: body })); + public submit = (body: string) => this.submitReviewCommand('pr.submit', body); + + public deleteReview = async () => { + try { + const result: DeleteReviewResult = await this.postMessage({ command: 'pr.delete-review' }); + + const state = this.pr; + const eventsWithoutPendingReview = state?.events.filter(event => + !(event.event === EventType.Reviewed && event.id === result.deletedReviewId) + ) ?? []; + + if (state && (eventsWithoutPendingReview.length < state.events.length)) { + // Update the PR state to reflect the deleted review + state.busy = false; + state.pendingCommentText = ''; + state.pendingCommentDrafts = {}; + // Remove the deleted review from events + state.events = eventsWithoutPendingReview; + this.updatePR(state); + } + return result; + } catch (error) { + return this.updatePR({ busy: false }); + } + }; public close = async (body?: string) => { + const { pr } = this; + if (!pr) { + throw new Error('Unexpectedly no pull request when trying to close'); + } try { - const result = await this.postMessage({ command: 'pr.close', args: body }); - const newComment = result.value; - newComment.event = EventType.Commented; + const result: CloseResult = await this.postMessage({ command: 'pr.close', args: body }); + let events: TimelineEvent[] = [...pr.events]; + if (result.commentEvent) { + events.push(result.commentEvent); + } + if (result.closeEvent) { + events.push(result.closeEvent); + } this.updatePR({ - events: [...this.pr.events, newComment], + events, pendingCommentText: '', + state: result.state }); } catch (_) { // Ignore @@ -149,8 +209,12 @@ export class PRContext { }; public removeLabel = async (label: string) => { + const { pr } = this; + if (!pr) { + throw new Error('Unexpectedly no pull request when trying to remove label'); + } await this.postMessage({ command: 'pr.remove-label', args: label }); - const labels = this.pr.labels.filter(r => r.name !== label); + const labels = pr.labels.filter(r => r.name !== label); this.updatePR({ labels }); }; @@ -158,71 +222,110 @@ export class PRContext { this.postMessage({ command: 'pr.apply-patch', args: { comment } }); }; - private appendReview({ event, reviewers }: { event?: ReviewEvent | TimelineEvent, reviewers?: ReviewState[] }) { - const state = this.pr; + private appendReview(reply: SubmitReviewReply) { + const { pr: state } = this; + if (!state) { + throw new Error('Unexpectedly no pull request when trying to append review'); + } + const { events, reviewers, reviewedEvent } = reply; state.busy = false; - if (!event) { + if (!events) { this.updatePR(state); return; } - const events = state.events.filter(e => e.event !== EventType.Reviewed || e.state?.toLowerCase() !== 'pending'); - events.forEach(event => { - if (event.event === EventType.Reviewed) { - event.comments.forEach(c => (c.isDraft = false)); - } - }); if (reviewers) { state.reviewers = reviewers; } - state.events = [...state.events.filter(e => (e.event === EventType.Reviewed ? e.state !== 'PENDING' : e)), event]; - if (event.event === EventType.Reviewed) { - state.currentUserReviewState = event.state; + state.events = events.length === 0 ? [...state.events, reviewedEvent] : events; + if (reviewedEvent.event === EventType.Reviewed) { + state.currentUserReviewState = reviewedEvent.state; } state.pendingCommentText = ''; state.pendingReviewType = undefined; 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) { + throw new Error('Unexpectedly no pull request when trying to re-request review'); + } const { reviewers } = await this.postMessage({ command: 'pr.re-request-review', args: reviewerId }); - const state = this.pr; state.reviewers = reviewers; this.updatePR(state); - } + }; public async updateAutoMerge({ autoMerge, autoMergeMethod }: { autoMerge?: boolean, autoMergeMethod?: MergeMethod }) { + const { pr: state } = this; + if (!state) { + throw new Error('Unexpectedly no pull request when trying to update auto merge'); + } const response: { autoMerge: boolean, autoMergeMethod?: MergeMethod } = await this.postMessage({ command: 'pr.update-automerge', args: { autoMerge, autoMergeMethod } }); - const state = this.pr; state.autoMerge = response.autoMerge; state.autoMergeMethod = response.autoMergeMethod; this.updatePR(state); } public updateBranch = async () => { + const { pr: state } = this; + if (!state) { + throw new Error('Unexpectedly no pull request when trying to update branch'); + } const result: Partial = await this.postMessage({ command: 'pr.update-branch' }); - const state = this.pr; state.events = result.events ?? state.events; state.mergeable = result.mergeable ?? state.mergeable; this.updatePR(state); - } + }; public dequeue = async () => { + const { pr: state } = this; + if (!state) { + throw new Error('Unexpectedly no pull request when trying to dequeue'); + } const isDequeued = await this.postMessage({ command: 'pr.dequeue' }); - const state = this.pr; if (isDequeued) { state.mergeQueueEntry = undefined; } this.updatePR(state); - } + }; public enqueue = async () => { + const { pr: state } = this; + if (!state) { + throw new Error('Unexpectedly no pull request when trying to enqueue'); + } const result = await this.postMessage({ command: 'pr.enqueue' }); - const state = this.pr; if (result.mergeQueueEntry) { state.mergeQueueEntry = result.mergeQueueEntry; } this.updatePR(state); - } + }; public openDiff = (comment: IComment) => this.postMessage({ command: 'pr.open-diff', args: { comment } }); @@ -240,7 +343,19 @@ export class PRContext { }); }; - setPR = (pr: PullRequest) => { + public openSessionLog = (link: SessionLinkInfo) => this.postMessage({ command: 'pr.open-session-log', args: { link } }); + + public openCommitChanges = async (commitSha: string) => { + this.updatePR({ loadingCommit: commitSha }); + try { + const args: OpenCommitChangesArgs = { commitSha }; + await this.postMessage({ command: 'pr.openCommitChanges', args }); + } finally { + this.updatePR({ loadingCommit: undefined }); + } + }; + + setPR = (pr: PullRequest | undefined) => { this.pr = pr; setState(this.pr); if (this.onchange) { @@ -249,9 +364,9 @@ export class PRContext { return this; }; - updatePR = (pr: Partial) => { + updatePR = (pr: Partial | undefined) => { updateState(pr); - this.pr = { ...this.pr, ...pr }; + this.pr = this.pr ? { ...this.pr, ...pr } : pr as PullRequest; if (this.onchange) { this.onchange(this.pr); } @@ -264,6 +379,9 @@ export class PRContext { handleMessage = (message: any) => { switch (message.command) { + case 'pr.clear': + this.setPR(undefined); + return; case 'pr.initialize': return this.setPR(message.pullrequest); case 'update-state': @@ -296,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/common/createContextNew.ts b/webviews/common/createContextNew.ts index 011195885a..bf27c8420f 100644 --- a/webviews/common/createContextNew.ts +++ b/webviews/common/createContextNew.ts @@ -4,10 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import { createContext } from 'react'; -import { ChooseBaseRemoteAndBranchResult, ChooseCompareRemoteAndBranchResult, ChooseRemoteAndBranchArgs, CreateParamsNew, CreatePullRequestNew, RemoteInfo, ScrollPosition, TitleAndDescriptionArgs, TitleAndDescriptionResult } from '../../common/views'; +import { getMessageHandler, MessageHandler, vscode } from './message'; +import { RemoteInfo } from '../../common/types'; +import { ChooseBaseRemoteAndBranchResult, ChooseCompareRemoteAndBranchResult, ChooseRemoteAndBranchArgs, CreateParamsNew, CreatePullRequestNew, ScrollPosition, TitleAndDescriptionArgs, TitleAndDescriptionResult } from '../../common/views'; import { compareIgnoreCase } from '../../src/common/utils'; import { PreReviewState } from '../../src/github/views'; -import { getMessageHandler, MessageHandler, vscode } from './message'; const defaultCreateParams: CreateParamsNew = { canModifyBranches: true, @@ -109,6 +110,8 @@ export class CreatePRContextNew { currentRemote, currentBranch }; + const startingBaseOwner = this.createParams.baseRemote?.owner; + const startingBaseRepo = this.createParams.baseRemote?.repositoryName; const response: ChooseBaseRemoteAndBranchResult = await this.postMessage({ command: 'pr.changeBaseRemoteAndBranch', args @@ -119,7 +122,7 @@ export class CreatePRContextNew { baseBranch: response.baseBranch, createError: '' }; - if ((this.createParams.baseRemote?.owner !== response.baseRemote.owner) || (this.createParams.baseRemote.repositoryName !== response.baseRemote.repositoryName)) { + if ((startingBaseOwner !== response.baseRemote.owner) || (startingBaseRepo !== response.baseRemote.repositoryName)) { updateValues.defaultMergeMethod = response.defaultMergeMethod; updateValues.allowAutoMerge = response.allowAutoMerge; updateValues.mergeMethodsAvailability = response.mergeMethodsAvailability; @@ -127,6 +130,8 @@ export class CreatePRContextNew { updateValues.baseHasMergeQueue = response.baseHasMergeQueue; if (!this.createParams.allowAutoMerge && updateValues.allowAutoMerge) { updateValues.autoMerge = this.createParams.isDraft ? false : updateValues.autoMergeDefault; + } else if (this.createParams.allowAutoMerge && !updateValues.allowAutoMerge) { + updateValues.autoMerge = false; } updateValues.defaultTitle = response.defaultTitle; if ((this.createParams.pendingTitle === undefined) || (this.createParams.pendingTitle === this.createParams.defaultTitle)) { @@ -172,9 +177,10 @@ export class CreatePRContextNew { command: 'pr.generateTitleAndDescription', args }); - const updateValues: { pendingTitle?: string, pendingDescription?: string } = {}; + const updateValues: { pendingTitle?: string, pendingDescription?: string, showTitleValidationError?: boolean } = {}; if (response.title) { updateValues.pendingTitle = response.title; + updateValues.showTitleValidationError = false; } if (response.description) { updateValues.pendingDescription = response.description; @@ -204,17 +210,17 @@ export class CreatePRContextNew { if (this._descriptionStack.length > 0) { this.updateState({ pendingDescription: this._descriptionStack.pop() }); } - } + }; public preReview = async (): Promise => { this.updateState({ reviewing: true }); const result: PreReviewState = await this.postMessage({ command: 'pr.preReview' }); this.updateState({ preReviewState: result, reviewing: false }); - } + }; public cancelPreReview = async (): Promise => { return this.postMessage({ command: 'pr.cancelPreReview' }); - } + }; public validate = (): boolean => { let isValid = true; diff --git a/webviews/common/errorBoundary.tsx b/webviews/common/errorBoundary.tsx index 879647d953..89e59961b8 100644 --- a/webviews/common/errorBoundary.tsx +++ b/webviews/common/errorBoundary.tsx @@ -5,23 +5,31 @@ import React from 'react'; -export class ErrorBoundary extends React.Component { - constructor(props) { +interface ErrorBoundaryProps { + children?: React.ReactNode; +} + +interface ErrorBoundaryState { + hasError: boolean; +} + +export class ErrorBoundary extends React.Component { + constructor(props: ErrorBoundaryProps) { super(props); this.state = { hasError: false }; } - static getDerivedStateFromError(_error) { + static getDerivedStateFromError(_error: unknown): ErrorBoundaryState { return { hasError: true }; } - override componentDidCatch(error, errorInfo) { - console.log(error); - console.log(errorInfo); + override componentDidCatch(error: unknown, errorInfo: React.ErrorInfo): void { + console.error(error); + console.error(errorInfo); } - override render() { - if ((this.state as any).hasError) { + override render(): React.ReactNode { + if (this.state.hasError) { return
Something went wrong.
; } diff --git a/webviews/common/label.tsx b/webviews/common/label.tsx index 9c1b442e76..eb6768b8ad 100644 --- a/webviews/common/label.tsx +++ b/webviews/common/label.tsx @@ -6,14 +6,14 @@ import React, { ReactNode } from 'react'; import { gitHubLabelColor } from '../../src/common/utils'; -import { ILabel } from '../../src/github/interface'; +import { DisplayLabel } from '../../src/github/views'; export interface LabelProps { - label: ILabel & { canDelete: boolean; isDarkTheme: boolean }; + label: DisplayLabel & { canDelete: boolean; isDarkTheme: boolean }; } -export function Label(label: ILabel & { canDelete: boolean; isDarkTheme: boolean; children?: ReactNode}) { - const { name, canDelete, color } = label; +export function Label(label: DisplayLabel & { canDelete: boolean; isDarkTheme: boolean; children?: ReactNode}) { + const { displayName, canDelete, color } = label; const labelColor = gitHubLabelColor(color, label.isDarkTheme, false); return (
- {name}{label.children} + {displayName}{label.children}
); } -export function LabelCreate(label: ILabel & { canDelete: boolean; isDarkTheme: boolean; children?: ReactNode}) { - const { name, color } = label; +export function LabelCreate(label: DisplayLabel & { canDelete: boolean; isDarkTheme: boolean; children?: ReactNode}) { + const { displayName, color } = label; const labelColor = gitHubLabelColor(color, label.isDarkTheme, false); return (
  • - {name}{label.children}
  • + {displayName}{label.children} ); } diff --git a/webviews/common/message.ts b/webviews/common/message.ts index 5c5640ea34..4d720f22f6 100644 --- a/webviews/common/message.ts +++ b/webviews/common/message.ts @@ -11,17 +11,19 @@ interface IRequestMessage { interface IReplyMessage { seq: string; - err: any; + err: string; + // eslint-disable-next-line rulesdir/no-any-except-union-method-signature res: any; } +// eslint-disable-next-line rulesdir/no-any-except-union-method-signature declare let acquireVsCodeApi: any; export const vscode = acquireVsCodeApi(); export class MessageHandler { private _commandHandler: ((message: any) => void) | null; private lastSentReq: number; - private pendingReplies: any; + private pendingReplies: Record void; reject: (reason?: string) => void }>; constructor(commandHandler: any) { this._commandHandler = commandHandler; this.lastSentReq = 0; diff --git a/webviews/components/automergeSelect.tsx b/webviews/components/automergeSelect.tsx index f50ecc69d5..5f05f68b9c 100644 --- a/webviews/components/automergeSelect.tsx +++ b/webviews/components/automergeSelect.tsx @@ -4,9 +4,9 @@ *--------------------------------------------------------------------------------------------*/ import * as React from 'react'; +import { MergeSelect } from './merge'; import { MergeMethod, MergeMethodsAvailability, MergeQueueEntry, MergeQueueState } from '../../src/github/interface'; import PullRequestContext from '../common/context'; -import { MergeSelect } from './merge'; const AutoMergeLabel = ({ busy, baseHasMergeQueue }: { busy: boolean, baseHasMergeQueue: boolean }) => { if (busy) { @@ -116,7 +116,7 @@ export const QueuedToMerge = ({ mergeQueueEntry }: { mergeQueueEntry: MergeQueue {message}
    - +
    ; }; diff --git a/webviews/components/comment.tsx b/webviews/components/comment.tsx index 13cdee7977..76fe901207 100644 --- a/webviews/components/comment.tsx +++ b/webviews/components/comment.tsx @@ -4,6 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; +import { ContextDropdown } from './contextDropdown'; +import { copyIcon, editIcon, quoteIcon, trashIcon } from './icon'; +import { nbsp, Spaced } from './space'; +import { Timestamp } from './timestamp'; +import { AuthorLink, Avatar } from './user'; import { IComment } from '../../src/common/comment'; import { CommentEvent, EventType, ReviewEvent } from '../../src/common/timelineEvent'; import { GithubItemStateEnum } from '../../src/github/interface'; @@ -12,16 +17,11 @@ import { ariaAnnouncementForReview } from '../common/aria'; import PullRequestContext from '../common/context'; import emitter from '../common/events'; import { useStateProp } from '../common/hooks'; -import { ContextDropdown } from './contextDropdown'; -import { commentIcon, deleteIcon, editIcon } from './icon'; -import { nbsp, Spaced } from './space'; -import { Timestamp } from './timestamp'; -import { AuthorLink, Avatar } from './user'; export type Props = { headerInEditMode?: boolean; isPRDescription?: boolean; - children?: any; + children?: React.ReactNode; comment: IComment | ReviewEvent | PullRequest | CommentEvent; allowEmpty?: boolean; }; @@ -36,17 +36,19 @@ const association = ({ authorAssociation }: ReviewEvent, format = (assoc: string export function CommentView(commentProps: Props) { const { isPRDescription, children, comment, headerInEditMode } = commentProps; const { bodyHTML, body } = comment; - const id = ('id' in comment) ? comment.id : -1; - const canEdit = ('canEdit' in comment) ? comment.canEdit : false; - const canDelete = ('canDelete' in comment) ? comment.canDelete : false; + + const id = (comment as Partial).id ?? -1; + const canEdit: boolean = !!(comment as Partial).canEdit; + const canDelete: boolean = !!(comment as Partial).canDelete; const pullRequestReviewId = (comment as IComment).pullRequestReviewId; const [bodyMd, setBodyMd] = useStateProp(body); const [bodyHTMLState, setBodyHtml] = useStateProp(bodyHTML); const { deleteComment, editComment, setDescription, pr } = useContext(PullRequestContext); - const currentDraft = pr.pendingCommentDrafts && pr.pendingCommentDrafts[id]; + 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 ? : <>, {}, [ @@ -55,7 +57,7 @@ export function CommentView(commentProps: Props) { key={`editComment${id}`} body={currentDraft || bodyMd} onCancel={() => { - if (pr.pendingCommentDrafts) { + if (pr?.pendingCommentDrafts) { delete pr.pendingCommentDrafts[id]; } setEditMode(false); @@ -86,15 +88,24 @@ export function CommentView(commentProps: Props) { onMouseLeave={() => setShowActionBar(false)} onFocus={() => setShowActionBar(true)} > - {ariaAnnouncement ?
    : null} + {ariaAnnouncement ?
    : null}
    + {commentUrl ? ( + + ) : null} {canEdit ? ( ) : null}
    @@ -114,7 +125,7 @@ export function CommentView(commentProps: Props) { comment={comment as IComment} bodyHTML={bodyHTMLState} body={bodyMd} - canApplyPatch={pr.isCurrentlyCheckedOut} + canApplyPatch={!!pr?.isCurrentlyCheckedOut} allowEmpty={!!commentProps.allowEmpty} specialDisplayBodyPostfix={(comment as IComment).specialDisplayBodyPostfix} /> @@ -126,30 +137,37 @@ export function CommentView(commentProps: Props) { type CommentBoxProps = { for: IComment | ReviewEvent | PullRequest | CommentEvent; header?: React.ReactChild; - onFocus?: any; - onMouseEnter?: any; - onMouseLeave?: any; - children?: any; + onFocus?: React.FocusEventHandler; + onMouseEnter?: React.MouseEventHandler; + onMouseLeave?: React.MouseEventHandler; + children?: React.ReactNode; }; function isReviewEvent(comment: IComment | ReviewEvent | PullRequest | CommentEvent): comment is ReviewEvent { return (comment as ReviewEvent).authorAssociation !== undefined; } +function isIComment(comment: any): comment is IComment { + return comment && typeof comment === 'object' && + typeof comment.body === 'string' && typeof comment.diffHunk === 'string'; +} + const DESCRIPTORS = { + REQUESTED: 'will review', PENDING: 'will review', COMMENTED: 'reviewed', CHANGES_REQUESTED: 'requested changes', APPROVED: 'approved', }; -const reviewDescriptor = (state: string) => DESCRIPTORS[state] || 'reviewed'; +const reviewDescriptor = (state: keyof typeof DESCRIPTORS) => DESCRIPTORS[state]; function CommentBox({ for: comment, onFocus, onMouseEnter, onMouseLeave, children }: CommentBoxProps) { - const htmlUrl = ('htmlUrl' in comment) ? comment.htmlUrl : (comment as PullRequest).url; - const isDraft = (comment as IComment).isDraft ?? (isReviewEvent(comment) && (comment.state?.toLocaleUpperCase() === 'PENDING')); - const author = ('user' in comment) ? comment.user! : (comment as PullRequest).author!; - const createdAt = ('createdAt' in comment) ? comment.createdAt : (comment as ReviewEvent).submittedAt; + const asNotPullRequest = comment as Partial; + const htmlUrl = asNotPullRequest.htmlUrl ?? (comment as PullRequest).url; + const isDraft = (isIComment(comment) && comment.isDraft) ?? (isReviewEvent(comment) && (comment.state?.toLocaleUpperCase() === 'PENDING')); + const author = asNotPullRequest.user ?? (comment as PullRequest).author; + const createdAt = (comment as IComment | CommentEvent | PullRequest).createdAt ?? (comment as ReviewEvent).submittedAt; return (
    @@ -239,7 +257,7 @@ function EditComment({ id, body, onCancel, onSave }: EditCommentProps) { const onInput = useCallback( e => { - draftComment.current.body = (e.target as any).value; + draftComment.current.body = e.target.value; draftComment.current.dirty = true; }, [draftComment], @@ -290,23 +308,61 @@ export const CommentBody = ({ comment, bodyHTML, body, canApplyPatch, allowEmpty
    {renderedBody} {applyPatchButton} - {specialDisplayBodyPostfix ?
    : null} + {specialDisplayBodyPostfix ?
    : null} {specialDisplayBodyPostfix ? {specialDisplayBodyPostfix} : null} + +
    + ); +}; + +type CommentReactionsProps = { + reactions?: { label: string; count: number; reactors: readonly string[] }[]; +}; + +const CommentReactions = ({ reactions }: CommentReactionsProps) => { + if (!Array.isArray(reactions) || reactions.length === 0) return null; + const filtered = reactions.filter(r => r.count > 0); + if (filtered.length === 0) return null; + return ( +
    + {filtered.map((reaction, idx) => { + const maxReactors = 10; + const reactors = reaction.reactors || []; + const displayReactors = reactors.slice(0, maxReactors); + const moreCount = reactors.length > maxReactors ? reactors.length - maxReactors : 0; + let title: string = ''; + if (displayReactors.length > 0) { + if (moreCount > 0) { + title = `${joinWithAnd(displayReactors)} and ${moreCount} more reacted with ${reaction.label}`; + } else { + title = `${joinWithAnd(displayReactors)} reacted with ${reaction.label}`; + } + } + return ( +
    + {reaction.label}{nbsp}{reaction.count > 1 ? {reaction.count} : null} +
    + ); + })}
    ); }; export function AddComment({ pendingCommentText, + isCopilotOnMyBehalf, state, hasWritePermission, isIssue, isAuthor, - isDraft, continueOnGitHub, currentUserReviewState, lastReviewType, busy, + hasReviewDraft, }: PullRequest) { const { updatePR, requestChanges, approve, close, openOnGitHub, submit } = useContext(PullRequestContext); const [isBusy, setBusy] = useState(false); @@ -320,7 +376,7 @@ export function AddComment({ textareaRef.current?.focus(); }); - const closeButton = e => { + const closeButton: React.MouseEventHandler = e => { e.preventDefault(); const { value } = textareaRef.current!; close(value); @@ -371,19 +427,30 @@ export function AddComment({ } : commentMethods(isIssue); + // Disable buttons when summary comment is empty AND there are no review comments + // Note: Approve button is allowed even with empty content and no pending review + const shouldDisableNonApproveButtons = !pendingCommentText?.trim() && !hasReviewDraft; + const shouldDisableApproveButton = false; // Approve is always allowed (when not busy) + return ( -
    } className="comment-form main-comment-form" onSubmit={() => submit(textareaRef.current?.value ?? '')}> + } className="comment-form main-comment-form" > +
    + {isAuthor ? null : ( )}
    @@ -260,7 +385,7 @@ const MergedEventView = (event: MergedEvent) => { return (
    - {mergeIcon} + {gitMergeIcon} {nbsp}
    @@ -275,12 +400,12 @@ const MergedEventView = (event: MergedEvent) => { into {event.mergeRef} {nbsp}
    -
    {pr.revertable ?
    : null} +
    ); }; @@ -295,8 +420,8 @@ const HeadDeleteEventView = (event: HeadRefDeleteEvent) => (
    deleted the {event.headRef} branch{nbsp}
    -
    +
    ); @@ -310,7 +435,7 @@ const CrossReferencedEventView = (event: CrossReferencedEvent) => {
    - linked #{source.number} {source.title} + linked #{source.number} {source.title} {nbsp} {event.willCloseTarget ? 'which will close this issue' : ''}
    @@ -327,8 +452,22 @@ function joinWithAnd(arr: JSX.Element[]): JSX.Element { return <>{arr.slice(0, -1).map(item => <>{item}, )} and {arr[arr.length - 1]}; } -const AssignEventView = (event: AssignEvent) => { - const { actor, assignees } = event; +const AssignUnassignEventView = ({ event }: { event: AssignEvent | UnassignEvent | ConsolidatedAssignUnassignEvent }) => { + const { actor } = event; + const assignees = (event as AssignEvent).assignees || []; + const unassignees = (event as UnassignEvent).unassignees || []; + const joinedAssignees = joinWithAnd(assignees.map(a => )); + const joinedUnassignees = joinWithAnd(unassignees.map(a => )); + + let message: JSX.Element; + if (assignees.length > 0 && unassignees.length > 0) { + message = <>assigned {joinedAssignees} and unassigned {joinedUnassignees}; + } else if (assignees.length > 0) { + message = <>assigned {joinedAssignees}; + } else { + message = <>unassigned {joinedUnassignees}; + } + return (
    @@ -337,10 +476,110 @@ const AssignEventView = (event: AssignEvent) => {
    - assigned {joinWithAnd(assignees.map(a => ))} to this pull request + {message}
    ); }; + +const ClosedEventView = ({ event, isIssue }: { event: ClosedEvent, isIssue: boolean }) => { + const { actor, createdAt } = event; + return ( +
    +
    +
    + +
    + +
    {isIssue ? 'closed this issue' : 'closed this pull request'}
    +
    + +
    + ); +}; + +const ReopenedEventView = ({ event, isIssue }: { event: ReopenedEvent, isIssue: boolean }) => { + const { actor, createdAt } = event; + return ( +
    +
    +
    + +
    + +
    {isIssue ? 'reopened this issue' : 'reopened this pull request'}
    +
    + +
    + ); +}; + +const CopilotStartedEventView = (event: CopilotStartedEvent) => { + const { createdAt, onBehalfOf, sessionLink } = event; + const { openSessionLog } = useContext(PullRequestContext); + + const handleSessionLogClick = (e: React.MouseEvent) => { + if (sessionLink) { + sessionLink.openToTheSide = e.ctrlKey || e.metaKey; + openSessionLog(sessionLink); + } + }; + + return ( +
    +
    + {threeBars} + {nbsp} +
    Copilot started work on behalf of
    +
    + {sessionLink ? ( + ) + : null} + +
    + ); +}; + +const CopilotFinishedEventView = (event: CopilotFinishedEvent) => { + const { createdAt, onBehalfOf } = event; + return ( +
    +
    + {tasklistIcon} + {nbsp} +
    Copilot finished work on behalf of
    +
    + +
    + ); +}; + +const CopilotFinishedErrorEventView = (event: CopilotFinishedErrorEvent) => { + const { createdAt, onBehalfOf } = event; + const { openSessionLog } = useContext(PullRequestContext); + + const handleSessionLogClick = (e: React.MouseEvent) => { + event.sessionLink.openToTheSide = e.ctrlKey || e.metaKey; + openSessionLog(event.sessionLink); + }; + + return ( +
    +
    +
    + {errorIcon} + {nbsp} +
    Copilot stopped work on behalf of due to an error
    +
    + +
    + +
    + ); +}; \ No newline at end of file diff --git a/webviews/components/timestamp.tsx b/webviews/components/timestamp.tsx index 988d8ae0a5..25b15cd05c 100644 --- a/webviews/components/timestamp.tsx +++ b/webviews/components/timestamp.tsx @@ -3,19 +3,98 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import * as React from 'react'; +import React, { useEffect, useState } from 'react'; import { dateFromNow } from '../../src/common/utils'; export const Timestamp = ({ date, href }: { date: Date | string; href?: string }) => { + const [timeString, setTimeString] = useState(dateFromNow(date)); const title = typeof date === 'string' ? new Date(date).toLocaleString() : date.toLocaleString(); + + useEffect(() => { + // Update the time string immediately + setTimeString(dateFromNow(date)); + + // Calculate appropriate update interval based on how old the timestamp is + const getUpdateInterval = () => { + const now = Date.now(); + const timestamp = typeof date === 'string' ? new Date(date).getTime() : date.getTime(); + const ageInMinutes = (now - timestamp) / (1000 * 60); + + // For very recent timestamps (< 1 minute), update every 20 seconds + if (ageInMinutes < 1) { + return 20000; // 20 seconds + } + // For timestamps < 1 hour old, update every 2 minutes + else if (ageInMinutes < 60) { + return 2 * 60000; // 2 minutes + } + // For timestamps < 1 day old, update every 10 minutes + else if (ageInMinutes < 60 * 24) { + return 10 * 60000; // 10 minutes + } + // Older timestamps shouldn't be updated + return null; + }; + + const intervalDuration = getUpdateInterval(); + + // If intervalDuration is null, don't set up any updates for very old timestamps + if (intervalDuration === null) { + return; + } + + let intervalId: number; + + const updateTimeString = () => { + // Only update if the page is visible + if (document.visibilityState === 'visible') { + setTimeString(dateFromNow(date)); + } + }; + + const startInterval = () => { + intervalId = window.setInterval(updateTimeString, intervalDuration); + }; + + const handleVisibilityChange = () => { + if (document.visibilityState === 'visible') { + // Page became visible, update immediately and restart interval + setTimeString(dateFromNow(date)); + if (intervalId) { + clearInterval(intervalId); + } + startInterval(); + } else { + // Page became hidden, pause the interval + if (intervalId) { + clearInterval(intervalId); + } + } + }; + + // Start the interval + startInterval(); + + // Listen for visibility changes + document.addEventListener('visibilitychange', handleVisibilityChange); + + // Clean up on component unmount + return () => { + if (intervalId) { + clearInterval(intervalId); + } + document.removeEventListener('visibilitychange', handleVisibilityChange); + }; + }, [date]); + return href ? ( - {dateFromNow(date)} + {timeString} ) : (
    - {dateFromNow(date)} + {timeString}
    ); }; diff --git a/webviews/components/user.tsx b/webviews/components/user.tsx index 9d538c05d9..edfd51e3d7 100644 --- a/webviews/components/user.tsx +++ b/webviews/components/user.tsx @@ -4,26 +4,26 @@ *--------------------------------------------------------------------------------------------*/ import * as React from 'react'; -import { IAccount, IActor, ITeam, reviewerLabel } from '../../src/github/interface'; import { Icon } from './icon'; +import { IAccount, IActor, ITeam, reviewerLabel } from '../../src/github/interface'; const InnerAvatar = ({ for: author }: { for: Partial }) => ( <> - {author.avatarUrl ? ( - + {author.avatarUrl && author.avatarUrl.includes('githubusercontent.com') ? ( + ) : ( )} ); -export const Avatar = ({ for: author, link = true }: { for: Partial, link?: boolean }) => { +export const Avatar = ({ for: author, link = true, substituteIcon }: { for: Partial, link?: boolean, substituteIcon?: JSX.Element }) => { if (link) { - return - + return ; } else { - return ; + return substituteIcon ?? ; } }; diff --git a/webviews/createPullRequestViewNew/app.tsx b/webviews/createPullRequestViewNew/app.tsx index 1295403d57..d762585360 100644 --- a/webviews/createPullRequestViewNew/app.tsx +++ b/webviews/createPullRequestViewNew/app.tsx @@ -5,13 +5,14 @@ import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; import { render } from 'react-dom'; -import { CreateParamsNew, RemoteInfo } from '../../common/views'; -import { isTeam, MergeMethod } from '../../src/github/interface'; +import { RemoteInfo } from '../../common/types'; +import { CreateParamsNew } from '../../common/views'; +import { isITeam, MergeMethod } from '../../src/github/interface'; 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'; @@ -191,7 +192,7 @@ export function main() {
    - +
    +
    updateTitle(e.currentTarget.value)} @@ -248,7 +249,7 @@ export function main() {
    {params.assignees && (params.assignees.length > 0) ?
    - +
      activateCommand(e.nativeEvent, 'pr.changeAssignees')} onKeyPress={(e) => activateCommand(e.nativeEvent, 'pr.changeAssignees')} @@ -257,7 +258,7 @@ export function main() {
    • - {assignee.login} + {assignee.specialDisplayName ?? assignee.login}
    • )}
    @@ -266,7 +267,7 @@ export function main() { {params.reviewers && (params.reviewers.length > 0) ?
    - +
      activateCommand(e.nativeEvent, 'pr.changeReviewers')} onKeyPress={(e) => activateCommand(e.nativeEvent, 'pr.changeReviewers')} @@ -275,7 +276,7 @@ export function main() {
    • - {isTeam(reviewer) ? reviewer.slug : reviewer.login} + {isITeam(reviewer) ? reviewer.slug : (reviewer.specialDisplayName ?? reviewer.login)}
    • )}
    @@ -284,7 +285,7 @@ export function main() { {params.labels && (params.labels.length > 0) ?
    - +
      activateCommand(e.nativeEvent, 'pr.changeLabels')} onKeyPress={(e) => activateCommand(e.nativeEvent, 'pr.changeLabels')} @@ -324,12 +325,12 @@ export function main() { : null}
    +
    +
    - {isAuthor ? null : ( )}
    @@ -385,7 +260,7 @@ const MergedEventView = (event: MergedEvent) => { return (
    - {gitMergeIcon} + {mergeIcon} {nbsp}
    @@ -400,12 +275,12 @@ const MergedEventView = (event: MergedEvent) => { into {event.mergeRef} {nbsp}
    +
    {pr.revertable ?
    : null} -
    ); }; @@ -420,8 +295,8 @@ const HeadDeleteEventView = (event: HeadRefDeleteEvent) => (
    deleted the {event.headRef} branch{nbsp}
    +
    -
    ); @@ -435,7 +310,7 @@ const CrossReferencedEventView = (event: CrossReferencedEvent) => {
    - linked #{source.number} {source.title} + linked #{source.number} {source.title} {nbsp} {event.willCloseTarget ? 'which will close this issue' : ''}
    @@ -452,22 +327,8 @@ function joinWithAnd(arr: JSX.Element[]): JSX.Element { return <>{arr.slice(0, -1).map(item => <>{item}, )} and {arr[arr.length - 1]}; } -const AssignUnassignEventView = ({ event }: { event: AssignEvent | UnassignEvent | ConsolidatedAssignUnassignEvent }) => { - const { actor } = event; - const assignees = (event as AssignEvent).assignees || []; - const unassignees = (event as UnassignEvent).unassignees || []; - const joinedAssignees = joinWithAnd(assignees.map(a => )); - const joinedUnassignees = joinWithAnd(unassignees.map(a => )); - - let message: JSX.Element; - if (assignees.length > 0 && unassignees.length > 0) { - message = <>assigned {joinedAssignees} and unassigned {joinedUnassignees}; - } else if (assignees.length > 0) { - message = <>assigned {joinedAssignees}; - } else { - message = <>unassigned {joinedUnassignees}; - } - +const AssignEventView = (event: AssignEvent) => { + const { actor, assignees } = event; return (
    @@ -476,110 +337,10 @@ const AssignUnassignEventView = ({ event }: { event: AssignEvent | UnassignEvent
    - {message} + assigned {joinWithAnd(assignees.map(a => ))} to this pull request
    ); }; - -const ClosedEventView = ({ event, isIssue }: { event: ClosedEvent, isIssue: boolean }) => { - const { actor, createdAt } = event; - return ( -
    -
    -
    - -
    - -
    {isIssue ? 'closed this issue' : 'closed this pull request'}
    -
    - -
    - ); -}; - -const ReopenedEventView = ({ event, isIssue }: { event: ReopenedEvent, isIssue: boolean }) => { - const { actor, createdAt } = event; - return ( -
    -
    -
    - -
    - -
    {isIssue ? 'reopened this issue' : 'reopened this pull request'}
    -
    - -
    - ); -}; - -const CopilotStartedEventView = (event: CopilotStartedEvent) => { - const { createdAt, onBehalfOf, sessionLink } = event; - const { openSessionLog } = useContext(PullRequestContext); - - const handleSessionLogClick = (e: React.MouseEvent) => { - if (sessionLink) { - sessionLink.openToTheSide = e.ctrlKey || e.metaKey; - openSessionLog(sessionLink); - } - }; - - return ( -
    -
    - {threeBars} - {nbsp} -
    Copilot started work on behalf of
    -
    - {sessionLink ? ( - ) - : null} - -
    - ); -}; - -const CopilotFinishedEventView = (event: CopilotFinishedEvent) => { - const { createdAt, onBehalfOf } = event; - return ( -
    -
    - {tasklistIcon} - {nbsp} -
    Copilot finished work on behalf of
    -
    - -
    - ); -}; - -const CopilotFinishedErrorEventView = (event: CopilotFinishedErrorEvent) => { - const { createdAt, onBehalfOf } = event; - const { openSessionLog } = useContext(PullRequestContext); - - const handleSessionLogClick = (e: React.MouseEvent) => { - event.sessionLink.openToTheSide = e.ctrlKey || e.metaKey; - openSessionLog(event.sessionLink); - }; - - return ( -
    -
    -
    - {errorIcon} - {nbsp} -
    Copilot stopped work on behalf of due to an error
    -
    - -
    - -
    - ); -}; \ No newline at end of file diff --git a/webviews/components/timestamp.tsx b/webviews/components/timestamp.tsx index 25b15cd05c..988d8ae0a5 100644 --- a/webviews/components/timestamp.tsx +++ b/webviews/components/timestamp.tsx @@ -3,98 +3,19 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import React, { useEffect, useState } from 'react'; +import * as React from 'react'; import { dateFromNow } from '../../src/common/utils'; export const Timestamp = ({ date, href }: { date: Date | string; href?: string }) => { - const [timeString, setTimeString] = useState(dateFromNow(date)); const title = typeof date === 'string' ? new Date(date).toLocaleString() : date.toLocaleString(); - - useEffect(() => { - // Update the time string immediately - setTimeString(dateFromNow(date)); - - // Calculate appropriate update interval based on how old the timestamp is - const getUpdateInterval = () => { - const now = Date.now(); - const timestamp = typeof date === 'string' ? new Date(date).getTime() : date.getTime(); - const ageInMinutes = (now - timestamp) / (1000 * 60); - - // For very recent timestamps (< 1 minute), update every 20 seconds - if (ageInMinutes < 1) { - return 20000; // 20 seconds - } - // For timestamps < 1 hour old, update every 2 minutes - else if (ageInMinutes < 60) { - return 2 * 60000; // 2 minutes - } - // For timestamps < 1 day old, update every 10 minutes - else if (ageInMinutes < 60 * 24) { - return 10 * 60000; // 10 minutes - } - // Older timestamps shouldn't be updated - return null; - }; - - const intervalDuration = getUpdateInterval(); - - // If intervalDuration is null, don't set up any updates for very old timestamps - if (intervalDuration === null) { - return; - } - - let intervalId: number; - - const updateTimeString = () => { - // Only update if the page is visible - if (document.visibilityState === 'visible') { - setTimeString(dateFromNow(date)); - } - }; - - const startInterval = () => { - intervalId = window.setInterval(updateTimeString, intervalDuration); - }; - - const handleVisibilityChange = () => { - if (document.visibilityState === 'visible') { - // Page became visible, update immediately and restart interval - setTimeString(dateFromNow(date)); - if (intervalId) { - clearInterval(intervalId); - } - startInterval(); - } else { - // Page became hidden, pause the interval - if (intervalId) { - clearInterval(intervalId); - } - } - }; - - // Start the interval - startInterval(); - - // Listen for visibility changes - document.addEventListener('visibilitychange', handleVisibilityChange); - - // Clean up on component unmount - return () => { - if (intervalId) { - clearInterval(intervalId); - } - document.removeEventListener('visibilitychange', handleVisibilityChange); - }; - }, [date]); - return href ? ( - {timeString} + {dateFromNow(date)} ) : (
    - {timeString} + {dateFromNow(date)}
    ); }; diff --git a/webviews/components/user.tsx b/webviews/components/user.tsx index edfd51e3d7..9d538c05d9 100644 --- a/webviews/components/user.tsx +++ b/webviews/components/user.tsx @@ -4,26 +4,26 @@ *--------------------------------------------------------------------------------------------*/ import * as React from 'react'; -import { Icon } from './icon'; import { IAccount, IActor, ITeam, reviewerLabel } from '../../src/github/interface'; +import { Icon } from './icon'; const InnerAvatar = ({ for: author }: { for: Partial }) => ( <> - {author.avatarUrl && author.avatarUrl.includes('githubusercontent.com') ? ( - + {author.avatarUrl ? ( + ) : ( )} ); -export const Avatar = ({ for: author, link = true, substituteIcon }: { for: Partial, link?: boolean, substituteIcon?: JSX.Element }) => { +export const Avatar = ({ for: author, link = true }: { for: Partial, link?: boolean }) => { if (link) { - return + ; } else { - return substituteIcon ?? ; + return ; } }; diff --git a/webviews/createPullRequestViewNew/app.tsx b/webviews/createPullRequestViewNew/app.tsx index d762585360..1295403d57 100644 --- a/webviews/createPullRequestViewNew/app.tsx +++ b/webviews/createPullRequestViewNew/app.tsx @@ -5,14 +5,13 @@ import React, { useCallback, useContext, useEffect, useRef, useState } from 'react'; import { render } from 'react-dom'; -import { RemoteInfo } from '../../common/types'; -import { CreateParamsNew } from '../../common/views'; -import { isITeam, MergeMethod } from '../../src/github/interface'; +import { CreateParamsNew, RemoteInfo } from '../../common/views'; +import { isTeam, MergeMethod } from '../../src/github/interface'; import PullRequestContextNew from '../common/createContextNew'; import { ErrorBoundary } from '../common/errorBoundary'; import { LabelCreate } from '../common/label'; import { ContextDropdown } from '../components/contextDropdown'; -import { accountIcon, feedbackIcon, gitCompareIcon, milestoneIcon, prMergeIcon, projectIcon, sparkleIcon, stopCircleIcon, tagIcon } from '../components/icon'; +import { assigneeIcon, labelIcon, milestoneIcon, prBaseIcon, prMergeIcon, projectIcon, reviewerIcon, sparkleIcon, stopCircleIcon } from '../components/icon'; import { Avatar } from '../components/user'; type CreateMethod = 'create-draft' | 'create' | 'create-automerge-squash' | 'create-automerge-rebase' | 'create-automerge-merge'; @@ -192,7 +191,7 @@ export function main() {
    - +
    -
    updateTitle(e.currentTarget.value)} @@ -249,7 +248,7 @@ export function main() {
    {params.assignees && (params.assignees.length > 0) ?
    - +
      activateCommand(e.nativeEvent, 'pr.changeAssignees')} onKeyPress={(e) => activateCommand(e.nativeEvent, 'pr.changeAssignees')} @@ -258,7 +257,7 @@ export function main() {
    • - {assignee.specialDisplayName ?? assignee.login} + {assignee.login}
    • )}
    @@ -267,7 +266,7 @@ export function main() { {params.reviewers && (params.reviewers.length > 0) ?
    - +
      activateCommand(e.nativeEvent, 'pr.changeReviewers')} onKeyPress={(e) => activateCommand(e.nativeEvent, 'pr.changeReviewers')} @@ -276,7 +275,7 @@ export function main() {
    • - {isITeam(reviewer) ? reviewer.slug : (reviewer.specialDisplayName ?? reviewer.login)} + {isTeam(reviewer) ? reviewer.slug : reviewer.login}
    • )}
    @@ -285,7 +284,7 @@ export function main() { {params.labels && (params.labels.length > 0) ?
    - +
      activateCommand(e.nativeEvent, 'pr.changeLabels')} onKeyPress={(e) => activateCommand(e.nativeEvent, 'pr.changeLabels')} @@ -325,12 +324,12 @@ export function main() { : null}
    -

    b|F9vyNu4Q7^wn`WL&(%c?ZBh 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