Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 7 additions & 9 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@ import { TemporaryState } from './common/temporaryState';
import { Schemes } from './common/uri';
import { EXTENSION_ID, FOCUS_REVIEW_MODE } from './constants';
import { createExperimentationService, ExperimentationTelemetry } from './experimentationService';
import { CopilotStateModel } from './github/copilotPrWatcher';
import { CopilotRemoteAgentManager } from './github/copilotRemoteAgent';
import { CredentialStore } from './github/credentials';
import { FolderRepositoryManager } from './github/folderRepositoryManager';
Expand Down Expand Up @@ -65,7 +64,7 @@ async function init(
showPRController: ShowPullRequest,
reposManager: RepositoriesManager,
createPrHelper: CreatePullRequestHelper,
copilotStateModel: CopilotStateModel
copilotRemoteAgentManager: CopilotRemoteAgentManager,
): Promise<void> {
context.subscriptions.push(Logger);
Logger.appendLine('Git repository found, initializing review manager and pr tree view.', ACTIVATION);
Expand Down Expand Up @@ -165,7 +164,7 @@ async function init(
context.subscriptions.push(treeDecorationProviders);
treeDecorationProviders.registerProviders([new FileTypeDecorationProvider(), new CommentDecorationProvider(reposManager)]);

const reviewsManager = new ReviewsManager(context, reposManager, reviewManagers, tree, changesTree, telemetry, credentialStore, git, copilotStateModel);
const reviewsManager = new ReviewsManager(context, reposManager, reviewManagers, tree, changesTree, telemetry, credentialStore, git, copilotRemoteAgentManager);
context.subscriptions.push(reviewsManager);

git.onDidChangeState(() => {
Expand Down Expand Up @@ -217,9 +216,6 @@ async function init(

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

const copilotRemoteAgentManager = new CopilotRemoteAgentManager(credentialStore, reposManager, copilotStateModel);
context.subscriptions.push(copilotRemoteAgentManager);

registerCommands(context, reposManager, reviewsManager, telemetry, tree, copilotRemoteAgentManager);

const layout = vscode.workspace.getConfiguration(PR_SETTINGS_NAMESPACE).get<string>(FILE_LIST_LAYOUT);
Expand Down Expand Up @@ -409,8 +405,10 @@ async function deferredActivate(context: vscode.ExtensionContext, apiImpl: GitAp
const reposManager = new RepositoriesManager(credentialStore, telemetry);
context.subscriptions.push(reposManager);

const copilotStateModel = new CopilotStateModel();
const prTree = new PullRequestsTreeDataProvider(telemetry, context, reposManager, copilotStateModel);
const copilotRemoteAgentManager = new CopilotRemoteAgentManager(credentialStore, reposManager);
context.subscriptions.push(copilotRemoteAgentManager);

const prTree = new PullRequestsTreeDataProvider(telemetry, context, reposManager, copilotRemoteAgentManager);
context.subscriptions.push(prTree);
context.subscriptions.push(credentialStore.onDidGetSession(() => prTree.refresh(undefined, true)));
Logger.appendLine('Looking for git repository', ACTIVATION);
Expand All @@ -433,7 +431,7 @@ async function deferredActivate(context: vscode.ExtensionContext, apiImpl: GitAp
readOnlyMessage.isTrusted = { enabledCommands: ['pr.checkoutFromReadonlyFile'] };
context.subscriptions.push(vscode.workspace.registerFileSystemProvider(Schemes.Pr, inMemPRFileSystemProvider, { isReadonly: readOnlyMessage }));

await init(context, apiImpl, credentialStore, repositories, prTree, liveshareApiPromise, showPRController, reposManager, createPrHelper, copilotStateModel);
await init(context, apiImpl, credentialStore, repositories, prTree, liveshareApiPromise, showPRController, reposManager, createPrHelper, copilotRemoteAgentManager);
}

export async function deactivate() {
Expand Down
14 changes: 7 additions & 7 deletions src/github/copilotApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@
import fetch from 'cross-fetch';
import JSZip from 'jszip';
import { AuthProvider } from '../common/authentication';
import { Remote } from '../common/remote';
import { OctokitCommon } from './common';
import { CredentialStore } from './credentials';
import { LoggingOctokit } from './loggingOctokit';
import { PullRequestModel } from './pullRequestModel';
import { hasEnterpriseUri } from './utils';

export interface RemoteAgentJobPayload {
Expand Down Expand Up @@ -78,11 +78,11 @@ export class CopilotApi {
}
}

public async getWorkflowRunsFromAction(pullRequest: PullRequestModel): Promise<OctokitCommon.ListWorkflowRunsForRepo> {
public async getWorkflowRunsFromAction(remote: Remote): Promise<OctokitCommon.ListWorkflowRunsForRepo> {
const runs = await this.octokit.api.actions.listWorkflowRunsForRepo(
{
owner: pullRequest.githubRepository.remote.owner,
repo: pullRequest.githubRepository.remote.repositoryName,
owner: remote.owner,
repo: remote.repositoryName,
event: 'dynamic'
}
);
Expand Down Expand Up @@ -115,10 +115,10 @@ export class CopilotApi {
return copilotSteps;
}

public async getAllSessions(pullRequest: PullRequestModel | undefined): Promise<SessionInfo[]> {
public async getAllSessions(pullRequestId: number | undefined): Promise<SessionInfo[]> {
const response = await fetch(
pullRequest
? `https://api.githubcopilot.com/agents/sessions/resource/pull/${pullRequest.id}`
pullRequestId
? `https://api.githubcopilot.com/agents/sessions/resource/pull/${pullRequestId}`
: 'https://api.githubcopilot.com/agents/sessions',
{
headers: {
Expand Down
47 changes: 30 additions & 17 deletions src/github/copilotRemoteAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,12 @@ import vscode from 'vscode';
import { Repository } from '../api/api';
import { AuthProvider } from '../common/authentication';
import { Disposable } from '../common/lifecycle';
import { Remote } from '../common/remote';
import { CODING_AGENT, CODING_AGENT_AUTO_COMMIT_AND_PUSH, CODING_AGENT_ENABLED } from '../common/settingKeys';
import { toOpenPullRequestWebviewUri } from '../common/uri';
import { CopilotApi, RemoteAgentJobPayload } from './copilotApi';
import { CopilotPRWatcher, CopilotStateModel } from './copilotPrWatcher';
import { CredentialStore } from './credentials';
import { PullRequestModel } from './pullRequestModel';
import { RepositoriesManager } from './repositoriesManager';

type RemoteAgentSuccessResult = { link: string; state: 'success'; number: number; webviewUri: vscode.Uri; llmDetails: string };
Expand All @@ -25,25 +25,29 @@ export interface IAPISessionLogs {
}

export class CopilotRemoteAgentManager extends Disposable {
private readonly _onDidChangeEnabled = new vscode.EventEmitter<boolean>();
public readonly onDidChangeEnabled: vscode.Event<boolean> = this._onDidChangeEnabled.event;
public static ID = 'CopilotRemoteAgentManager';
private readonly workflowRunUrlBase = 'https://github.com/microsoft/vscode/actions/runs/';

constructor(private credentialStore: CredentialStore, public repositoriesManager: RepositoriesManager, stateModel: CopilotStateModel) {
private readonly _stateModel: CopilotStateModel;
private readonly _onDidChangeStates = this._register(new vscode.EventEmitter<void>());
readonly onDidChangeStates = this._onDidChangeStates.event;
private readonly _onDidChangeNotifications = this._register(new vscode.EventEmitter<void>());
readonly onDidChangeNotifications = this._onDidChangeNotifications.event;
private readonly _onDidCreatePullRequest = this._register(new vscode.EventEmitter<number>());
readonly onDidCreatePullRequest = this._onDidCreatePullRequest.event;

constructor(private credentialStore: CredentialStore, public repositoriesManager: RepositoriesManager) {
super();
this._register(this.credentialStore.onDidChangeSessions((e: vscode.AuthenticationSessionsChangeEvent) => {
if (e.provider.id === 'github') {
this._copilotApiPromise = undefined; // Invalidate cached session
}
}));
this._register(vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(`${CODING_AGENT}.${CODING_AGENT_ENABLED}`)) {
this._onDidChangeEnabled.fire(this.enabled());
}
}));
this._register(new CopilotPRWatcher(this.repositoriesManager, stateModel));

this._stateModel = new CopilotStateModel();
this._register(new CopilotPRWatcher(this.repositoriesManager, this._stateModel));
this._register(this._stateModel.onDidChangeStates(() => this._onDidChangeStates.fire()));
this._register(this._stateModel.onDidChangeNotifications(() => this._onDidChangeNotifications.fire()));
}

private _copilotApiPromise: Promise<CopilotApi | undefined> | undefined;
Expand Down Expand Up @@ -258,6 +262,7 @@ export class CopilotRemoteAgentManager extends Disposable {
const { pull_request } = await capiClient.postRemoteAgentJob(owner, repo, payload);
const webviewUri = await toOpenPullRequestWebviewUri({ owner, repo, pullRequestNumber: pull_request.number });
const prLlmString = `The remote agent has begun work. The user can track progress by visiting ${pull_request.html_url} or from the PR extension.`;
this._onDidCreatePullRequest.fire(pull_request.number);
return {
state: 'success',
number: pull_request.number,
Expand All @@ -267,15 +272,15 @@ export class CopilotRemoteAgentManager extends Disposable {
};
}

async getSessionLogsFromAction(pullRequest: PullRequestModel) {
async getSessionLogsFromAction(remote: Remote, pullRequestId: number) {
const capi = await this.copilotApi;
if (!capi) {
return [];
}
const runs = await capi.getWorkflowRunsFromAction(pullRequest);
const runs = await capi.getWorkflowRunsFromAction(remote);
const padawanRuns = runs
.filter(run => run.path && run.path.startsWith('dynamic/copilot-swe-agent'))
.filter(run => run.pull_requests?.some(pr => pr.id === pullRequest.id));
.filter(run => run.pull_requests?.some(pr => pr.id === pullRequestId));

const lastRun = this.getLatestRun(padawanRuns);

Expand All @@ -286,13 +291,13 @@ export class CopilotRemoteAgentManager extends Disposable {
return await capi.getLogsFromZipUrl(lastRun.logs_url);
}

async getSessionLogsFromPullRequest(pullRequest: PullRequestModel): Promise<IAPISessionLogs> {
async getSessionLogsFromPullRequest(pullRequestId: number): Promise<IAPISessionLogs> {
const capi = await this.copilotApi;
if (!capi) {
return { sessionId: '', logs: '' };
}

const sessions = await capi.getAllSessions(pullRequest);
const sessions = await capi.getAllSessions(pullRequestId);
const completedSessions = sessions.filter(s => s.state === 'completed');
if (completedSessions.length === 0) {
return { sessionId: '', logs: '' };
Expand All @@ -302,13 +307,13 @@ export class CopilotRemoteAgentManager extends Disposable {
return { sessionId: mostRecentSession.id, logs };
}

async getSessionUrlFromPullRequest(pullRequest: PullRequestModel): Promise<string | undefined> {
async getSessionUrlFromPullRequest(pullRequestId: number | undefined): Promise<string | undefined> {
const capi = await this.copilotApi;
if (!capi) {
return undefined;
}

const sessions = await capi.getAllSessions(pullRequest);
const sessions = await capi.getAllSessions(pullRequestId);
const completedSessions = sessions.filter(s => s.state === 'completed');
if (completedSessions.length === 0) {
return undefined;
Expand Down Expand Up @@ -336,4 +341,12 @@ export class CopilotRemoteAgentManager extends Disposable {
return dateB - dateA;
})[0];
}

clearNotifications() {
this._stateModel.clearNotifications();
}

get notifications(): ReadonlySet<string> {
return this._stateModel.notifications;
}
}
2 changes: 1 addition & 1 deletion src/github/pullRequestOverview.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,7 @@ export class PullRequestOverviewPanel extends IssueOverviewPanel<PullRequestMode
} else {
const copilotApi = await getCopilotApi(this._folderRepositoryManager.credentialStore, this._item.remote.authProviderId);
if (copilotApi) {
const session = (await copilotApi.getAllSessions(this._item))[0];
const session = (await copilotApi.getAllSessions(this._item.id))[0];
if (session.state !== 'completed') {
result = await this._item.githubRepository.cancelWorkflow(session.workflow_run_id);
}
Expand Down
4 changes: 2 additions & 2 deletions src/lm/tools/activePullRequestTool.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export class ActivePullRequestTool implements vscode.LanguageModelTool<FetchIssu
model: vscode.LanguageModelChat,
cancellationToken: vscode.CancellationToken
) {
const logs = await this.copilotRemoteAgentManager.getSessionLogsFromAction(pullRequest);
const logs = await this.copilotRemoteAgentManager.getSessionLogsFromAction(pullRequest.remote, pullRequest.id);
// Summarize the Copilot agent's thinking process using the model
const messages = [
vscode.LanguageModelChatMessage.Assistant('You are an expert summarizer. The following logs show the thinking process and performed actions of a GitHub Copilot agent that was in charge of working on the current pull request. Read the logs and always maintain the thinking process. You can remove information on the tool call results that you think are not necessary for building context.'),
Expand All @@ -98,7 +98,7 @@ export class ActivePullRequestTool implements vscode.LanguageModelTool<FetchIssu
): Promise<string | string[]> {
let copilotSteps: string | string[] = [];
try {
const logsResponseText = await this.copilotRemoteAgentManager.getSessionLogsFromPullRequest(pullRequest);
const logsResponseText = await this.copilotRemoteAgentManager.getSessionLogsFromPullRequest(pullRequest.id);
copilotSteps = this.parseCopilotEventStream(logsResponseText.logs);
if (copilotSteps.length === 0) {
throw new Error('Empty Copilot agent logs received');
Expand Down
8 changes: 4 additions & 4 deletions src/test/view/prsTree.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ import { DataUri } from '../../common/uri';
import { IAccount, ITeam } from '../../github/interface';
import { asPromise } from '../../common/utils';
import { CreatePullRequestHelper } from '../../view/createPullRequestHelper';
import { CopilotStateModel } from '../../github/copilotPrWatcher';
import { CopilotRemoteAgentManager } from '../../github/copilotRemoteAgent';

describe('GitHub Pull Requests view', function () {
let sinon: SinonSandbox;
Expand All @@ -41,7 +41,7 @@ describe('GitHub Pull Requests view', function () {
let credentialStore: CredentialStore;
let reposManager: RepositoriesManager;
let createPrHelper: CreatePullRequestHelper;
let copilotStateModel: CopilotStateModel;
let copilotManager: CopilotRemoteAgentManager;

beforeEach(function () {
sinon = createSandbox();
Expand All @@ -54,9 +54,9 @@ describe('GitHub Pull Requests view', function () {
credentialStore,
telemetry,
);
copilotStateModel = new CopilotStateModel();
provider = new PullRequestsTreeDataProvider(telemetry, context, reposManager, copilotStateModel);
credentialStore = new CredentialStore(telemetry, context);
copilotManager = new CopilotRemoteAgentManager(credentialStore, reposManager);
provider = new PullRequestsTreeDataProvider(telemetry, context, reposManager, copilotManager);
createPrHelper = new CreatePullRequestHelper();

// For tree view unit tests, we don't test the authentication flow, so `showSignInNotification` returns
Expand Down
9 changes: 4 additions & 5 deletions src/test/view/reviewCommentController.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,9 +37,8 @@ import { WebviewViewCoordinator } from '../../view/webviewViewCoordinator';
import { GitHubServerType } from '../../common/authentication';
import { CreatePullRequestHelper } from '../../view/createPullRequestHelper';
import { mergeQuerySchemaWithShared } from '../../github/common';
import { GitHubRef } from '../../common/githubRef';
import { AccountType } from '../../github/interface';
import { CopilotStateModel } from '../../github/copilotPrWatcher';
import { CopilotRemoteAgentManager } from '../../github/copilotRemoteAgent';
const schema = mergeQuerySchemaWithShared(require('../../github/queries.gql'), require('../../github/queriesShared.gql')) as any;

const protocol = new Protocol('https://github.com/github/test.git');
Expand All @@ -63,7 +62,7 @@ describe('ReviewCommentController', function () {
let reviewManager: ReviewManager;
let reposManager: RepositoriesManager;
let gitApiImpl: GitApiImpl;
let copilotStateModel: CopilotStateModel;
let copilotManager: CopilotRemoteAgentManager;

beforeEach(async function () {
sinon = createSandbox();
Expand All @@ -76,8 +75,8 @@ describe('ReviewCommentController', function () {
repository = new MockRepository();
repository.addRemote('origin', 'git@github.com:aaa/bbb');
reposManager = new RepositoriesManager(credentialStore, telemetry);
copilotStateModel = new CopilotStateModel();
provider = new PullRequestsTreeDataProvider(telemetry, context, reposManager, copilotStateModel);
copilotManager = new CopilotRemoteAgentManager(credentialStore, reposManager);
provider = new PullRequestsTreeDataProvider(telemetry, context, reposManager, copilotManager);
const activePrViewCoordinator = new WebviewViewCoordinator(context);
const createPrHelper = new CreatePullRequestHelper();
Resource.initialize(context);
Expand Down
10 changes: 5 additions & 5 deletions src/view/prStatusDecorationProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import * as vscode from 'vscode';
import { Disposable } from '../common/lifecycle';
import { COPILOT_QUERY, createPRNodeUri, fromPRNodeUri, Schemes } from '../common/uri';
import { CopilotStateModel } from '../github/copilotPrWatcher';
import { CopilotRemoteAgentManager } from '../github/copilotRemoteAgent';
import { getStatusDecoration } from '../github/markdownUtils';
import { PrsTreeModel } from './prsTreeModel';

Expand All @@ -17,7 +17,7 @@ export class PRStatusDecorationProvider extends Disposable implements vscode.Fil
>();
onDidChangeFileDecorations: vscode.Event<vscode.Uri | vscode.Uri[]> = this._onDidChangeFileDecorations.event;

constructor(private readonly _prsTreeModel: PrsTreeModel, private readonly _copilotStateModel: CopilotStateModel) {
constructor(private readonly _prsTreeModel: PrsTreeModel, private readonly _copilotManager: CopilotRemoteAgentManager) {
super();
this._register(vscode.window.registerFileDecorationProvider(this));
this._register(
Expand All @@ -26,7 +26,7 @@ export class PRStatusDecorationProvider extends Disposable implements vscode.Fil
})
);

this._register(this._copilotStateModel.onDidChangeNotifications(() => {
this._register(this._copilotManager.onDidChangeNotifications(() => {
this._onDidChangeFileDecorations.fire(COPILOT_QUERY);
}));
}
Expand Down Expand Up @@ -56,9 +56,9 @@ export class PRStatusDecorationProvider extends Disposable implements vscode.Fil

private _queryDecoration(uri: vscode.Uri): vscode.ProviderResult<vscode.FileDecoration> {
if (uri.path === 'copilot') {
if (this._copilotStateModel.notifications.size > 0) {
if (this._copilotManager.notifications.size > 0) {
return {
tooltip: vscode.l10n.t('Coding agent has made changes', this._copilotStateModel.notifications.size),
tooltip: vscode.l10n.t('Coding agent has made changes', this._copilotManager.notifications.size),
badge: new vscode.ThemeIcon('copilot') as any,
color: new vscode.ThemeColor('pullRequests.notification'),
};
Expand Down
Loading