Skip to content

Commit ba96154

Browse files
authored
Begin consolidating prs tree model and copilot status model (#7991)
* Begin consolidating prs tree model and copilot status model Fixes microsoft/vscode#264638 * Fix tests
1 parent d3791af commit ba96154

18 files changed

Lines changed: 398 additions & 277 deletions

src/commands.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { getIssuesUrl, getPullsUrl, isInCodespaces, ISSUE_OR_URL_EXPRESSION, par
3535
import { OverviewContext } from './github/views';
3636
import { isNotificationTreeItem, NotificationTreeItem } from './notifications/notificationItem';
3737
import { NotificationsManager } from './notifications/notificationsManager';
38+
import { PrsTreeModel } from './view/prsTreeModel';
3839
import { ReviewCommentController } from './view/reviewCommentController';
3940
import { ReviewManager } from './view/reviewManager';
4041
import { ReviewsManager } from './view/reviewsManager';
@@ -203,7 +204,8 @@ export function registerCommands(
203204
reviewsManager: ReviewsManager,
204205
telemetry: ITelemetry,
205206
copilotRemoteAgentManager: CopilotRemoteAgentManager,
206-
notificationManager: NotificationsManager
207+
notificationManager: NotificationsManager,
208+
prsTreeModel: PrsTreeModel
207209
) {
208210
const logId = 'RegisterCommands';
209211
context.subscriptions.push(
@@ -900,15 +902,15 @@ export function registerCommands(
900902
vscode.commands.registerCommand('pr.dismissNotification', node => {
901903
if (node instanceof PRNode) {
902904
notificationManager.markPrNotificationsAsRead(node.pullRequestModel);
903-
copilotRemoteAgentManager.clearNotification(node.pullRequestModel.remote.owner, node.pullRequestModel.remote.repositoryName, node.pullRequestModel.number);
905+
prsTreeModel.clearCopilotNotification(node.pullRequestModel.remote.owner, node.pullRequestModel.remote.repositoryName, node.pullRequestModel.number);
904906
}
905907
}),
906908
);
907909

908910
context.subscriptions.push(
909911
vscode.commands.registerCommand('pr.markAllCopilotNotificationsAsRead', node => {
910912
if (node instanceof CategoryTreeNode && node.isCopilot && node.repo) {
911-
copilotRemoteAgentManager.clearAllNotifications(node.repo.owner, node.repo.repositoryName);
913+
prsTreeModel.clearAllCopilotNotifications(node.repo.owner, node.repo.repositoryName);
912914
}
913915
}),
914916
);

src/extension.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,7 @@ import { GitHubCommitFileSystemProvider } from './view/githubFileContentProvider
4848
import { getInMemPRFileSystemProvider } from './view/inMemPRContentProvider';
4949
import { PullRequestChangesTreeDataProvider } from './view/prChangesTreeDataProvider';
5050
import { PullRequestsTreeDataProvider } from './view/prsTreeDataProvider';
51+
import { PrsTreeModel } from './view/prsTreeModel';
5152
import { ReviewManager, ShowPullRequest } from './view/reviewManager';
5253
import { ReviewsManager } from './view/reviewsManager';
5354
import { TreeDecorationProviders } from './view/treeDecorationProviders';
@@ -70,7 +71,8 @@ async function init(
7071
reposManager: RepositoriesManager,
7172
createPrHelper: CreatePullRequestHelper,
7273
copilotRemoteAgentManager: CopilotRemoteAgentManager,
73-
themeWatcher: ThemeWatcher
74+
themeWatcher: ThemeWatcher,
75+
prsTreeModel: PrsTreeModel,
7476
): Promise<void> {
7577
context.subscriptions.push(Logger);
7678
Logger.appendLine('Git repository found, initializing review manager and pr tree view.', ACTIVATION);
@@ -176,7 +178,7 @@ async function init(
176178
const notificationsManager = new NotificationsManager(notificationsProvider, credentialStore, reposManager, context);
177179
context.subscriptions.push(notificationsManager);
178180

179-
const reviewsManager = new ReviewsManager(context, reposManager, reviewManagers, tree, changesTree, telemetry, credentialStore, git, copilotRemoteAgentManager, notificationsManager);
181+
const reviewsManager = new ReviewsManager(context, reposManager, reviewManagers, prsTreeModel, tree, changesTree, telemetry, credentialStore, git, copilotRemoteAgentManager, notificationsManager);
180182
context.subscriptions.push(reviewsManager);
181183

182184
git.onDidChangeState(() => {
@@ -238,7 +240,7 @@ async function init(
238240

239241
tree.initialize(reviewsManager.reviewManagers.map(manager => manager.reviewModel), notificationsManager);
240242

241-
registerCommands(context, reposManager, reviewsManager, telemetry, copilotRemoteAgentManager, notificationsManager);
243+
registerCommands(context, reposManager, reviewsManager, telemetry, copilotRemoteAgentManager, notificationsManager, prsTreeModel);
242244

243245
const layout = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<string>(FILE_LIST_LAYOUT);
244246
await vscode.commands.executeCommand('setContext', 'fileListLayout:flat', layout === 'flat');
@@ -258,7 +260,7 @@ async function init(
258260

259261
registerPostCommitCommandsProvider(reposManager, git);
260262

261-
initChat(context, credentialStore, reposManager, copilotRemoteAgentManager, telemetry);
263+
initChat(context, credentialStore, reposManager, copilotRemoteAgentManager, telemetry, prsTreeModel);
262264
context.subscriptions.push(vscode.window.registerUriHandler(new UriHandler(reposManager, telemetry, context)));
263265

264266
// Make sure any compare changes tabs, which come from the create flow, are closed.
@@ -269,11 +271,11 @@ async function init(
269271
telemetry.sendTelemetryEvent('startup');
270272
}
271273

272-
function initChat(context: vscode.ExtensionContext, credentialStore: CredentialStore, reposManager: RepositoriesManager, copilotRemoteManager: CopilotRemoteAgentManager, telemetry: ExperimentationTelemetry) {
274+
function initChat(context: vscode.ExtensionContext, credentialStore: CredentialStore, reposManager: RepositoriesManager, copilotRemoteManager: CopilotRemoteAgentManager, telemetry: ExperimentationTelemetry, prsTreeModel: PrsTreeModel) {
273275
const createParticipant = () => {
274276
const chatParticipantState = new ChatParticipantState();
275277
context.subscriptions.push(new ChatParticipant(context, chatParticipantState));
276-
registerTools(context, credentialStore, reposManager, chatParticipantState, copilotRemoteManager, telemetry);
278+
registerTools(context, credentialStore, reposManager, chatParticipantState, copilotRemoteManager, telemetry, prsTreeModel);
277279
};
278280

279281
const chatEnabled = () => vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<boolean>(EXPERIMENTAL_CHAT, false);
@@ -409,6 +411,10 @@ async function deferredActivate(context: vscode.ExtensionContext, showPRControll
409411

410412
const reposManager = new RepositoriesManager(credentialStore, telemetry);
411413
context.subscriptions.push(reposManager);
414+
415+
const prsTreeModel = new PrsTreeModel(telemetry, reposManager, context);
416+
context.subscriptions.push(prsTreeModel);
417+
412418
// API
413419
const apiImpl = new GitApiImpl(reposManager);
414420
context.subscriptions.push(apiImpl);
@@ -424,7 +430,7 @@ async function deferredActivate(context: vscode.ExtensionContext, showPRControll
424430

425431
Logger.debug('Creating tree view.', 'Activation');
426432

427-
const copilotRemoteAgentManager = new CopilotRemoteAgentManager(credentialStore, reposManager, telemetry, context, apiImpl);
433+
const copilotRemoteAgentManager = new CopilotRemoteAgentManager(credentialStore, reposManager, telemetry, context, apiImpl, prsTreeModel);
428434
context.subscriptions.push(copilotRemoteAgentManager);
429435
if (vscode.chat?.registerChatSessionItemProvider) {
430436
const chatParticipant = vscode.chat.createChatParticipant(COPILOT_SWE_AGENT, async (request, context, stream, token) =>
@@ -457,7 +463,7 @@ async function deferredActivate(context: vscode.ExtensionContext, showPRControll
457463
));
458464
}
459465

460-
const prTree = new PullRequestsTreeDataProvider(telemetry, context, reposManager, copilotRemoteAgentManager);
466+
const prTree = new PullRequestsTreeDataProvider(prsTreeModel, telemetry, context, reposManager, copilotRemoteAgentManager);
461467
context.subscriptions.push(prTree);
462468
context.subscriptions.push(credentialStore.onDidGetSession(() => prTree.refreshAll(true)));
463469
Logger.appendLine('Looking for git repository', ACTIVATION);
@@ -485,7 +491,7 @@ async function deferredActivate(context: vscode.ExtensionContext, showPRControll
485491
const githubFilesystemProvider = new GitHubCommitFileSystemProvider(reposManager, apiImpl, credentialStore);
486492
context.subscriptions.push(vscode.workspace.registerFileSystemProvider(Schemes.GitHubCommit, githubFilesystemProvider, { isReadonly: new vscode.MarkdownString(vscode.l10n.t('GitHub commits cannot be edited')) }));
487493

488-
await init(context, apiImpl, credentialStore, repositories, prTree, liveshareApiPromise, showPRController, reposManager, createPrHelper, copilotRemoteAgentManager, themeWatcher);
494+
await init(context, apiImpl, credentialStore, repositories, prTree, liveshareApiPromise, showPRController, reposManager, createPrHelper, copilotRemoteAgentManager, themeWatcher, prsTreeModel);
489495
return apiImpl;
490496
}
491497

src/github/copilotPrWatcher.ts

Lines changed: 25 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,26 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as vscode from 'vscode';
7-
import { PRType } from './interface';
87
import { PullRequestModel } from './pullRequestModel';
98
import { PullRequestOverviewPanel } from './pullRequestOverview';
109
import { RepositoriesManager } from './repositoriesManager';
1110
import { debounce } from '../common/async';
1211
import { COPILOT_ACCOUNTS } from '../common/comment';
1312
import { COPILOT_LOGINS, copilotEventToStatus, CopilotPRStatus } from '../common/copilot';
1413
import { Disposable } from '../common/lifecycle';
15-
import Logger from '../common/logger';
1614
import { PR_SETTINGS_NAMESPACE, QUERIES } from '../common/settingKeys';
15+
import { PrsTreeModel } from '../view/prsTreeModel';
1716

1817
export function isCopilotQuery(query: string): boolean {
1918
const lowerQuery = query.toLowerCase();
2019
return COPILOT_LOGINS.some(login => lowerQuery.includes(`author:${login.toLowerCase()}`));
2120
}
2221

22+
export function getCopilotQuery(): string | undefined {
23+
const queries = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<{ label: string; query: string }[]>(QUERIES, []);
24+
return queries.find(query => isCopilotQuery(query.query))?.query;
25+
}
26+
2327
export interface CodingAgentPRAndStatus {
2428
item: PullRequestModel;
2529
status: CopilotPRStatus;
@@ -31,15 +35,9 @@ export class CopilotStateModel extends Disposable {
3135
private readonly _states: Map<string, CodingAgentPRAndStatus> = new Map();
3236
private readonly _showNotification: Set<string> = new Set();
3337
private readonly _onDidChangeStates = this._register(new vscode.EventEmitter<void>());
34-
readonly onDidChangeStates = this._onDidChangeStates.event;
38+
readonly onDidChangeCopilotStates = this._onDidChangeStates.event;
3539
private readonly _onDidChangeNotifications = this._register(new vscode.EventEmitter<PullRequestModel[]>());
36-
readonly onDidChangeNotifications = this._onDidChangeNotifications.event;
37-
private readonly _onRefresh = this._register(new vscode.EventEmitter<void>());
38-
readonly onRefresh = this._onRefresh.event;
39-
40-
clear(): void {
41-
this._onRefresh.fire();
42-
}
40+
readonly onDidChangeCopilotNotifications = this._onDidChangeNotifications.event;
4341

4442
makeKey(owner: string, repo: string, prNumber?: number): string {
4543
if (prNumber === undefined) {
@@ -60,17 +58,17 @@ export class CopilotStateModel extends Disposable {
6058
}
6159
}
6260

63-
set(statuses: { pullRequestModel: PullRequestModel, status: CopilotPRStatus }[]): void {
61+
set(statuses: CodingAgentPRAndStatus[]): void {
6462
const changedModels: PullRequestModel[] = [];
6563
const changedKeys: string[] = [];
66-
for (const { pullRequestModel, status } of statuses) {
67-
const key = this.makeKey(pullRequestModel.remote.owner, pullRequestModel.remote.repositoryName, pullRequestModel.number);
64+
for (const { item, status } of statuses) {
65+
const key = this.makeKey(item.remote.owner, item.remote.repositoryName, item.number);
6866
const currentStatus = this._states.get(key);
6967
if (currentStatus?.status === status) {
7068
continue;
7169
}
72-
this._states.set(key, { item: pullRequestModel, status });
73-
changedModels.push(pullRequestModel);
70+
this._states.set(key, { item, status });
71+
changedModels.push(item);
7472
changedKeys.push(key);
7573
}
7674
if (changedModels.length > 0) {
@@ -188,9 +186,11 @@ export class CopilotStateModel extends Disposable {
188186
}
189187

190188
export class CopilotPRWatcher extends Disposable {
189+
private readonly _model: CopilotStateModel;
191190

192-
constructor(private readonly _reposManager: RepositoriesManager, private readonly _model: CopilotStateModel) {
191+
constructor(private readonly _reposManager: RepositoriesManager, private readonly _prsTreeModel: PrsTreeModel) {
193192
super();
193+
this._model = _prsTreeModel.copilotStateModel;
194194
if (this._reposManager.folderManagers.length === 0) {
195195
const initDisposable = this._reposManager.onDidChangeAnyGitHubRepository(() => {
196196
initDisposable.dispose();
@@ -199,16 +199,18 @@ export class CopilotPRWatcher extends Disposable {
199199
} else {
200200
this._initialize();
201201
}
202-
this._register(this._model.onRefresh(() => this._getStateChanges()));
203202
}
204203

205204
private _initialize() {
206-
this._getStateChanges();
205+
this._prsTreeModel.refreshCopilotStateChanges(true);
207206
this._pollForChanges();
208-
const updateFullState = debounce(() => this._getStateChanges(), 50);
207+
const updateFullState = debounce(() => this._prsTreeModel.refreshCopilotStateChanges(true), 50);
209208
this._register(this._reposManager.onDidChangeAnyPullRequests(e => {
210209
if (e.some(pr => COPILOT_ACCOUNTS[pr.model.author.login])) {
211-
if (this._model.isInitialized && e.some(pr => this._model.get(pr.model.remote.owner, pr.model.remote.repositoryName, pr.model.number) === CopilotPRStatus.None)) {
210+
if (!this._model.isInitialized) {
211+
return;
212+
}
213+
if (e.some(pr => this._model.get(pr.model.remote.owner, pr.model.remote.repositoryName, pr.model.number) === CopilotPRStatus.None)) {
212214
// A PR we don't know about was updated
213215
updateFullState();
214216
} else {
@@ -238,11 +240,6 @@ export class CopilotPRWatcher extends Disposable {
238240
this._register({ dispose: () => this._pollTimeout && clearTimeout(this._pollTimeout) });
239241
}
240242

241-
private _queriesIncludeCopilot(): string | undefined {
242-
const queries = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<{ label: string; query: string }[]>(QUERIES, []);
243-
return queries.find(query => isCopilotQuery(query.query))?.query;
244-
}
245-
246243
private get _pollInterval(): number {
247244
if (vscode.window.state.active || vscode.window.state.focused) {
248245
return 60 * 1000 * 2; // Poll every 2 minutes
@@ -258,7 +255,7 @@ export class CopilotPRWatcher extends Disposable {
258255
this._pollTimeout = undefined;
259256
}
260257
this._lastPollTime = Date.now();
261-
const shouldContinue = await this._getStateChanges();
258+
const shouldContinue = await this._prsTreeModel.refreshCopilotStateChanges(true);
262259

263260
if (shouldContinue) {
264261
this._pollTimeout = setTimeout(() => {
@@ -268,7 +265,7 @@ export class CopilotPRWatcher extends Disposable {
268265
}
269266

270267
private async _updateSingleState(pr: PullRequestModel): Promise<void> {
271-
const changes: { pullRequestModel: PullRequestModel, status: CopilotPRStatus }[] = [];
268+
const changes: CodingAgentPRAndStatus[] = [];
272269

273270
const copilotEvents = await pr.getCopilotTimelineEvents(pr, false, !this._model.isInitialized);
274271
let latestEvent = copilotEventToStatus(copilotEvents[copilotEvents.length - 1]);
@@ -280,74 +277,10 @@ export class CopilotPRWatcher extends Disposable {
280277
}
281278
const lastStatus = this._model.get(pr.remote.owner, pr.remote.repositoryName, pr.number) ?? CopilotPRStatus.None;
282279
if (latestEvent !== lastStatus) {
283-
changes.push({ pullRequestModel: pr, status: latestEvent });
280+
changes.push({ item: pr, status: latestEvent });
284281
}
285282
this._model.set(changes);
286283
}
287284

288-
private _getStateChangesPromise: Promise<boolean> | undefined;
289-
private async _getStateChanges(): Promise<boolean> {
290-
// Return the existing in-flight promise if one exists
291-
if (this._getStateChangesPromise) {
292-
return this._getStateChangesPromise;
293-
}
294-
295-
// Create and store the in-flight promise, and ensure it's cleared when done
296-
this._getStateChangesPromise = (async () => {
297-
try {
298-
const query = this._queriesIncludeCopilot();
299-
if (!query) {
300-
return false;
301-
}
302-
const unseenKeys: Set<string> = new Set(this._model.keys());
303-
let initialized = 0;
304-
305-
const changes: { pullRequestModel: PullRequestModel, status: CopilotPRStatus }[] = [];
306-
for (const folderManager of this._reposManager.folderManagers) {
307-
initialized++;
308-
const items: PullRequestModel[] = [];
309-
let hasMore = true;
310-
do {
311-
const prs = await folderManager.getPullRequests(PRType.Query, { fetchOnePagePerRepo: true, fetchNextPage: !this._model.isInitialized }, query);
312-
items.push(...prs.items);
313-
hasMore = prs.hasMorePages;
314-
} while (hasMore);
315-
316-
for (const pr of items) {
317-
unseenKeys.delete(this._model.makeKey(pr.remote.owner, pr.remote.repositoryName, pr.number));
318-
const copilotEvents = await pr.getCopilotTimelineEvents(pr, false, !this._model.isInitialized);
319-
let latestEvent = copilotEventToStatus(copilotEvents[copilotEvents.length - 1]);
320-
if (latestEvent === CopilotPRStatus.None) {
321-
if (!COPILOT_ACCOUNTS[pr.author.login]) {
322-
continue;
323-
}
324-
latestEvent = CopilotPRStatus.Started;
325-
}
326-
const lastStatus = this._model.get(pr.remote.owner, pr.remote.repositoryName, pr.number) ?? CopilotPRStatus.None;
327-
if (latestEvent !== lastStatus) {
328-
changes.push({ pullRequestModel: pr, status: latestEvent });
329-
}
330-
}
331-
}
332-
for (const key of unseenKeys) {
333-
this._model.deleteKey(key);
334-
}
335-
this._model.set(changes);
336-
if (!this._model.isInitialized) {
337-
if ((initialized === this._reposManager.folderManagers.length) && (this._reposManager.folderManagers.length > 0)) {
338-
Logger.debug(`Copilot PR state initialized with ${this._model.keys().length} PRs`, CopilotStateModel.ID);
339-
this._model.setInitialized();
340-
}
341-
return true;
342-
} else {
343-
return true;
344-
}
345-
} finally {
346-
// Ensure the stored promise is cleared so subsequent calls start a new run
347-
this._getStateChangesPromise = undefined;
348-
}
349-
})();
350285

351-
return this._getStateChangesPromise;
352-
}
353286
}

0 commit comments

Comments
 (0)