Skip to content

Commit db34b84

Browse files
authored
Add cancel coding agent button (#7079)
Fixes microsoft/vscode-copilot#18809
1 parent d57159e commit db34b84

File tree

14 files changed

+198
-60
lines changed

14 files changed

+198
-60
lines changed

src/@types/vscode.proposed.chatParticipantPrivate.d.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,16 @@ declare module 'vscode' {
190190
terminalCommand?: string;
191191
}
192192

193+
export interface LanguageModelToolInvocationPrepareOptions<T> {
194+
/**
195+
* The input that the tool is being invoked with.
196+
*/
197+
input: T;
198+
chatRequestId?: string;
199+
chatSessionId?: string;
200+
chatInteractionId?: string;
201+
}
202+
193203
export interface PreparedToolInvocation {
194204
pastTenseMessage?: string | MarkdownString;
195205
presentation?: 'hidden' | undefined;
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
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+
// empty placeholder for coding agent contribution point from core
7+
8+
// @joshspicer

src/common/copilot.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,44 @@
33
* Licensed under the MIT License. See License.txt in the project root for license information.
44
*--------------------------------------------------------------------------------------------*/
55

6+
import { EventType, TimelineEvent } from './timelineEvent';
7+
68
export const COPILOT_LOGINS = [
79
'copilot-pull-request-reviewer',
810
'copilot-swe-agent',
911
'Copilot'
10-
];
12+
];
13+
14+
export enum CopilotPRStatus {
15+
None = 0,
16+
Started = 1,
17+
Completed = 2,
18+
Failed = 3,
19+
}
20+
21+
export function copilotEventToStatus(event: TimelineEvent | undefined): CopilotPRStatus {
22+
if (!event) {
23+
return CopilotPRStatus.None;
24+
}
25+
26+
switch (event.event) {
27+
case EventType.CopilotStarted:
28+
return CopilotPRStatus.Started;
29+
case EventType.CopilotFinished:
30+
return CopilotPRStatus.Completed;
31+
case EventType.CopilotFinishedError:
32+
return CopilotPRStatus.Failed;
33+
default:
34+
return CopilotPRStatus.None;
35+
}
36+
}
37+
38+
export function mostRecentCopilotEvent(events: TimelineEvent[]): TimelineEvent | undefined {
39+
for (let i = events.length - 1; i >= 0; i--) {
40+
const status = copilotEventToStatus(events[i]);
41+
if (status !== CopilotPRStatus.None) {
42+
return events[i];
43+
}
44+
}
45+
return undefined;
46+
}

src/github/copilotApi.ts

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,12 @@
55

66
import fetch from 'cross-fetch';
77
import JSZip from 'jszip';
8+
import { AuthProvider } from '../common/authentication';
89
import { OctokitCommon } from './common';
10+
import { CredentialStore } from './credentials';
911
import { LoggingOctokit } from './loggingOctokit';
1012
import { PullRequestModel } from './pullRequestModel';
13+
import { hasEnterpriseUri } from './utils';
1114

1215
export interface RemoteAgentJobPayload {
1316
problem_statement: string;
@@ -168,7 +171,7 @@ export interface SessionInfo {
168171
agent_id: number;
169172
logs: string;
170173
logs_blob_id: string;
171-
state: string;
174+
state: 'completed' | 'in_progress' | string;
172175
owner_id: number;
173176
repo_id: number;
174177
resource_type: string;
@@ -181,3 +184,23 @@ export interface SessionInfo {
181184
premium_requests: number;
182185
error: string | null;
183186
}
187+
188+
export async function getCopilotApi(credentialStore: CredentialStore, authProvider?: AuthProvider): Promise<CopilotApi | undefined> {
189+
if (!authProvider) {
190+
if (credentialStore.isAuthenticated(AuthProvider.githubEnterprise) && hasEnterpriseUri()) {
191+
authProvider = AuthProvider.githubEnterprise;
192+
} else if (credentialStore.isAuthenticated(AuthProvider.github)) {
193+
authProvider = AuthProvider.github;
194+
} else {
195+
return;
196+
}
197+
}
198+
199+
const github = credentialStore.getHub(authProvider);
200+
if (!github || !github.octokit) {
201+
return;
202+
}
203+
204+
const { token } = await github.octokit.api.auth() as { token: string };
205+
return new CopilotApi(github.octokit, token);
206+
}

src/github/copilotPrWatcher.ts

Lines changed: 1 addition & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,18 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as vscode from 'vscode';
7-
import { COPILOT_LOGINS } from '../common/copilot';
7+
import { COPILOT_LOGINS, copilotEventToStatus, CopilotPRStatus } from '../common/copilot';
88
import { Disposable } from '../common/lifecycle';
99
import { PR_SETTINGS_NAMESPACE, QUERIES } from '../common/settingKeys';
10-
import { EventType, TimelineEvent } from '../common/timelineEvent';
1110
import { FolderRepositoryManager } from './folderRepositoryManager';
1211
import { RepositoriesManager } from './repositoriesManager';
1312
import { variableSubstitution } from './utils';
1413

15-
export enum CopilotPRStatus {
16-
None = 0,
17-
Started = 1,
18-
Completed = 2,
19-
Failed = 3,
20-
}
21-
2214
export function isCopilotQuery(query: string): boolean {
2315
const lowerQuery = query.toLowerCase();
2416
return COPILOT_LOGINS.some(login => lowerQuery.includes(`author:${login.toLowerCase()}`));
2517
}
2618

27-
function copilotEventToStatus(event: TimelineEvent): CopilotPRStatus {
28-
switch (event.event) {
29-
case EventType.CopilotStarted:
30-
return CopilotPRStatus.Started;
31-
case EventType.CopilotFinished:
32-
return CopilotPRStatus.Completed;
33-
case EventType.CopilotFinishedError:
34-
return CopilotPRStatus.Failed;
35-
default:
36-
return CopilotPRStatus.None;
37-
}
38-
}
39-
4019
export class CopilotStateModel extends Disposable {
4120
private _isInitialized = false;
4221
private readonly _states: Map<string, CopilotPRStatus> = new Map();

src/github/githubRepository.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1269,6 +1269,22 @@ export class GitHubRepository extends Disposable {
12691269
return ret;
12701270
}
12711271

1272+
async cancelWorkflow(workflowRunId: number): Promise<boolean> {
1273+
Logger.debug(`Cancel workflow run - enter`, this.id);
1274+
const { octokit, remote } = await this.ensure();
1275+
try {
1276+
const result = await octokit.call(octokit.api.actions.cancelWorkflowRun, {
1277+
owner: remote.owner,
1278+
repo: remote.repositoryName,
1279+
run_id: workflowRunId,
1280+
});
1281+
return result.status === 202;
1282+
} catch (e) {
1283+
Logger.error(`Unable to cancel workflow run: ${e}`, this.id);
1284+
return false;
1285+
}
1286+
}
1287+
12721288
async getOrgTeamsCount(): Promise<number> {
12731289
Logger.debug(`Fetch Teams Count - enter`, this.id);
12741290
if (!this._credentialStore.isAuthenticatedWithAdditionalScopes(this.remote.authProviderId)) {

src/github/pullRequestModel.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1509,7 +1509,7 @@ export class PullRequestModel extends IssueModel<PullRequest> implements IPullRe
15091509
const { query, remote, schema } = await this.githubRepository.ensure();
15101510

15111511
// hard code the users for selfhost purposes
1512-
const { data } = (schema.PullRequestMergeabilityMergeRequirements && ((await this.credentialStore.getCurrentUser(this.remote.authProviderId))?.login === 'alexr00')) ? await query<PullRequestMergabilityResponse>({
1512+
const { data } = /*(schema.PullRequestMergeabilityMergeRequirements && ((await this.credentialStore.getCurrentUser(this.remote.authProviderId))?.login === 'alexr00')) ? await query<PullRequestMergabilityResponse>({
15131513
query: schema.PullRequestMergeabilityMergeRequirements,
15141514
variables: {
15151515
owner: remote.owner,
@@ -1521,7 +1521,7 @@ export class PullRequestModel extends IssueModel<PullRequest> implements IPullRe
15211521
'GraphQL-Features': 'pull_request_merge_requirements_api' // This flag allows specific users to test a private field.
15221522
}
15231523
}
1524-
}) : await query<PullRequestMergabilityResponse>({
1524+
}) : */await query<PullRequestMergabilityResponse>({
15251525
query: schema.PullRequestMergeability,
15261526
variables: {
15271527
owner: remote.owner,

src/github/pullRequestOverview.ts

Lines changed: 41 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,15 +7,17 @@
77
import * as vscode from 'vscode';
88
import { openPullRequestOnGitHub } from '../commands';
99
import { IComment } from '../common/comment';
10+
import { copilotEventToStatus, CopilotPRStatus, mostRecentCopilotEvent } from '../common/copilot';
1011
import { commands, contexts } from '../common/executeCommands';
1112
import { disposeAll } from '../common/lifecycle';
1213
import Logger from '../common/logger';
1314
import { DEFAULT_MERGE_METHOD, PR_SETTINGS_NAMESPACE } from '../common/settingKeys';
1415
import { ITelemetry } from '../common/telemetry';
15-
import { ReviewEvent, SessionPullInfo } from '../common/timelineEvent';
16+
import { EventType, ReviewEvent, SessionPullInfo, TimelineEvent } from '../common/timelineEvent';
1617
import { asPromise, formatError } from '../common/utils';
1718
import { IRequestMessage, PULL_REQUEST_OVERVIEW_VIEW_TYPE } from '../common/webview';
1819
import { SessionLogViewManager } from '../view/sessionLogView';
20+
import { getCopilotApi } from './copilotApi';
1921
import { FolderRepositoryManager } from './folderRepositoryManager';
2022
import {
2123
GithubItemStateEnum,
@@ -34,7 +36,7 @@ import { PullRequestModel } from './pullRequestModel';
3436
import { PullRequestView } from './pullRequestOverviewCommon';
3537
import { pickEmail, reviewersQuickPick } from './quickPicks';
3638
import { parseReviewers } from './utils';
37-
import { MergeArguments, MergeResult, PullRequest, ReviewType, SubmitReviewReply } from './views';
39+
import { CancelCodingAgentReply, MergeArguments, MergeResult, PullRequest, ReviewType, SubmitReviewReply } from './views';
3840

3941
export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestModel> {
4042
public static override ID: string = 'PullRequestOverviewPanel';
@@ -359,6 +361,8 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
359361
return this.revert(message);
360362
case 'pr.open-session-log':
361363
return this.openSessionLog(message.args.link);
364+
case 'pr.cancel-coding-agent':
365+
return this.cancelCodingAgent(message);
362366
}
363367
}
364368

@@ -462,6 +466,41 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
462466
}
463467
}
464468

469+
private async cancelCodingAgent(message: IRequestMessage<TimelineEvent>): Promise<void> {
470+
try {
471+
let result = false;
472+
if (message.args.event !== EventType.CopilotStarted) {
473+
return this._replyMessage(message, { success: false, error: 'Invalid event type' });
474+
} else {
475+
const copilotApi = await getCopilotApi(this._folderRepositoryManager.credentialStore, this._item.remote.authProviderId);
476+
if (copilotApi) {
477+
const session = (await copilotApi.getAllSessions(this._item))[0];
478+
if (session.state !== 'completed') {
479+
result = await this._item.githubRepository.cancelWorkflow(session.workflow_run_id);
480+
}
481+
}
482+
}
483+
// need to wait until we get the updated timeline events
484+
let events: TimelineEvent[] = [];
485+
if (result) {
486+
do {
487+
events = await this._item.getTimelineEvents();
488+
} while (copilotEventToStatus(mostRecentCopilotEvent(events)) !== CopilotPRStatus.Completed && await new Promise<boolean>(c => setTimeout(() => c(true), 2000)));
489+
}
490+
const reply: CancelCodingAgentReply = {
491+
events
492+
};
493+
this._replyMessage(message, reply);
494+
} catch (e) {
495+
Logger.error(`Cancelling coding agent failed: ${formatError(e)}`, PullRequestOverviewPanel.ID);
496+
vscode.window.showErrorMessage(vscode.l10n.t('Cannot cancel coding agent'));
497+
const reply: CancelCodingAgentReply = {
498+
events: [],
499+
};
500+
this._replyMessage(message, reply);
501+
}
502+
}
503+
465504
private async openChanges(): Promise<void> {
466505
return PullRequestModel.openChanges(this._folderRepositoryManager, this._item);
467506
}

src/github/views.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,7 @@ export enum PreReviewState {
133133
ReviewedWithComments,
134134
ReviewedWithoutComments
135135
}
136+
137+
export interface CancelCodingAgentReply {
138+
events: TimelineEvent[];
139+
}

src/view/sessionLogView.ts

Lines changed: 1 addition & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,16 +5,14 @@
55

66
import * as vscode from 'vscode';
77
import type * as messages from '../../webviews/sessionLogView/messages';
8-
import { AuthProvider } from '../common/authentication';
98
import { Disposable } from '../common/lifecycle';
109
import { ITelemetry } from '../common/telemetry';
1110
import { SessionPullInfo } from '../common/timelineEvent';
12-
import { CopilotApi } from '../github/copilotApi';
11+
import { CopilotApi, getCopilotApi } from '../github/copilotApi';
1312
import { CredentialStore } from '../github/credentials';
1413
import { PullRequestModel } from '../github/pullRequestModel';
1514
import { PullRequestOverviewPanel } from '../github/pullRequestOverview';
1615
import { RepositoriesManager } from '../github/repositoriesManager';
17-
import { hasEnterpriseUri } from '../github/utils';
1816

1917
export class SessionLogViewManager extends Disposable implements vscode.WebviewPanelSerializer {
2018
public static instance: SessionLogViewManager | undefined;
@@ -308,26 +306,6 @@ class SessionLogView extends Disposable {
308306
}
309307
}
310308

311-
async function getCopilotApi(credentialStore: CredentialStore): Promise<CopilotApi | undefined> {
312-
let authProvider: AuthProvider | undefined;
313-
if (credentialStore.isAuthenticated(AuthProvider.githubEnterprise) && hasEnterpriseUri()) {
314-
authProvider = AuthProvider.githubEnterprise;
315-
} else if (credentialStore.isAuthenticated(AuthProvider.github)) {
316-
authProvider = AuthProvider.github;
317-
} else {
318-
return;
319-
}
320-
321-
const github = credentialStore.getHub(authProvider);
322-
if (!github || !github.octokit) {
323-
return;
324-
}
325-
326-
const { token } = await github.octokit.api.auth() as { token: string };
327-
return new CopilotApi(github.octokit, token);
328-
}
329-
330-
331309
async function loadCurrentThemeData(): Promise<any> {
332310
let themeData: any = null;
333311
const currentThemeName = vscode.workspace.getConfiguration('workbench').get<string>('colorTheme');

0 commit comments

Comments
 (0)