Skip to content

Commit 1887bf7

Browse files
committed
GitHub PR view highlights all repos with Copilot notification
Fixes #7852
1 parent efbe564 commit 1887bf7

File tree

12 files changed

+119
-73
lines changed

12 files changed

+119
-73
lines changed

src/common/uri.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import { Buffer } from 'buffer';
99
import * as pathUtils from 'path';
1010
import fetch from 'cross-fetch';
1111
import * as vscode from 'vscode';
12+
import { RemoteInfo } from '../../common/types';
1213
import { Repository } from '../api/api';
1314
import { EXTENSION_ID } from '../constants';
1415
import { IAccount, isITeam, ITeam, reviewerId } from '../github/interface';
@@ -675,6 +676,24 @@ export function fromOpenPullRequestWebviewUri(uri: vscode.Uri): OpenPullRequestW
675676
} catch (e) { }
676677
}
677678

679+
export function toQueryUri(params: { remote: RemoteInfo | undefined, isCopilot?: boolean }) {
680+
const uri = vscode.Uri.from({ scheme: Schemes.PRQuery, path: params.isCopilot ? 'copilot' : undefined, query: params.remote ? JSON.stringify({ remote: params.remote }) : undefined });
681+
return uri;
682+
}
683+
684+
export function fromQueryUri(uri: vscode.Uri): { remote: RemoteInfo | undefined, isCopilot?: boolean } | undefined {
685+
if (uri.scheme !== Schemes.PRQuery) {
686+
return;
687+
}
688+
try {
689+
const query = uri.query ? JSON.parse(uri.query) : undefined;
690+
return {
691+
remote: query.remote,
692+
isCopilot: uri.path === 'copilot'
693+
};
694+
} catch (e) { }
695+
}
696+
678697
export enum Schemes {
679698
File = 'file',
680699
Review = 'review', // File content for a checked out PR
@@ -694,8 +713,6 @@ export enum Schemes {
694713
GitHubCommit = 'githubcommit' // file content from GitHub for a commit
695714
}
696715

697-
export const COPILOT_QUERY = vscode.Uri.from({ scheme: Schemes.PRQuery, path: 'copilot' });
698-
699716
export function resolvePath(from: vscode.Uri, to: string) {
700717
if (from.scheme === Schemes.File) {
701718
return pathUtils.resolve(from.fsPath, to);

src/github/copilotPrWatcher.ts

Lines changed: 19 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,6 @@ import { COPILOT_LOGINS, copilotEventToStatus, CopilotPRStatus } from '../common
1010
import { Disposable } from '../common/lifecycle';
1111
import Logger from '../common/logger';
1212
import { PR_SETTINGS_NAMESPACE, QUERIES } from '../common/settingKeys';
13-
import { FolderRepositoryManager } from './folderRepositoryManager';
1413
import { PRType } from './interface';
1514
import { PullRequestModel } from './pullRequestModel';
1615
import { PullRequestOverviewPanel } from './pullRequestOverview';
@@ -42,7 +41,10 @@ export class CopilotStateModel extends Disposable {
4241
this._onRefresh.fire();
4342
}
4443

45-
makeKey(owner: string, repo: string, prNumber: number): string {
44+
makeKey(owner: string, repo: string, prNumber?: number): string {
45+
if (prNumber === undefined) {
46+
return `${owner}/${repo}`;
47+
}
4648
return `${owner}/${repo}#${prNumber}`;
4749
}
4850

@@ -109,6 +111,17 @@ export class CopilotStateModel extends Disposable {
109111
return this._showNotification;
110112
}
111113

114+
getNotificationsCount(owner: string, repo: string): number {
115+
let total = 0;
116+
const partialKey = `${this.makeKey(owner, repo)}#`;
117+
for (const state of this._showNotification.values()) {
118+
if (state.startsWith(partialKey)) {
119+
total++;
120+
}
121+
}
122+
return total;
123+
}
124+
112125
setInitialized() {
113126
this._isInitialized = true;
114127
}
@@ -117,11 +130,14 @@ export class CopilotStateModel extends Disposable {
117130
return this._isInitialized;
118131
}
119132

120-
getCounts(): { total: number; inProgress: number; error: number } {
133+
getCounts(owner: string, repo: string): { total: number; inProgress: number; error: number } {
121134
let inProgressCount = 0;
122135
let errorCount = 0;
123136

124137
for (const state of this._states.values()) {
138+
if (state.item.remote.owner !== owner || state.item.remote.repositoryName !== repo) {
139+
continue;
140+
}
125141
if (state.status === CopilotPRStatus.Started) {
126142
inProgressCount++;
127143
} else if (state.status === CopilotPRStatus.Failed) {
@@ -221,14 +237,6 @@ export class CopilotPRWatcher extends Disposable {
221237
}
222238
}
223239

224-
private _currentUser: string | undefined;
225-
private async _getCurrentUser(folderManager: FolderRepositoryManager): Promise<string> {
226-
if (!this._currentUser) {
227-
this._currentUser = (await folderManager.getCurrentUser()).login;
228-
}
229-
return this._currentUser;
230-
}
231-
232240
private async _updateSingleState(pr: PullRequestModel): Promise<void> {
233241
const changes: { pullRequestModel: PullRequestModel, status: CopilotPRStatus }[] = [];
234242

src/github/copilotRemoteAgent.ts

Lines changed: 16 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -743,21 +743,32 @@ export class CopilotRemoteAgentManager extends Disposable {
743743
})[0];
744744
}
745745

746+
getNotificationsCount(owner: string, repo: string): number {
747+
return this._stateModel.getNotificationsCount(owner, repo);
748+
}
749+
746750
get notificationsCount(): number {
747751
return this._stateModel.notifications.size;
748752
}
749753

750-
hasNotification(owner: string, repo: string, pullRequestNumber: number): boolean {
751-
const key = this._stateModel.makeKey(owner, repo, pullRequestNumber);
752-
return this._stateModel.notifications.has(key);
754+
hasNotification(owner: string, repo: string, pullRequestNumber?: number): boolean {
755+
if (pullRequestNumber !== undefined) {
756+
const key = this._stateModel.makeKey(owner, repo, pullRequestNumber);
757+
return this._stateModel.notifications.has(key);
758+
} else {
759+
const partialKey = this._stateModel.makeKey(owner, repo);
760+
return Array.from(this._stateModel.notifications.keys()).some(key => {
761+
return key.startsWith(partialKey);
762+
});
763+
}
753764
}
754765

755766
getStateForPR(owner: string, repo: string, prNumber: number): CopilotPRStatus {
756767
return this._stateModel.get(owner, repo, prNumber);
757768
}
758769

759-
getCounts(): { total: number; inProgress: number; error: number } {
760-
return this._stateModel.getCounts();
770+
getCounts(owner: string, repo: string): { total: number; inProgress: number; error: number } {
771+
return this._stateModel.getCounts(owner, repo);
761772
}
762773

763774
async extractHistory(history: ReadonlyArray<vscode.ChatRequestTurn | vscode.ChatResponseTurn>): Promise<string | undefined> {

src/github/createPRViewProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -277,7 +277,7 @@ export abstract class BaseCreatePullRequestViewProvider<T extends BasePullReques
277277
if (!configuration) {
278278
return;
279279
}
280-
const resolved = await variableSubstitution(configuration, pr, undefined, (await this._folderRepositoryManager.getCurrentUser(pr.githubRepository))?.login);
280+
const resolved = variableSubstitution(configuration, pr, undefined, (await this._folderRepositoryManager.getCurrentUser(pr.githubRepository))?.login);
281281
if (!resolved) {
282282
return;
283283
}

src/github/folderRepositoryManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1108,7 +1108,7 @@ export class FolderRepositoryManager extends Disposable {
11081108
pageNumber: number,
11091109
): Promise<{ items: any[]; hasMorePages: boolean, totalCount?: number } | undefined> => {
11101110
// Resolve variables in the query with each repo
1111-
const resolvedQuery = query ? await variableSubstitution(query, undefined,
1111+
const resolvedQuery = query ? variableSubstitution(query, undefined,
11121112
{ base: await githubRepository.getDefaultBranch(), owner: githubRepository.remote.owner, repo: githubRepository.remote.repositoryName }) : undefined;
11131113
switch (pagedDataType) {
11141114
case PagedDataType.PullRequest: {

src/github/utils.ts

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
import * as crypto from 'crypto';
88
import * as OctokitTypes from '@octokit/types';
99
import * as vscode from 'vscode';
10+
import { RemoteInfo } from '../../common/types';
1011
import { Repository } from '../api/api';
1112
import { GitApiImpl } from '../api/api1';
1213
import { AuthProvider, GitHubServerType } from '../common/authentication';
@@ -1670,12 +1671,12 @@ function computeSinceValue(sinceValue: string | undefined): string {
16701671
const COPILOT_PATTERN = /\:(Copilot|copilot)(\s|$)/g;
16711672

16721673
const VARIABLE_PATTERN = /\$\{([^-]*?)(-.*?)?\}/g;
1673-
export async function variableSubstitution(
1674+
export function variableSubstitution(
16741675
value: string,
16751676
issueModel?: IssueModel,
16761677
defaults?: PullRequestDefaults,
16771678
user?: string,
1678-
): Promise<string> {
1679+
): string {
16791680
const withVariables = value.replace(VARIABLE_PATTERN, (match: string, variable: string, extra: string) => {
16801681
let result: string;
16811682
switch (variable) {
@@ -1787,4 +1788,22 @@ export enum UnsatisfiedChecks {
17871788
ChangesRequested = 1 << 1,
17881789
CIFailed = 1 << 2,
17891790
CIPending = 1 << 3
1791+
}
1792+
1793+
export async function extractRepoFromQuery(folderManager: FolderRepositoryManager, query: string | undefined): Promise<RemoteInfo | undefined> {
1794+
if (!query) {
1795+
return undefined;
1796+
}
1797+
1798+
const defaults = await folderManager.getPullRequestDefaults();
1799+
// Use a fake user since we only care about pulling out the repo and repo owner
1800+
const substituted = variableSubstitution(query, undefined, defaults, 'fakeUser');
1801+
1802+
const repoRegex = /(?:^|\s)repo:(?:"?(?<owner>[A-Za-z0-9_.-]+)\/(?<repo>[A-Za-z0-9_.-]+)"?)/i;
1803+
const repoMatch = repoRegex.exec(substituted);
1804+
if (repoMatch && repoMatch.groups) {
1805+
return { owner: repoMatch.groups.owner, repositoryName: repoMatch.groups.repo };
1806+
}
1807+
1808+
return undefined;
17901809
}

src/issues/currentIssue.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -213,7 +213,7 @@ export class CurrentIssue extends Disposable {
213213
}
214214
const state: IssueState = this.stateManager.getSavedIssueState(this.issueModel.number);
215215
this._branchName = this.shouldPromptForBranch ? undefined : state.branch;
216-
const branchNameConfig = await variableSubstitution(
216+
const branchNameConfig = variableSubstitution(
217217
await this.getBranchTitle(),
218218
this.issue,
219219
undefined,

src/issues/issueCompletionProvider.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -210,7 +210,7 @@ export class IssueCompletionProvider implements vscode.CompletionItemProvider {
210210
.getConfiguration(ISSUES_SETTINGS_NAMESPACE)
211211
.get(ISSUE_COMPLETION_FORMAT_SCM);
212212
if (document.uri.path.match(/git\/scm\d\/input/) && typeof configuration === 'string') {
213-
item.insertText = await variableSubstitution(configuration, issue, repo);
213+
item.insertText = variableSubstitution(configuration, issue, repo);
214214
} else {
215215
item.insertText = `${getIssueNumberLabel(issue, repo)}`;
216216
}

src/issues/stateManager.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ export class StateManager {
316316
items = this.setIssues(
317317
folderManager,
318318
// Do not resolve pull request defaults as they will get resolved in the query later per repository
319-
await variableSubstitution(query.query, undefined, undefined, user),
319+
variableSubstitution(query.query, undefined, undefined, user),
320320
).then(issues => ({ groupBy: query.groupBy ?? [], issues }));
321321

322322
if (items) {

src/view/prStatusDecorationProvider.ts

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import * as vscode from 'vscode';
77
import { Disposable } from '../common/lifecycle';
88
import { Protocol } from '../common/protocol';
9-
import { COPILOT_QUERY, createPRNodeUri, fromPRNodeUri, parsePRNodeIdentifier, PRNodeUriParams, Schemes } from '../common/uri';
9+
import { createPRNodeUri, fromPRNodeUri, fromQueryUri, parsePRNodeIdentifier, PRNodeUriParams, Schemes, toQueryUri } from '../common/uri';
1010
import { CopilotRemoteAgentManager } from '../github/copilotRemoteAgent';
1111
import { getStatusDecoration } from '../github/markdownUtils';
1212
import { PrsTreeModel } from './prsTreeModel';
@@ -28,8 +28,14 @@ export class PRStatusDecorationProvider extends Disposable implements vscode.Fil
2828
);
2929

3030
this._register(this._copilotManager.onDidChangeNotifications(items => {
31-
const uris = [COPILOT_QUERY];
31+
const repoItems = new Set<string>();
32+
const uris: vscode.Uri[] = [];
3233
for (const item of items) {
34+
const queryUri = toQueryUri({ remote: { owner: item.remote.owner, repositoryName: item.remote.repositoryName }, isCopilot: true });
35+
if (!repoItems.has(queryUri.toString())) {
36+
repoItems.add(queryUri.toString());
37+
uris.push(queryUri);
38+
}
3339
uris.push(createPRNodeUri(item));
3440
}
3541
this._onDidChangeFileDecorations.fire(uris);
@@ -81,14 +87,19 @@ export class PRStatusDecorationProvider extends Disposable implements vscode.Fil
8187
}
8288

8389
private _queryDecoration(uri: vscode.Uri): vscode.ProviderResult<vscode.FileDecoration> {
84-
if (uri.path === 'copilot') {
85-
if (this._copilotManager.notificationsCount > 0) {
86-
return {
87-
tooltip: vscode.l10n.t('Coding agent has made changes', this._copilotManager.notificationsCount),
88-
badge: new vscode.ThemeIcon('copilot') as any,
89-
color: new vscode.ThemeColor('pullRequests.notification'),
90-
};
91-
}
90+
const params = fromQueryUri(uri);
91+
if (!params?.isCopilot || !params.remote) {
92+
return;
9293
}
94+
const counts = this._copilotManager.getNotificationsCount(params.remote.owner, params.remote.repositoryName);
95+
if (counts === 0) {
96+
return;
97+
}
98+
99+
return {
100+
tooltip: vscode.l10n.t('Coding agent has made changes'),
101+
badge: new vscode.ThemeIcon('copilot') as any,
102+
color: new vscode.ThemeColor('pullRequests.notification'),
103+
};
93104
}
94105
}

0 commit comments

Comments
 (0)