diff --git a/src/github/copilotApi.ts b/src/github/copilotApi.ts index edb8b88740..b36ecb6c7f 100644 --- a/src/github/copilotApi.ts +++ b/src/github/copilotApi.ts @@ -12,8 +12,9 @@ import Logger from '../common/logger'; import { ITelemetry } from '../common/telemetry'; import { CredentialStore, GitHub } from './credentials'; import { PRType } from './interface'; -import { LoggingOctokit } from './loggingOctokit'; +import { LoggingApolloClient, LoggingOctokit } from './loggingOctokit'; import { PullRequestModel } from './pullRequestModel'; +import defaultSchema from './queries.gql'; import { RepositoriesManager } from './repositoriesManager'; import { hasEnterpriseUri } from './utils'; @@ -42,7 +43,7 @@ export interface RemoteAgentJobResponse { } export interface ChatSessionWithPR extends vscode.ChatSessionItem { - pullRequest: PullRequestModel; + pullRequest?: PullRequestModel; } export interface ChatSessionFromSummarizedChat extends vscode.ChatSessionItem { @@ -58,6 +59,7 @@ export class CopilotApi { constructor( private octokit: LoggingOctokit, + private graphql: LoggingApolloClient, private token: string, private credentialStore: CredentialStore, private telemetry: ITelemetry @@ -190,11 +192,9 @@ export class CopilotApi { return copilotSteps; } - public async getAllSessions(pullRequestId: number | undefined): Promise { + public async getAllSessions(pullRequestId: number): Promise { const response = await this.makeApiCall( - pullRequestId - ? `/agents/sessions/resource/pull/${pullRequestId}` - : `/agents/sessions`, + `/agents/sessions/resource/pull/${pullRequestId}`, { headers: { Authorization: `Bearer ${this.token}`, @@ -208,6 +208,49 @@ export class CopilotApi { return sessions.sessions; } + public async getAllSessionsForAllRepositories(): Promise { + let hasNextPage = false; + const sessionInfos: SessionInfo[] = []; + const page_size = 20; + let page = 1; + do { + const response = await this.makeApiCall( + `/agents/sessions?page_size=${page_size}&page_number=${page}`, + { + headers: { + Authorization: `Bearer ${this.token}`, + Accept: 'application/json', + }, + }); + if (!response.ok) { + throw new Error(`Failed to fetch sessions: ${response.statusText}`); + } + const sessions = await response.json(); + sessionInfos.push(...sessions.sessions); + hasNextPage = sessions.sessions.length === page_size; + page++; + } while (hasNextPage); + + return sessionInfos; + } + + public async getPullRequestFromSession(globalId): Promise { + try { + const { data } = await this.graphql.query({ + query: (defaultSchema as any).GetPullRequestGlobal, + variables: { + globalId: globalId + } + }); + + return data.node; + } catch (ex) { + console.log(ex); + } + + return undefined; + } + public async getAllCodingAgentPRs(repositoriesManager: RepositoriesManager): Promise { const hub = this.getHub(); const username = (await hub?.currentUser)?.login; @@ -311,6 +354,22 @@ export interface SessionInfo { workflow_run_id: number; premium_requests: number; error: string | null; + resource_global_id?: string + resource_state: 'open' | 'closed' | 'draft' | 'merged'; +} + +export interface SessionPullRequestInfo { + number: number; + title: string; + state: 'OPEN' | 'CLOSED' | 'MERGED'; + additions: number; + deletions: number; + headRepository: { + owner: { + login: string; + }; + name: string; + }; } export interface SessionSetupStep { @@ -363,5 +422,5 @@ export async function getCopilotApi(credentialStore: CredentialStore, telemetry: } const { token } = await github.octokit.api.auth() as { token: string }; - return new CopilotApi(github.octokit, token, credentialStore, telemetry); + return new CopilotApi(github.octokit, github.graphql, token, credentialStore, telemetry); } \ No newline at end of file diff --git a/src/github/copilotRemoteAgent.ts b/src/github/copilotRemoteAgent.ts index f8cd3994ce..ef0876ba9d 100644 --- a/src/github/copilotRemoteAgent.ts +++ b/src/github/copilotRemoteAgent.ts @@ -9,7 +9,7 @@ import vscode, { ChatPromptReference } from 'vscode'; import { parseSessionLogs, parseToolCallDetails, StrReplaceEditorToolData } from '../../common/sessionParsing'; import { COPILOT_ACCOUNTS } from '../common/comment'; import { CopilotRemoteAgentConfig } from '../common/config'; -import { COPILOT_LOGINS, COPILOT_SWE_AGENT, copilotEventToStatus, CopilotPRStatus, mostRecentCopilotEvent } from '../common/copilot'; +import { COPILOT_LOGINS, COPILOT_SWE_AGENT, CopilotPRStatus, mostRecentCopilotEvent } from '../common/copilot'; import { commands } from '../common/executeCommands'; import { Disposable } from '../common/lifecycle'; import Logger from '../common/logger'; @@ -17,9 +17,9 @@ import { GitHubRemote } from '../common/remote'; import { CODING_AGENT, CODING_AGENT_AUTO_COMMIT_AND_PUSH } from '../common/settingKeys'; import { ITelemetry } from '../common/telemetry'; import { toOpenPullRequestWebviewUri } from '../common/uri'; -import { copilotEventToSessionStatus, copilotPRStatusToSessionStatus, IAPISessionLogs, ICopilotRemoteAgentCommandArgs, ICopilotRemoteAgentCommandResponse, OctokitCommon, RemoteAgentResult, RepoInfo } from './common'; -import { ChatSessionFromSummarizedChat, ChatSessionWithPR, CopilotApi, getCopilotApi, RemoteAgentJobPayload, SessionInfo, SessionSetupStep } from './copilotApi'; -import { CodingAgentPRAndStatus, CopilotPRWatcher, CopilotStateModel } from './copilotPrWatcher'; +import { copilotEventToSessionStatus, IAPISessionLogs, ICopilotRemoteAgentCommandArgs, ICopilotRemoteAgentCommandResponse, OctokitCommon, RemoteAgentResult, RepoInfo } from './common'; +import { ChatSessionFromSummarizedChat, ChatSessionWithPR, CopilotApi, getCopilotApi, RemoteAgentJobPayload, SessionInfo, SessionPullRequestInfo, SessionSetupStep } from './copilotApi'; +import { CopilotPRWatcher, CopilotStateModel } from './copilotPrWatcher'; import { ChatSessionContentBuilder } from './copilotRemoteAgent/chatSessionContentBuilder'; import { GitOperationsManager } from './copilotRemoteAgent/gitOperationsManager'; import { CredentialStore } from './credentials'; @@ -82,10 +82,7 @@ export class CopilotRemoteAgentManager extends Disposable { private readonly gitOperationsManager: GitOperationsManager; private readonly ephemeralChatSessions: Map = new Map(); - private codingAgentPRsPromise: Promise<{ - item: PullRequestModel; - status: CopilotPRStatus; - }[]> | undefined; + private codingAgentPRsPromise: Promise | undefined; constructor(private credentialStore: CredentialStore, public repositoriesManager: RepositoriesManager, private telemetry: ITelemetry, private context: vscode.ExtensionContext) { super(); @@ -868,7 +865,7 @@ export class CopilotRemoteAgentManager extends Disposable { }; } - public async provideChatSessions(token: vscode.CancellationToken): Promise { + public async provideChatSessions(token: vscode.CancellationToken): Promise { try { const capi = await this.copilotApi; if (!capi) { @@ -882,58 +879,87 @@ export class CopilotRemoteAgentManager extends Disposable { await this.waitRepoManagerInitialization(); - let codingAgentPRs: CodingAgentPRAndStatus[] = []; - if (this._stateModel.isInitialized) { - codingAgentPRs = this._stateModel.all; - Logger.debug(`Fetched PRs from state model: ${codingAgentPRs.length}`, CopilotRemoteAgentManager.ID); - } else { - this.codingAgentPRsPromise = this.codingAgentPRsPromise ?? new Promise(async (resolve) => { - try { - const sessions = await capi.getAllCodingAgentPRs(this.repositoriesManager); - const prAndStatus = await Promise.all(sessions.map(async pr => { - const timeline = await pr.getCopilotTimelineEvents(pr); - const status = copilotEventToStatus(mostRecentCopilotEvent(timeline)); - return { item: pr, status }; - })); - - resolve(prAndStatus); - } catch (error) { - Logger.error(`Failed to fetch coding agent PRs: ${error}`, CopilotRemoteAgentManager.ID); - resolve([]); + const currentRepositories = this.repositoriesManager.folderManagers.map(folder => folder.gitHubRepositories).flat(); + + this.codingAgentPRsPromise = this.codingAgentPRsPromise ?? new Promise(async (resolve) => { + const sessions = await capi.getAllSessionsForAllRepositories(); + const sessionMap = new Map(); + + for (const session of sessions) { + if (!session.resource_global_id || session.resource_state === 'closed' || session.resource_state === 'merged') { + continue; } + const existing = sessionMap.get(session.resource_global_id) || []; + existing.push(session); + existing.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()); + sessionMap.set(session.resource_global_id, existing); + } + + const groupedSessions = Array.from(sessionMap.values()).sort((a, b) => { + const aFirstSession = a[0]; + const bFirstSession = b[0]; + return new Date(bFirstSession.created_at).getTime() - new Date(aFirstSession.created_at).getTime(); }); - codingAgentPRs = await this.codingAgentPRsPromise; - Logger.debug(`Fetched PRs from API: ${codingAgentPRs.length}`, CopilotRemoteAgentManager.ID); - } - return await Promise.all(codingAgentPRs.map(async prAndStatus => { - const timestampNumber = new Date(prAndStatus.item.createdAt).getTime(); - const status = copilotPRStatusToSessionStatus(prAndStatus.status); - const pullRequest = prAndStatus.item; - const tooltip = await issueMarkdown(pullRequest, this.context, this.repositoriesManager); - - const uri = await toOpenPullRequestWebviewUri({ owner: pullRequest.remote.owner, repo: pullRequest.remote.repositoryName, pullRequestNumber: pullRequest.number }); - const description = new vscode.MarkdownString(`[#${pullRequest.number}](${uri.toString()})`); // pullRequest.base.ref === defaultBranch ? `PR #${pullRequest.number}`: `PR #${pullRequest.number} → ${pullRequest.base.ref}`; - return { - id: `${pullRequest.number}`, - label: pullRequest.title || `Session ${pullRequest.number}`, - iconPath: this.getIconForSession(status), - pullRequest: pullRequest, - description: description, - tooltip, - status, - timing: { - startTime: timestampNumber - }, - statistics: pullRequest.item.additions !== undefined && pullRequest.item.deletions !== undefined && (pullRequest.item.additions > 0 || pullRequest.item.deletions > 0) ? { - insertions: pullRequest.item.additions, - deletions: pullRequest.item.deletions - } : undefined - }; - })); + + const filteredPRs = (await Promise.all(groupedSessions.map(async sessions => { + const initialSession = sessions[0]; + + const pullRequestInfo = await capi.getPullRequestFromSession(initialSession.resource_global_id); + if (!pullRequestInfo) { + return; + } + + return { + id: initialSession.resource_global_id, + pullRequest: pullRequestInfo, + sessions + }; + }))).filter(pr => { + if (!pr) { + return false; + } + + if (pr.pullRequest.state !== 'OPEN') { + return false; + } + + // Filter out PRs that are not in the current repositories + const prRepo = currentRepositories.find(repo => + repo.remote.owner === pr.pullRequest.headRepository.owner.login && + repo.remote.repositoryName === pr.pullRequest.headRepository.name + ); + + if (!prRepo) { + return false; + } + + return true; + }).map((pr: { id: string, pullRequest: SessionPullRequestInfo; sessions: SessionInfo[] }) => { + const { id, pullRequest, sessions } = pr; + const latestSession = sessions[sessions.length - 1]; + const status = latestSession.state === 'completed' ? vscode.ChatSessionStatus.Completed : (latestSession.state === 'failed' ? vscode.ChatSessionStatus.Failed : vscode.ChatSessionStatus.InProgress); + + return { + id: id, + label: pullRequest.title || `Session ${pullRequest.number}`, + status: status, + description: `#${pullRequest.number}`, + statistics: pullRequest.additions !== undefined && pullRequest.deletions !== undefined && (pullRequest.additions > 0 || pullRequest.deletions > 0) ? { + insertions: pullRequest.additions, + deletions: pullRequest.deletions + } : undefined + }; + }); + + resolve(filteredPRs); + this.codingAgentPRsPromise = undefined; + }); + + return this.codingAgentPRsPromise; } catch (error) { Logger.error(`Failed to provide coding agents information: ${error}`, CopilotRemoteAgentManager.ID); } finally { - this.codingAgentPRsPromise = undefined; + //todo, previously we set codingAgentPRsPromise to undefined here, but that caused issues with multiple simultaneous calls } return []; } diff --git a/src/github/queries.gql b/src/github/queries.gql index 9412fd955a..3e2a4d48c1 100644 --- a/src/github/queries.gql +++ b/src/github/queries.gql @@ -378,6 +378,24 @@ query GetSuggestedActors($owner: String!, $name: String!, $capabilities: [Reposi } } +query GetPullRequestGlobal($globalId: ID!) { + node(id: $globalId) { + ... on PullRequest { + number + title + state + additions + deletions + headRepository { + owner { + login + } + name + } + } + } +} + mutation DequeuePullRequest($input: DequeuePullRequestInput!) { dequeuePullRequest(input: $input) { mergeQueueEntry {