Skip to content
Draft
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
73 changes: 66 additions & 7 deletions src/github/copilotApi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ import Logger from '../common/logger';
import { ITelemetry } from '../common/telemetry';

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

التغييرات ممتازة، إضافة النقاط وتحسين الصياغة تخلي النص أوضح وأكثر تناسقًا 👌

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';

Expand Down Expand Up @@ -42,7 +43,7 @@ export interface RemoteAgentJobResponse {
}

export interface ChatSessionWithPR extends vscode.ChatSessionItem {
pullRequest: PullRequestModel;
pullRequest?: PullRequestModel;
}

export interface ChatSessionFromSummarizedChat extends vscode.ChatSessionItem {
Expand All @@ -58,6 +59,7 @@ export class CopilotApi {

constructor(
private octokit: LoggingOctokit,
private graphql: LoggingApolloClient,
private token: string,
private credentialStore: CredentialStore,
private telemetry: ITelemetry
Expand Down Expand Up @@ -190,11 +192,9 @@ export class CopilotApi {
return copilotSteps;
}

public async getAllSessions(pullRequestId: number | undefined): Promise<SessionInfo[]> {
public async getAllSessions(pullRequestId: number): Promise<SessionInfo[]> {
const response = await this.makeApiCall(
pullRequestId
? `/agents/sessions/resource/pull/${pullRequestId}`
: `/agents/sessions`,
`/agents/sessions/resource/pull/${pullRequestId}`,
{
headers: {
Authorization: `Bearer ${this.token}`,
Expand All @@ -208,6 +208,49 @@ export class CopilotApi {
return sessions.sessions;
}

public async getAllSessionsForAllRepositories(): Promise<SessionInfo[]> {
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<SessionPullRequestInfo | undefined> {
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<PullRequestModel[]> {
const hub = this.getHub();
const username = (await hub?.currentUser)?.login;
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
}
138 changes: 82 additions & 56 deletions src/github/copilotRemoteAgent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,17 @@ 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';
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';
Expand Down Expand Up @@ -82,10 +82,7 @@ export class CopilotRemoteAgentManager extends Disposable {
private readonly gitOperationsManager: GitOperationsManager;
private readonly ephemeralChatSessions: Map<string, ChatSessionFromSummarizedChat> = new Map();

private codingAgentPRsPromise: Promise<{
item: PullRequestModel;
status: CopilotPRStatus;
}[]> | undefined;
private codingAgentPRsPromise: Promise<ChatSessionWithPR[]> | undefined;

constructor(private credentialStore: CredentialStore, public repositoriesManager: RepositoriesManager, private telemetry: ITelemetry, private context: vscode.ExtensionContext) {
super();
Expand Down Expand Up @@ -868,7 +865,7 @@ export class CopilotRemoteAgentManager extends Disposable {
};
}

public async provideChatSessions(token: vscode.CancellationToken): Promise<ChatSessionWithPR[]> {
public async provideChatSessions(token: vscode.CancellationToken): Promise<vscode.ChatSessionItem[]> {
try {
const capi = await this.copilotApi;
if (!capi) {
Expand All @@ -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<CodingAgentPRAndStatus[]>(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<vscode.ChatSessionItem[]>(async (resolve) => {
const sessions = await capi.getAllSessionsForAllRepositories();
const sessionMap = new Map<string, SessionInfo[]>();

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 [];
}
Expand Down
18 changes: 18 additions & 0 deletions src/github/queries.gql
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down