Skip to content

Commit fb246e3

Browse files
osortegarebornix
andauthored
Performance upgrades to chat session APIs (#7719)
* Performance upgrades to chat session APIs * Fix from merge --------- Co-authored-by: Peng Lyu <penn.lv@gmail.com>
1 parent fccc441 commit fb246e3

4 files changed

Lines changed: 126 additions & 73 deletions

File tree

src/github/copilotPrWatcher.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,14 @@ export function isCopilotQuery(query: string): boolean {
2020
return COPILOT_LOGINS.some(login => lowerQuery.includes(`author:${login.toLowerCase()}`));
2121
}
2222

23+
export interface CodingAgentPRAndStatus {
24+
item: PullRequestModel;
25+
status: CopilotPRStatus;
26+
}
27+
2328
export class CopilotStateModel extends Disposable {
2429
private _isInitialized = false;
25-
private readonly _states: Map<string, { item: PullRequestModel, status: CopilotPRStatus }> = new Map();
30+
private readonly _states: Map<string, CodingAgentPRAndStatus> = new Map();
2631
private readonly _showNotification: Set<string> = new Set();
2732
private readonly _onDidChangeStates = this._register(new vscode.EventEmitter<void>());
2833
readonly onDidChangeStates = this._onDidChangeStates.event;
@@ -129,7 +134,7 @@ export class CopilotStateModel extends Disposable {
129134
};
130135
}
131136

132-
get all(): { item: PullRequestModel, status: CopilotPRStatus }[] {
137+
get all(): CodingAgentPRAndStatus[] {
133138
return Array.from(this._states.values());
134139
}
135140
}

src/github/copilotRemoteAgent.ts

Lines changed: 39 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import vscode from 'vscode';
99
import { parseSessionLogs, parseToolCallDetails, StrReplaceEditorToolData } from '../../common/sessionParsing';
1010
import { COPILOT_ACCOUNTS } from '../common/comment';
1111
import { CopilotRemoteAgentConfig } from '../common/config';
12-
import { COPILOT_LOGINS, COPILOT_SWE_AGENT, CopilotPRStatus, mostRecentCopilotEvent } from '../common/copilot';
12+
import { COPILOT_LOGINS, COPILOT_SWE_AGENT, copilotEventToStatus, CopilotPRStatus, mostRecentCopilotEvent } from '../common/copilot';
1313
import { commands } from '../common/executeCommands';
1414
import { Disposable } from '../common/lifecycle';
1515
import Logger from '../common/logger';
@@ -19,7 +19,7 @@ import { ITelemetry } from '../common/telemetry';
1919
import { toOpenPullRequestWebviewUri } from '../common/uri';
2020
import { copilotEventToSessionStatus, copilotPRStatusToSessionStatus, IAPISessionLogs, ICopilotRemoteAgentCommandArgs, ICopilotRemoteAgentCommandResponse, OctokitCommon, RemoteAgentResult, RepoInfo } from './common';
2121
import { ChatSessionFromSummarizedChat, ChatSessionWithPR, CopilotApi, getCopilotApi, RemoteAgentJobPayload, SessionInfo, SessionSetupStep } from './copilotApi';
22-
import { CopilotPRWatcher, CopilotStateModel } from './copilotPrWatcher';
22+
import { CodingAgentPRAndStatus, CopilotPRWatcher, CopilotStateModel } from './copilotPrWatcher';
2323
import { ChatSessionContentBuilder } from './copilotRemoteAgent/chatSessionContentBuilder';
2424
import { GitOperationsManager } from './copilotRemoteAgent/gitOperationsManager';
2525
import { CredentialStore } from './credentials';
@@ -61,6 +61,11 @@ export class CopilotRemoteAgentManager extends Disposable {
6161
private readonly gitOperationsManager: GitOperationsManager;
6262
private readonly ephemeralChatSessions: Map<string, ChatSessionFromSummarizedChat> = new Map();
6363

64+
private codingAgentPRsPromise: Promise<{
65+
item: PullRequestModel;
66+
status: CopilotPRStatus;
67+
}[]> | undefined;
68+
6469
constructor(private credentialStore: CredentialStore, public repositoriesManager: RepositoriesManager, private telemetry: ITelemetry, private context: vscode.ExtensionContext) {
6570
super();
6671
this.gitOperationsManager = new GitOperationsManager(CopilotRemoteAgentManager.ID);
@@ -821,7 +826,27 @@ export class CopilotRemoteAgentManager extends Disposable {
821826

822827
await this.waitRepoManagerInitialization();
823828

824-
const codingAgentPRs = this._stateModel.all;
829+
let codingAgentPRs: CodingAgentPRAndStatus[] = [];
830+
if (this._stateModel.isInitialized) {
831+
codingAgentPRs = this._stateModel.all;
832+
} else {
833+
this.codingAgentPRsPromise = this.codingAgentPRsPromise ?? new Promise<CodingAgentPRAndStatus[]>(async (resolve) => {
834+
try {
835+
const sessions = await capi.getAllCodingAgentPRs(this.repositoriesManager);
836+
const prAndStatus = await Promise.all(sessions.map(async pr => {
837+
const timeline = await pr.getCopilotTimelineEvents(pr);
838+
const status = copilotEventToStatus(mostRecentCopilotEvent(timeline));
839+
return { item: pr, status };
840+
}));
841+
842+
resolve(prAndStatus);
843+
} catch (error) {
844+
Logger.error(`Failed to fetch coding agent PRs: ${error}`, CopilotRemoteAgentManager.ID);
845+
resolve([]);
846+
}
847+
});
848+
codingAgentPRs = await this.codingAgentPRsPromise;
849+
}
825850
return await Promise.all(codingAgentPRs.map(async prAndStatus => {
826851
const timestampNumber = new Date(prAndStatus.item.createdAt).getTime();
827852
const status = copilotPRStatusToSessionStatus(prAndStatus.status);
@@ -974,7 +999,11 @@ export class CopilotRemoteAgentManager extends Disposable {
974999
return this.createEmptySession();
9751000
}
9761001

1002+
// Parallelize independent operations
1003+
const timelineEvents = pullRequest.getTimelineEvents();
1004+
const changeModels = this.getChangeModels(pullRequest);
9771005
const sessions = await capi.getAllSessions(pullRequest.id);
1006+
9781007
if (!sessions || sessions.length === 0) {
9791008
Logger.warn(`No sessions found for pull request ${pullRequestNumber}`, CopilotRemoteAgentManager.ID);
9801009
return this.createEmptySession();
@@ -984,15 +1013,16 @@ export class CopilotRemoteAgentManager extends Disposable {
9841013
Logger.error(`getAllSessions returned non-array: ${typeof sessions}`, CopilotRemoteAgentManager.ID);
9851014
return this.createEmptySession();
9861015
}
987-
const contentBuilder = new ChatSessionContentBuilder(CopilotRemoteAgentManager.ID, COPILOT, () => this.getChangeModels(pullRequest));
988-
const history = await contentBuilder.buildSessionHistory(sessions, pullRequest, capi);
989-
const activeResponseCallback = this.findActiveResponseCallback(sessions, pullRequest);
990-
const requestHandler = this.createRequestHandlerIfNeeded(pullRequest);
9911016

1017+
// Create content builder with pre-fetched change models
1018+
const contentBuilder = new ChatSessionContentBuilder(CopilotRemoteAgentManager.ID, COPILOT, changeModels);
1019+
1020+
// Parallelize operations that don't depend on each other
1021+
const history = await contentBuilder.buildSessionHistory(sessions, pullRequest, capi, timelineEvents);
9921022
return {
9931023
history,
994-
activeResponseCallback,
995-
requestHandler
1024+
activeResponseCallback: this.findActiveResponseCallback(sessions, pullRequest),
1025+
requestHandler: this.createRequestHandlerIfNeeded(pullRequest)
9961026
};
9971027
} catch (error) {
9981028
Logger.error(`Failed to provide chat session content: ${error}`, CopilotRemoteAgentManager.ID);

src/github/copilotRemoteAgent/chatSessionContentBuilder.ts

Lines changed: 47 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -21,54 +21,70 @@ export class ChatSessionContentBuilder {
2121
constructor(
2222
private loggerId: string,
2323
private readonly handler: string,
24-
private getChangeModels: () => Promise<(RemoteFileChangeModel | InMemFileChangeModel)[]>
24+
private getChangeModels: Promise<(RemoteFileChangeModel | InMemFileChangeModel)[]>
2525
) { }
2626

2727
public async buildSessionHistory(
2828
sessions: SessionInfo[],
2929
pullRequest: PullRequestModel,
30-
capi: CopilotApi
30+
capi: CopilotApi,
31+
timelineEventsPromise: Promise<TimelineEvent[]>
3132
): Promise<Array<vscode.ChatRequestTurn | vscode.ChatResponseTurn2>> {
3233
const sortedSessions = sessions.slice().sort((a, b) =>
3334
new Date(a.created_at).getTime() - new Date(b.created_at).getTime()
3435
);
3536

36-
const history: Array<vscode.ChatRequestTurn | vscode.ChatResponseTurn2> = [];
37-
const timelineEvents = await pullRequest.getTimelineEvents();
37+
// Process all sessions concurrently while maintaining order
38+
const sessionResults = await Promise.all(
39+
sortedSessions.map(async (session, sessionIndex) => {
40+
const firstHistoryEntry = async () => {
41+
const sessionPrompt = await this.determineSessionPrompt(session, sessionIndex, pullRequest, timelineEventsPromise, capi);
42+
43+
// Create request turn for this session
44+
const sessionRequest = new vscode.ChatRequestTurn2(
45+
sessionPrompt,
46+
undefined, // command
47+
[], // references
48+
COPILOT_SWE_AGENT,
49+
[], // toolReferences
50+
[]
51+
);
52+
return sessionRequest;
53+
};
54+
const secondHistoryEntry = async () => {
55+
const logs = await capi.getLogsFromSession(session.id);
56+
// Create response turn
57+
const responseHistory = await this.createResponseTurn(pullRequest, logs, session);
58+
return responseHistory;
59+
};
60+
const [first, second] = await Promise.all([
61+
firstHistoryEntry(),
62+
secondHistoryEntry(),
63+
]);
64+
65+
return { first, second, sessionIndex };
66+
})
67+
);
3868

39-
Logger.appendLine(`Found ${timelineEvents.length} timeline events`, this.loggerId);
69+
const history: Array<vscode.ChatRequestTurn | vscode.ChatResponseTurn2> = [];
4070

41-
for (const [sessionIndex, session] of sortedSessions.entries()) {
42-
const logs = await capi.getLogsFromSession(session.id);
43-
const sessionPrompt = await this.determineSessionPrompt(session, sessionIndex, pullRequest, timelineEvents, capi);
44-
45-
// Create request turn for this session
46-
const sessionRequest = new vscode.ChatRequestTurn2(
47-
sessionPrompt,
48-
undefined, // command
49-
[], // references
50-
COPILOT_SWE_AGENT,
51-
[], // toolReferences
52-
[]
53-
);
54-
history.push(sessionRequest);
71+
// Build history array in the correct order
72+
for (const { first, second, sessionIndex } of sessionResults) {
73+
history.push(first);
5574

56-
// Create response turn
57-
const responseHistory = await this.createResponseTurn(pullRequest, logs, session);
58-
if (responseHistory) {
75+
if (second) {
5976
// if this is the first response, then also add the PR card
60-
if (history.length === 1) {
77+
if (sessionIndex === 0) {
6178
const uri = await toOpenPullRequestWebviewUri({ owner: pullRequest.remote.owner, repo: pullRequest.remote.repositoryName, pullRequestNumber: pullRequest.number });
6279
const plaintextBody = marked.parse(pullRequest.body, { renderer: new PlainTextRenderer(), }).trim();
6380

6481
const card = new vscode.ChatResponsePullRequestPart(uri, pullRequest.title, plaintextBody, pullRequest.author.specialDisplayName ?? pullRequest.author.login, `#${pullRequest.number}`);
6582
const cardTurn = new vscode.ChatResponseTurn2([card], {}, COPILOT_SWE_AGENT);
6683
history.push(cardTurn);
6784
}
68-
history.push(responseHistory);
85+
history.push(second);
6986
}
7087
}
71-
7288
return history;
7389
}
7490

@@ -92,15 +108,15 @@ export class ChatSessionContentBuilder {
92108
session: SessionInfo,
93109
sessionIndex: number,
94110
pullRequest: PullRequestModel,
95-
timelineEvents: readonly TimelineEvent[],
111+
timelineEventsPromise: Promise<TimelineEvent[]>,
96112
capi: CopilotApi
97113
): Promise<string> {
98114
let sessionPrompt = session.name || `Session ${sessionIndex + 1} (ID: ${session.id})`;
99115

100116
if (sessionIndex === 0) {
101117
sessionPrompt = await this.getInitialSessionPrompt(session, pullRequest, capi, sessionPrompt);
102118
} else {
103-
sessionPrompt = await this.getFollowUpSessionPrompt(sessionIndex, timelineEvents, sessionPrompt);
119+
sessionPrompt = await this.getFollowUpSessionPrompt(sessionIndex, timelineEventsPromise, sessionPrompt);
104120
}
105121

106122
// TODO: @rebornix, remove @copilot prefix from session prompt for now
@@ -110,9 +126,11 @@ export class ChatSessionContentBuilder {
110126

111127
private async getFollowUpSessionPrompt(
112128
sessionIndex: number,
113-
timelineEvents: readonly TimelineEvent[],
129+
timelineEventsPromise: Promise<TimelineEvent[]>,
114130
defaultPrompt: string
115131
): Promise<string> {
132+
const timelineEvents = await timelineEventsPromise;
133+
Logger.appendLine(`Found ${timelineEvents.length} timeline events`, this.loggerId);
116134
const copilotStartedEvents = timelineEvents
117135
.filter((event): event is CopilotStartedEvent => event.event === EventType.CopilotStarted)
118136
.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
@@ -395,7 +413,7 @@ export class ChatSessionContentBuilder {
395413

396414
private async getFileChangesMultiDiffPart(pullRequest: PullRequestModel): Promise<vscode.ChatResponseMultiDiffPart | undefined> {
397415
try {
398-
const changeModels = await this.getChangeModels();
416+
const changeModels = await this.getChangeModels;
399417

400418
if (changeModels.length === 0) {
401419
return undefined;

src/issues/issueFeatureRegistrar.ts

Lines changed: 33 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1512,38 +1512,38 @@ ${options?.body ?? ''}\n
15121512
}
15131513
}
15141514

1515-
async assignToCodingAgent(issueModel: any) {
1516-
if (!issueModel) {
1517-
return;
1518-
}
1519-
1520-
// Check if the issue model is an IssueModel
1521-
if (!(issueModel instanceof IssueModel)) {
1522-
return;
1523-
}
1524-
1525-
try {
1526-
// Get the folder manager for this issue
1527-
const folderManager = this.manager.getManagerForIssueModel(issueModel);
1528-
if (!folderManager) {
1529-
vscode.window.showErrorMessage(vscode.l10n.t('Failed to find repository for issue #{0}', issueModel.number));
1530-
return;
1531-
}
1532-
1533-
// Get assignable users and find the copilot user
1534-
const assignableUsers = await folderManager.getAssignableUsers();
1535-
const copilotUser = assignableUsers[issueModel.remote.remoteName]?.find(user => COPILOT_ACCOUNTS[user.login]);
1536-
1537-
if (!copilotUser) {
1538-
vscode.window.showErrorMessage(vscode.l10n.t('Copilot coding agent is not available for assignment in this repository'));
1539-
return;
1540-
}
1541-
1542-
// Assign the issue to the copilot user
1543-
await issueModel.replaceAssignees([...(issueModel.assignees ?? []), copilotUser]);
1544-
vscode.window.showInformationMessage(vscode.l10n.t('Issue #{0} has been assigned to Copilot coding agent', issueModel.number));
1545-
} catch (error) {
1546-
vscode.window.showErrorMessage(vscode.l10n.t('Failed to assign issue to coding agent: {0}', error.message));
1547-
}
1515+
async assignToCodingAgent(issueModel: any) {
1516+
if (!issueModel) {
1517+
return;
1518+
}
1519+
1520+
// Check if the issue model is an IssueModel
1521+
if (!(issueModel instanceof IssueModel)) {
1522+
return;
1523+
}
1524+
1525+
try {
1526+
// Get the folder manager for this issue
1527+
const folderManager = this.manager.getManagerForIssueModel(issueModel);
1528+
if (!folderManager) {
1529+
vscode.window.showErrorMessage(vscode.l10n.t('Failed to find repository for issue #{0}', issueModel.number));
1530+
return;
1531+
}
1532+
1533+
// Get assignable users and find the copilot user
1534+
const assignableUsers = await folderManager.getAssignableUsers();
1535+
const copilotUser = assignableUsers[issueModel.remote.remoteName]?.find(user => COPILOT_ACCOUNTS[user.login]);
1536+
1537+
if (!copilotUser) {
1538+
vscode.window.showErrorMessage(vscode.l10n.t('Copilot coding agent is not available for assignment in this repository'));
1539+
return;
1540+
}
1541+
1542+
// Assign the issue to the copilot user
1543+
await issueModel.replaceAssignees([...(issueModel.assignees ?? []), copilotUser]);
1544+
vscode.window.showInformationMessage(vscode.l10n.t('Issue #{0} has been assigned to Copilot coding agent', issueModel.number));
1545+
} catch (error) {
1546+
vscode.window.showErrorMessage(vscode.l10n.t('Failed to assign issue to coding agent: {0}', error.message));
1547+
}
15481548
}
15491549
}

0 commit comments

Comments
 (0)