Skip to content

Commit bbdfce4

Browse files
authored
Coding Agent (#7043)
* createCopilotIssueTool v1 * implement jobs api * tidy * add chat participant slash cmd * command * add back slash command * tidy * shuffle around * shuffling around * consent dialog * enablement settings * delete CopilotApi * add back copilotApi * remove sandwich * tidy up user string notifications and git * quickpick * polish quickpick strings * localization * open webview * tidy up * more tidy up * redundant check * dynamically show/hide status bar icon with event * fix promise and also a string
1 parent 2714251 commit bbdfce4

File tree

11 files changed

+526
-22
lines changed

11 files changed

+526
-22
lines changed

package.json

Lines changed: 72 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,14 @@
6767
"name": "githubpr",
6868
"fullName": "GitHub Pull Requests",
6969
"description": "Chat participant for GitHub Pull Requests extension",
70-
"when": "config.githubPullRequests.experimental.chat"
70+
"when": "config.githubPullRequests.experimental.chat",
71+
"commands": [
72+
{
73+
"name": "codingAgent",
74+
"description": "Provide a task for the Copilot Coding Agent to complete asynchronously",
75+
"when": "config.githubPullRequests.codingAgent.enabled"
76+
}
77+
]
7178
}
7279
],
7380
"configuration": {
@@ -178,7 +185,7 @@
178185
"description": "%githubPullRequests.queries.query.description%"
179186
}
180187
},
181-
"default": {
188+
"default": {
182189
"label": "%githubPullRequests.queries.assignedToMe%",
183190
"query": "repo:${owner}/${repository} is:open assignee:${user}"
184191
}
@@ -459,7 +466,11 @@
459466
},
460467
"githubPullRequests.createDefaultBaseBranch": {
461468
"type": "string",
462-
"enum": ["repositoryDefault", "createdFromBranch", "auto"],
469+
"enum": [
470+
"repositoryDefault",
471+
"createdFromBranch",
472+
"auto"
473+
],
463474
"markdownEnumDescriptions": [
464475
"%githubPullRequests.createDefaultBaseBranch.repositoryDefault%",
465476
"%githubPullRequests.createDefaultBaseBranch.createdFromBranch%",
@@ -473,10 +484,30 @@
473484
"markdownDescription": "%githubPullRequests.experimental.chat.description%",
474485
"default": false
475486
},
487+
"githubPullRequests.codingAgent.enabled": {
488+
"type": "boolean",
489+
"default": false,
490+
"markdownDescription": "%githubPullRequests.codingAgent.description%",
491+
"tags": [
492+
"experimental"
493+
]
494+
},
495+
"githubPullRequests.codingAgent.autoCommitAndPush": {
496+
"type": "boolean",
497+
"default": true,
498+
"markdownDescription": "%githubPullRequests.codingAgent.autoCommitAndPush.description%",
499+
"tags": [
500+
"experimental"
501+
]
502+
},
476503
"githubPullRequests.experimental.notificationsMarkPullRequests": {
477504
"type": "string",
478505
"markdownDescription": "%githubPullRequests.experimental.notificationsMarkPullRequests.description%",
479-
"enum": ["markAsDone", "markAsRead", "none"],
506+
"enum": [
507+
"markAsDone",
508+
"markAsRead",
509+
"none"
510+
],
480511
"default": "none"
481512
},
482513
"githubPullRequests.experimental.useQuickChat": {
@@ -630,7 +661,9 @@
630661
{
631662
"label": "%githubIssues.queries.default.myIssues%",
632663
"query": "is:open assignee:${user} repo:${owner}/${repository}",
633-
"groupBy": ["milestone"]
664+
"groupBy": [
665+
"milestone"
666+
]
634667
},
635668
{
636669
"label": "%githubIssues.queries.default.createdIssues%",
@@ -1149,6 +1182,13 @@
11491182
"category": "%command.pull.request.category%",
11501183
"icon": "$(sparkle)"
11511184
},
1185+
{
1186+
"command": "pr.continueAsyncWithCopilot",
1187+
"title": "Continue Asynchronously with Copilot",
1188+
"category": "%command.pull.request.category%",
1189+
"icon": "$(copilot)",
1190+
"enablement": "config.githubPullRequests.codingAgent.enabled"
1191+
},
11521192
{
11531193
"command": "pr.addAssigneesToNewPr",
11541194
"title": "%command.pr.addAssigneesToNewPr.title%",
@@ -3285,6 +3325,33 @@
32853325
}
32863326
],
32873327
"languageModelTools": [
3328+
{
3329+
"name": "github-pull-request_copilot-coding-agent",
3330+
"displayName": "%languageModelTools.github-pull-request_copilot-coding-agent.displayName%",
3331+
"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.",
3332+
"when": "config.githubPullRequests.codingAgent.enabled",
3333+
"icon": "resources/icons/copilot.svg",
3334+
"canBeReferencedInPrompt": true,
3335+
"toolReferenceName": "codingAgent",
3336+
"userDescription": "%languageModelTools.github-pull-request_copilot-coding-agent.userDescription%",
3337+
"inputSchema": {
3338+
"type": "object",
3339+
"required": [
3340+
"title",
3341+
"body"
3342+
],
3343+
"properties": {
3344+
"title": {
3345+
"type": "string",
3346+
"description": "The title of the issue. Populate from chat context."
3347+
},
3348+
"body": {
3349+
"type": "string",
3350+
"description": "The body/description of the issue. Populate from chat context."
3351+
}
3352+
}
3353+
}
3354+
},
32883355
{
32893356
"name": "github-pull-request_issue_fetch",
32903357
"tags": [

package.nls.json

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020
"{Locked='](command:workbench.action.setLogLevel)'}"
2121
]
2222
},
23+
"githubPullRequests.codingAgent.description": "Enables integration with the asynchronous GitHub Coding Agent",
24+
"githubPullRequests.codingAgent.autoCommitAndPush.description": "Automatically commit and push changes before initiating a GitHub Coding Agent session",
2325
"githubPullRequests.remotes.markdownDescription": "List of remotes, by name, to fetch pull requests from.",
2426
"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.",
2527
"githubPullRequests.queries.label.description": "The label to display for the query in the Pull Requests tree",
@@ -368,6 +370,7 @@
368370
"languageModelTools.github-pull-request_doSearch.displayName": "Execute a GitHub search",
369371
"languageModelTools.github-pull-request_renderIssues.displayName": "Render issue items in a markdown table",
370372
"languageModelTools.github-pull-request_activePullRequest.displayName": "Active Pull Request",
371-
"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."
372-
373+
"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.",
374+
"languageModelTools.github-pull-request_copilot-coding-agent.displayName": "Copilot Coding Agent",
375+
"languageModelTools.github-pull-request_copilot-coding-agent.userDescription": "Complete a task asynchronously using the current chat context and in-progress work."
373376
}

src/commands.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ import { ITelemetry } from './common/telemetry';
1717
import { asTempStorageURI, fromPRUri, fromReviewUri, Schemes, toPRUri } from './common/uri';
1818
import { formatError } from './common/utils';
1919
import { EXTENSION_ID } from './constants';
20+
import { CopilotRemoteAgentManager } from './github/copilotRemoteAgent';
2021
import { FolderRepositoryManager } from './github/folderRepositoryManager';
2122
import { GitHubRepository } from './github/githubRepository';
2223
import { Issue } from './github/interface';
@@ -149,6 +150,7 @@ export function registerCommands(
149150
reviewsManager: ReviewsManager,
150151
telemetry: ITelemetry,
151152
tree: PullRequestsTreeDataProvider,
153+
copilotRemoteAgentManager: CopilotRemoteAgentManager,
152154
) {
153155
const logId = 'RegisterCommands';
154156
context.subscriptions.push(
@@ -1453,6 +1455,9 @@ ${contents}
14531455
handler.applySuggestion(comment);
14541456
}
14551457
}));
1458+
context.subscriptions.push(
1459+
vscode.commands.registerCommand('pr.continueAsyncWithCopilot', async () => copilotRemoteAgentManager.commandImpl())
1460+
);
14561461
context.subscriptions.push(
14571462
vscode.commands.registerCommand('pr.applySuggestionWithCopilot', async (comment: GHPRComment) => {
14581463
/* __GDPR__

src/common/settingKeys.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,9 @@ export const OPEN_VIEW = 'openView';
8080
// Explorer
8181
export const EXPLORER = 'explorer';
8282
export const AUTO_REVEAL = 'autoReveal';
83+
84+
// Coding Agent
85+
86+
export const CODING_AGENT = `${PR_SETTINGS_NAMESPACE}.codingAgent`;
87+
export const CODING_AGENT_ENABLED = 'enabled';
88+
export const CODING_AGENT_AUTO_COMMIT_AND_PUSH = 'autoCommitAndPush';

src/extension.ts

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import { TemporaryState } from './common/temporaryState';
2121
import { Schemes } from './common/uri';
2222
import { EXTENSION_ID, FOCUS_REVIEW_MODE } from './constants';
2323
import { createExperimentationService, ExperimentationTelemetry } from './experimentationService';
24+
import { CopilotRemoteAgentManager } from './github/copilotRemoteAgent';
2425
import { CredentialStore } from './github/credentials';
2526
import { FolderRepositoryManager } from './github/folderRepositoryManager';
2627
import { RepositoriesManager } from './github/repositoriesManager';
@@ -213,7 +214,25 @@ async function init(
213214

214215
context.subscriptions.push(new PRNotificationDecorationProvider(tree.notificationProvider));
215216

216-
registerCommands(context, reposManager, reviewsManager, telemetry, tree);
217+
const copilotRemoteAgentManager = new CopilotRemoteAgentManager(credentialStore, reposManager);
218+
219+
context.subscriptions.push(copilotRemoteAgentManager);
220+
let remoteAgentStatusBarItem: vscode.Disposable | undefined;
221+
if (copilotRemoteAgentManager.enabled()) {
222+
remoteAgentStatusBarItem = copilotRemoteAgentManager.statusBarItemImpl();
223+
context.subscriptions.push(remoteAgentStatusBarItem);
224+
}
225+
context.subscriptions.push(copilotRemoteAgentManager.onDidChangeEnabled((enabled: boolean) => {
226+
if (enabled && !remoteAgentStatusBarItem) {
227+
remoteAgentStatusBarItem = copilotRemoteAgentManager.statusBarItemImpl();
228+
context.subscriptions.push(remoteAgentStatusBarItem);
229+
} else if (!enabled && remoteAgentStatusBarItem) {
230+
remoteAgentStatusBarItem.dispose();
231+
remoteAgentStatusBarItem = undefined;
232+
}
233+
}));
234+
235+
registerCommands(context, reposManager, reviewsManager, telemetry, tree, copilotRemoteAgentManager);
217236

218237
const layout = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<string>(FILE_LIST_LAYOUT);
219238
await vscode.commands.executeCommand('setContext', 'fileListLayout:flat', layout === 'flat');
@@ -231,7 +250,7 @@ async function init(
231250

232251
registerPostCommitCommandsProvider(reposManager, git);
233252

234-
initChat(context, credentialStore, reposManager);
253+
initChat(context, credentialStore, reposManager, copilotRemoteAgentManager);
235254
context.subscriptions.push(vscode.window.registerUriHandler(new UriHandler(reposManager, telemetry, context)));
236255

237256
// Make sure any compare changes tabs, which come from the create flow, are closed.
@@ -242,11 +261,11 @@ async function init(
242261
telemetry.sendTelemetryEvent('startup');
243262
}
244263

245-
function initChat(context: vscode.ExtensionContext, credentialStore: CredentialStore, reposManager: RepositoriesManager) {
264+
function initChat(context: vscode.ExtensionContext, credentialStore: CredentialStore, reposManager: RepositoriesManager, copilotRemoteManager: CopilotRemoteAgentManager) {
246265
const createParticipant = () => {
247266
const chatParticipantState = new ChatParticipantState();
248267
context.subscriptions.push(new ChatParticipant(context, chatParticipantState));
249-
registerTools(context, credentialStore, reposManager, chatParticipantState);
268+
registerTools(context, credentialStore, reposManager, chatParticipantState, copilotRemoteManager);
250269
};
251270

252271
const chatEnabled = () => vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<boolean>(EXPERIMENTAL_CHAT, false);

src/github/copilotApi.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
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+
import fetch from 'cross-fetch';
7+
8+
export interface RemoteAgentJobPayload {
9+
problem_statement: string;
10+
pull_request?: {
11+
title?: string;
12+
body_placeholder?: string;
13+
body_suffix?: string;
14+
base_ref?: string;
15+
};
16+
run_name?: string;
17+
}
18+
19+
export interface RemoteAgentJobResponse {
20+
pull_request: {
21+
html_url: string;
22+
number: number;
23+
}
24+
}
25+
26+
export class CopilotApi {
27+
constructor(private token: string) { }
28+
29+
private get baseUrl(): string {
30+
return 'https://api.githubcopilot.com';
31+
}
32+
33+
async postRemoteAgentJob(
34+
owner: string,
35+
name: string,
36+
payload: RemoteAgentJobPayload,
37+
): Promise<RemoteAgentJobResponse> {
38+
const repoSlug = `${owner}/${name}`;
39+
const apiUrl = `${this.baseUrl}/agents/swe/jobs/${repoSlug}`;
40+
const response = await fetch(apiUrl, {
41+
method: 'POST',
42+
headers: {
43+
'Copilot-Integration-Id': 'copilot-developer-dev',
44+
'Authorization': `Bearer ${this.token}`,
45+
'Content-Type': 'application/json',
46+
'Accept': 'application/json'
47+
},
48+
body: JSON.stringify(payload)
49+
});
50+
if (!response.ok) {
51+
const text = await response.text();
52+
throw new Error(`Remote agent API error: ${response.status} ${text}`);
53+
}
54+
const data = await response.json();
55+
this.validateRemoteAgentJobResponse(data);
56+
return data;
57+
}
58+
59+
private validateRemoteAgentJobResponse(data: any): asserts data is RemoteAgentJobResponse {
60+
if (!data || typeof data !== 'object') {
61+
throw new Error('Invalid response from coding agent');
62+
}
63+
if (!data.pull_request || typeof data.pull_request !== 'object') {
64+
throw new Error('Invalid pull_request in response');
65+
}
66+
if (typeof data.pull_request.html_url !== 'string') {
67+
throw new Error('Invalid pull_request.html_url in response');
68+
}
69+
if (typeof data.pull_request.number !== 'number') {
70+
throw new Error('Invalid pull_request.number in response');
71+
}
72+
}
73+
}

0 commit comments

Comments
 (0)