Skip to content

Commit 5f48d91

Browse files
authored
Adding chat sessions provider (#7354)
* Adding chat sessions provider * Review comments * Missing change * Review comments * Revert change * Update query
1 parent 5e87b97 commit 5f48d91

File tree

6 files changed

+145
-5
lines changed

6 files changed

+145
-5
lines changed

package.json

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
"activeComment",
1515
"chatParticipantAdditions",
1616
"chatParticipantPrivate",
17+
"chatSessionsProvider",
1718
"codiconDecoration",
1819
"codeActionRanges",
1920
"commentingRangeHint",
@@ -3273,6 +3274,12 @@
32733274
"command": "review.requestChangesOnDotComDescription",
32743275
"when": "webviewId == PullRequestOverview && github:reviewCommentMenu && github:reviewCommentRequestChangesOnDotCom"
32753276
}
3277+
],
3278+
"chat/chatSessions": [
3279+
{
3280+
"command": "pr.openDescription",
3281+
"when": "chatSessionType == github.pullRequest.codingAgent"
3282+
}
32763283
]
32773284
},
32783285
"colors": [
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/*---------------------------------------------------------------------------------------------
2+
* Copyright (c) Microsoft Corporation. All rights reserved.
3+
* Licensed under the MIT License. See License.txt in the project root for license information.
4+
*--------------------------------------------------------------------------------------------*/
5+
6+
declare module 'vscode' {
7+
/**
8+
* Provides a list of chat sessions
9+
*/
10+
export interface ChatSessionsProvider extends Disposable {
11+
/**
12+
* Type to identify providers.
13+
*/
14+
readonly chatSessionType: string;
15+
16+
/**
17+
* Fired when chat sessions change.
18+
*/
19+
readonly onDidChangeChatSessionContent: Event<void>;
20+
21+
/**
22+
* Provide a list of chat sessions.
23+
* */
24+
provideChatSessions(token: CancellationToken): Thenable<ChatSessionContent[]>;
25+
}
26+
27+
export interface ChatSessionContent {
28+
/**
29+
* Identifies the session
30+
* */
31+
uri: Uri;
32+
33+
/**
34+
* Human readable name of the session shown in the UI
35+
*/
36+
label: string;
37+
38+
/**
39+
* An icon for the participant shown in UI.
40+
*/
41+
iconPath?: IconPath;
42+
}
43+
44+
export namespace chat {
45+
export function registerChatSessionsProvider(provider: ChatSessionsProvider): Disposable;
46+
}
47+
}

src/commands.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { ITelemetry } from './common/telemetry';
1818
import { asTempStorageURI, fromPRUri, fromReviewUri, Schemes, toPRUri } from './common/uri';
1919
import { formatError } from './common/utils';
2020
import { EXTENSION_ID } from './constants';
21+
import { ChatSessionWithPR } from './github/copilotApi';
2122
import { CopilotRemoteAgentManager, ICopilotRemoteAgentCommandArgs } from './github/copilotRemoteAgent';
2223
import { FolderRepositoryManager } from './github/folderRepositoryManager';
2324
import { GitHubRepository } from './github/githubRepository';
@@ -201,6 +202,11 @@ export async function closeAllPrAndReviewEditors() {
201202
}
202203
}
203204

205+
function isChatSessionWithPR(value: any): value is ChatSessionWithPR {
206+
const asChatSessionWithPR = value as Partial<ChatSessionWithPR>;
207+
return !!asChatSessionWithPR.pullRequest;
208+
}
209+
204210
export function registerCommands(
205211
context: vscode.ExtensionContext,
206212
reposManager: RepositoriesManager,
@@ -835,7 +841,7 @@ export function registerCommands(
835841
}),
836842
);
837843

838-
async function openDescriptionCommand(argument: RepositoryChangesNode | PRNode | IssueModel | undefined) {
844+
async function openDescriptionCommand(argument: RepositoryChangesNode | PRNode | IssueModel | ChatSessionWithPR | undefined) {
839845
let issueModel: IssueModel | undefined;
840846
if (!argument) {
841847
const activePullRequests: PullRequestModel[] = reposManager.folderManagers
@@ -852,6 +858,8 @@ export function registerCommands(
852858
issueModel = argument.pullRequestModel;
853859
} else if (argument instanceof PRNode) {
854860
issueModel = argument.pullRequestModel;
861+
} else if (isChatSessionWithPR(argument)) {
862+
issueModel = argument.pullRequest;
855863
} else {
856864
issueModel = argument;
857865
}

src/extension.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,15 @@ async function deferredActivate(context: vscode.ExtensionContext, showPRControll
411411

412412
const copilotRemoteAgentManager = new CopilotRemoteAgentManager(credentialStore, reposManager, telemetry);
413413
context.subscriptions.push(copilotRemoteAgentManager);
414+
context.subscriptions.push(vscode.chat.registerChatSessionsProvider({
415+
chatSessionType: 'github.pullRequest.codingAgent',
416+
provideChatSessions: async (token) => {
417+
return await copilotRemoteAgentManager.provideChatSessions(token);
418+
},
419+
// Events not used yet, but required by interface.
420+
onDidChangeChatSessionContent: new vscode.EventEmitter<void>().event,
421+
dispose: () => { }
422+
}));
414423

415424
const prTree = new PullRequestsTreeDataProvider(telemetry, context, reposManager, copilotRemoteAgentManager);
416425
context.subscriptions.push(prTree);

src/github/copilotApi.ts

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,11 @@ import * as vscode from 'vscode';
99
import { AuthProvider } from '../common/authentication';
1010
import Logger from '../common/logger';
1111
import { ITelemetry } from '../common/telemetry';
12-
import { CredentialStore } from './credentials';
12+
import { CredentialStore, GitHub } from './credentials';
13+
import { PRType } from './interface';
1314
import { LoggingOctokit } from './loggingOctokit';
15+
import { PullRequestModel } from './pullRequestModel';
16+
import { RepositoriesManager } from './repositoriesManager';
1417
import { hasEnterpriseUri } from './utils';
1518

1619
const LEARN_MORE_URL = 'https://docs.github.com/en/copilot/how-tos/agents/copilot-coding-agent';
@@ -35,10 +38,19 @@ export interface RemoteAgentJobResponse {
3538
}
3639
}
3740

41+
export interface ChatSessionWithPR extends vscode.ChatSessionContent {
42+
pullRequest: PullRequestModel;
43+
}
44+
3845
export class CopilotApi {
3946
protected static readonly ID = 'copilotApi';
4047

41-
constructor(private octokit: LoggingOctokit, private token: string, private telemetry: ITelemetry) { }
48+
constructor(
49+
private octokit: LoggingOctokit,
50+
private token: string,
51+
private credentialStore: CredentialStore,
52+
private telemetry: ITelemetry
53+
) { }
4254

4355
private get baseUrl(): string {
4456
return 'https://api.githubcopilot.com';
@@ -173,6 +185,23 @@ export class CopilotApi {
173185
return sessions.sessions;
174186
}
175187

188+
public async getAllCodingAgentPRs(repositoriesManager: RepositoriesManager): Promise<PullRequestModel[]> {
189+
const hub = this.getHub();
190+
const username = (await hub?.currentUser)?.login;
191+
if (!username) {
192+
Logger.error('Failed to get GitHub username from auth provider', CopilotApi.ID);
193+
return [];
194+
}
195+
const query = `is:open author:copilot-swe-agent[bot] assignee:${username} is:pr`;
196+
const allItems = await Promise.all(
197+
repositoriesManager.folderManagers.map(async fm => {
198+
const result = await fm.getPullRequests(PRType.Query, undefined, query);
199+
return result.items;
200+
})
201+
);
202+
return allItems.flat();
203+
}
204+
176205
public async getSessionInfo(sessionId: string): Promise<SessionInfo> {
177206
const response = await fetch(`https://api.githubcopilot.com/agents/sessions/${sessionId}`, {
178207
method: 'GET',
@@ -201,6 +230,19 @@ export class CopilotApi {
201230
}
202231
return await logsResponse.text();
203232
}
233+
234+
private getHub(): GitHub | undefined {
235+
let authProvider: AuthProvider | undefined;
236+
if (this.credentialStore.isAuthenticated(AuthProvider.githubEnterprise) && hasEnterpriseUri()) {
237+
authProvider = AuthProvider.githubEnterprise;
238+
} else if (this.credentialStore.isAuthenticated(AuthProvider.github)) {
239+
authProvider = AuthProvider.github;
240+
} else {
241+
return;
242+
}
243+
244+
return this.credentialStore.getHub(authProvider);
245+
}
204246
}
205247

206248

@@ -242,5 +284,5 @@ export async function getCopilotApi(credentialStore: CredentialStore, telemetry:
242284
}
243285

244286
const { token } = await github.octokit.api.auth() as { token: string };
245-
return new CopilotApi(github.octokit, token, telemetry);
287+
return new CopilotApi(github.octokit, token, credentialStore, telemetry);
246288
}

src/github/copilotRemoteAgent.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { CODING_AGENT, CODING_AGENT_AUTO_COMMIT_AND_PUSH, CODING_AGENT_ENABLED }
1414
import { ITelemetry } from '../common/telemetry';
1515
import { toOpenPullRequestWebviewUri } from '../common/uri';
1616
import { OctokitCommon } from './common';
17-
import { CopilotApi, getCopilotApi, RemoteAgentJobPayload, SessionInfo } from './copilotApi';
17+
import { ChatSessionWithPR, CopilotApi, getCopilotApi, RemoteAgentJobPayload, SessionInfo } from './copilotApi';
1818
import { CopilotPRWatcher, CopilotStateModel } from './copilotPrWatcher';
1919
import { CredentialStore } from './credentials';
2020
import { FolderRepositoryManager } from './folderRepositoryManager';
@@ -681,4 +681,31 @@ export class CopilotRemoteAgentManager extends Disposable {
681681
getCounts(): { total: number; inProgress: number; error: number } {
682682
return this._stateModel.getCounts();
683683
}
684+
685+
public async provideChatSessions(token: vscode.CancellationToken): Promise<ChatSessionWithPR[]> {
686+
try {
687+
const capi = await this.copilotApi;
688+
if (!capi) {
689+
return [];
690+
}
691+
692+
// Check if the token is already cancelled
693+
if (token.isCancellationRequested) {
694+
return [];
695+
}
696+
697+
const sessions = await capi.getAllCodingAgentPRs(this.repositoriesManager);
698+
return sessions.map(session => {
699+
return {
700+
uri: vscode.Uri.parse(`github://coding-agent/${session.id}`),
701+
label: session.title || `Session ${session.id}`,
702+
iconPath: undefined,
703+
pullRequest: session
704+
};
705+
});
706+
} catch (error) {
707+
Logger.error(`Failed to provide coding agents information: ${error}`, CopilotRemoteAgentManager.ID);
708+
}
709+
return [];
710+
}
684711
}

0 commit comments

Comments
 (0)