Skip to content

Commit 2565b75

Browse files
authored
Show Copilot Coding Agent tasks overall progress (#7297)
Fixes #7148
1 parent 5999d37 commit 2565b75

File tree

9 files changed

+91
-19
lines changed

9 files changed

+91
-19
lines changed

src/github/copilotPrWatcher.ts

Lines changed: 49 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,26 @@ export class CopilotStateModel extends Disposable {
4646
}
4747
}
4848

49-
set(pullRequestModel: PullRequestModel, status: CopilotPRStatus): void {
50-
const key = this.makeKey(pullRequestModel.remote.owner, pullRequestModel.remote.repositoryName, pullRequestModel.number);
51-
const currentStatus = this._states.get(key);
52-
if (currentStatus?.status === status) {
53-
return;
49+
set(statuses: { pullRequestModel: PullRequestModel, status: CopilotPRStatus }[]): void {
50+
const changedModels: PullRequestModel[] = [];
51+
const changedKeys: string[] = [];
52+
for (const { pullRequestModel, status } of statuses) {
53+
const key = this.makeKey(pullRequestModel.remote.owner, pullRequestModel.remote.repositoryName, pullRequestModel.number);
54+
const currentStatus = this._states.get(key);
55+
if (currentStatus?.status === status) {
56+
continue;
57+
}
58+
this._states.set(key, { item: pullRequestModel, status });
59+
changedModels.push(pullRequestModel);
60+
changedKeys.push(key);
5461
}
55-
this._states.set(key, { item: pullRequestModel, status });
5662
if (this._isInitialized) {
57-
this._showNotification.add(key);
58-
this._onDidChangeNotifications.fire(pullRequestModel ? [pullRequestModel] : []);
63+
changedKeys.forEach(key => this._showNotification.add(key));
64+
this._onDidChangeNotifications.fire(changedModels);
65+
}
66+
if (changedModels.length > 0) {
67+
this._onDidChangeStates.fire();
5968
}
60-
this._onDidChangeStates.fire();
6169
}
6270

6371
get(owner: string, repo: string, prNumber: number): CopilotPRStatus {
@@ -86,13 +94,42 @@ export class CopilotStateModel extends Disposable {
8694
get isInitialized(): boolean {
8795
return this._isInitialized;
8896
}
97+
98+
getCounts(): { total: number; inProgress: number; error: number } {
99+
let inProgressCount = 0;
100+
let errorCount = 0;
101+
102+
for (const state of this._states.values()) {
103+
if (state.status === CopilotPRStatus.Started) {
104+
inProgressCount++;
105+
} else if (state.status === CopilotPRStatus.Failed) {
106+
errorCount++;
107+
}
108+
}
109+
110+
return {
111+
total: this._states.size,
112+
inProgress: inProgressCount,
113+
error: errorCount
114+
};
115+
}
89116
}
90117

91118
export class CopilotPRWatcher extends Disposable {
92119

93120
constructor(private readonly _reposManager: RepositoriesManager, private readonly _model: CopilotStateModel) {
94121
super();
122+
if (this._reposManager.folderManagers.length === 0) {
123+
const initDisposable = this._reposManager.onDidChangeAnyGitHubRepository(() => {
124+
initDisposable.dispose();
125+
this._initialize();
126+
});
127+
} else {
128+
this._initialize();
129+
}
130+
}
95131

132+
private _initialize() {
96133
this._getStateChanges();
97134
this._pollForChanges();
98135
this._register(this._reposManager.onDidChangeAnyPullRequests(() => this._getStateChanges()));
@@ -138,6 +175,7 @@ export class CopilotPRWatcher extends Disposable {
138175
const unseenKeys: Set<string> = new Set(this._model.keys());
139176
let initialized = 0;
140177

178+
const changes: { pullRequestModel: PullRequestModel, status: CopilotPRStatus }[] = [];
141179
for (const folderManager of this._reposManager.folderManagers) {
142180
// It doesn't matter which repo we use since the query will specify the owner/repo.
143181
const githubRepository = folderManager.gitHubRepositories[0];
@@ -161,14 +199,15 @@ export class CopilotPRWatcher extends Disposable {
161199
prNumber: pr.number,
162200
status: latestEvent
163201
});
164-
this._model.set(pr, latestEvent);
202+
changes.push({ pullRequestModel: pr, status: latestEvent });
165203
}
166204
}
167205

168206
for (const key of unseenKeys) {
169207
this._model.deleteKey(key);
170208
}
171209
}
210+
this._model.set(changes);
172211
if (!this._model.isInitialized) {
173212
if ((initialized === this._reposManager.folderManagers.length) && (this._reposManager.folderManagers.length > 0)) {
174213
this._model.setInitialized();

src/github/copilotRemoteAgent.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -668,7 +668,11 @@ export class CopilotRemoteAgentManager extends Disposable {
668668
return this._stateModel.notifications.has(key);
669669
}
670670

671-
public getStateForPR(owner: string, repo: string, prNumber: number): CopilotPRStatus {
671+
getStateForPR(owner: string, repo: string, prNumber: number): CopilotPRStatus {
672672
return this._stateModel.get(owner, repo, prNumber);
673673
}
674+
675+
getCounts(): { total: number; inProgress: number; error: number } {
676+
return this._stateModel.getCounts();
677+
}
674678
}

src/github/repositoriesManager.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,9 @@ export class RepositoriesManager extends Disposable {
4848
private _onDidChangeAnyPullRequests = new vscode.EventEmitter<IssueModel[]>();
4949
readonly onDidChangeAnyPullRequests = this._onDidChangeAnyPullRequests.event;
5050

51+
private _onDidAddAnyGitHubRepository = new vscode.EventEmitter<FolderRepositoryManager>();
52+
readonly onDidChangeAnyGitHubRepository = this._onDidAddAnyGitHubRepository.event;
53+
5154
private _state: ReposManagerState = ReposManagerState.Initializing;
5255

5356
constructor(
@@ -82,6 +85,7 @@ export class RepositoriesManager extends Disposable {
8285
folderManager.onDidChangeActivePullRequest(() => this.updateActiveReviewCount()),
8386
folderManager.onDidDispose(() => this.removeRepo(folderManager.repository)),
8487
folderManager.onDidChangeAnyPullRequests(e => this._onDidChangeAnyPullRequests.fire(e)),
88+
folderManager.onDidChangeGithubRepositories(() => this._onDidAddAnyGitHubRepository.fire(folderManager)),
8589
];
8690
this._subs.set(folderManager, disposables);
8791
}

src/view/prsTreeDataProvider.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,9 @@ export class PullRequestsTreeDataProvider extends Disposable implements vscode.T
110110
} else {
111111
this._view.badge = undefined;
112112
}
113+
114+
// also need to refresh the Copilot query category to update the status
115+
this.refresh(undefined);
113116
}));
114117

115118
this._register(this._copilotManager.onDidCreatePullRequest(() => this.refresh(undefined, true)));

src/view/treeNodes/categoryNode.ts

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,7 @@ export class CategoryTreeNode extends TreeNode implements vscode.TreeItem {
131131
public contextValue: string;
132132
public resourceUri: vscode.Uri;
133133
public tooltip?: string | vscode.MarkdownString | undefined;
134+
readonly isCopilot: boolean;
134135

135136
constructor(
136137
parent: TreeNodeParent,
@@ -146,8 +147,8 @@ export class CategoryTreeNode extends TreeNode implements vscode.TreeItem {
146147
super(parent);
147148

148149
this.prs = new Map();
149-
const isCopilot = _categoryQuery && isCopilotQuery(_categoryQuery);
150-
const hasCopilotChanges = isCopilot && this._copilotManager.notificationsCount > 0;
150+
this.isCopilot = !!_categoryQuery && isCopilotQuery(_categoryQuery);
151+
const hasCopilotChanges = this.isCopilot && this._copilotManager.notificationsCount > 0;
151152

152153
switch (this.type) {
153154
case PRType.All:
@@ -184,7 +185,7 @@ export class CategoryTreeNode extends TreeNode implements vscode.TreeItem {
184185
this.contextValue = 'query';
185186
}
186187

187-
if (isCopilot) {
188+
if (this.isCopilot) {
188189
this.tooltip = vscode.l10n.t('Pull requests you asked the coding agent to create');
189190
} else if (this.type === PRType.LocalPullRequest) {
190191
this.tooltip = vscode.l10n.t('Pull requests for branches you have locally');
@@ -195,6 +196,28 @@ export class CategoryTreeNode extends TreeNode implements vscode.TreeItem {
195196
} else {
196197
this.tooltip = this.label;
197198
}
199+
200+
this.description = this._getDescription();
201+
}
202+
203+
private _getDescription(): string | undefined {
204+
if (!this.isCopilot) {
205+
return undefined;
206+
}
207+
const counts = this._copilotManager.getCounts();
208+
if (counts.total === 0) {
209+
return undefined;
210+
} else if (counts.error > 0) {
211+
if (counts.inProgress > 0) {
212+
return vscode.l10n.t('{0} in progress, {1} with errors', counts.inProgress, counts.error);
213+
} else {
214+
return vscode.l10n.t('{0} with errors', counts.error);
215+
}
216+
} else if (counts.inProgress > 0) {
217+
return vscode.l10n.t('{0} in progress', counts.inProgress);
218+
} else {
219+
return vscode.l10n.t('done working on {0}', counts.total);
220+
}
198221
}
199222

200223
public async expandPullRequest(pullRequest: PullRequestModel, retry: boolean = true): Promise<boolean> {

src/view/treeNodes/commitNode.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ export class CommitNode extends TreeNode implements vscode.TreeItem {
2222
public collapsibleState: vscode.TreeItemCollapsibleState;
2323
public iconPath: vscode.Uri | undefined;
2424
public contextValue?: string;
25-
public description: string | undefined;
2625

2726
constructor(
2827
parent: TreeNodeParent,

src/view/treeNodes/fileChangeNode.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -139,13 +139,14 @@ export class FileChangeNode extends TreeNode implements vscode.TreeItem {
139139
);
140140

141141
this.accessibilityInformation = { label: `${this.label} pull request diff`, role: 'link' };
142+
this.description = this._getDescription();
142143
}
143144

144145
get resourceUri(): vscode.Uri {
145146
return this.changeModel.filePath.with({ query: this.fileChangeResourceUri.query });
146147
}
147148

148-
get description(): string | true {
149+
protected _getDescription(): string | true {
149150
const layout = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<string>(FILE_LIST_LAYOUT);
150151
if (layout === 'flat') {
151152
return true;
@@ -221,7 +222,7 @@ export class FileChangeNode extends TreeNode implements vscode.TreeItem {
221222
* File change node whose content can not be resolved locally and we direct users to GitHub.
222223
*/
223224
export class RemoteFileChangeNode extends FileChangeNode implements vscode.TreeItem {
224-
override get description(): string {
225+
protected override _getDescription(): string {
225226
let description = vscode.workspace.asRelativePath(path.dirname(this.changeModel.fileName), false);
226227
if (description === '.') {
227228
description = '';
@@ -406,7 +407,6 @@ export class GitFileChangeNode extends FileChangeNode implements vscode.TreeItem
406407
* File change node whose content is resolved from GitHub. For files not yet associated with a pull request.
407408
*/
408409
export class GitHubFileChangeNode extends TreeNode implements vscode.TreeItem {
409-
public description: string;
410410
public iconPath: vscode.ThemeIcon;
411411
public fileChangeResourceUri: vscode.Uri;
412412
public readonly tooltip: string;

src/view/treeNodes/repositoryChangesNode.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,6 @@ export class RepositoryChangesNode extends TreeNode implements vscode.TreeItem {
2323
public contextValue?: string;
2424
public tooltip: string;
2525
public iconPath: vscode.ThemeIcon | vscode.Uri | undefined;
26-
public description?: string;
2726
readonly collapsibleState = vscode.TreeItemCollapsibleState.Expanded;
2827
private isLocal: boolean;
2928
public readonly repository: Repository;

src/view/treeNodes/treeNode.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ export abstract class TreeNode extends Disposable {
2222
label?: string;
2323
accessibilityInformation?: vscode.AccessibilityInformation;
2424
id?: string;
25+
description?: string | boolean;
2526

2627
constructor(public parent: TreeNodeParent) {
2728
super();

0 commit comments

Comments
 (0)