Skip to content

Commit 4485827

Browse files
committed
experiment chat editor integration
1 parent b72d161 commit 4485827

File tree

3 files changed

+275
-1
lines changed

3 files changed

+275
-1
lines changed

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

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@ declare module 'vscode' {
2222
* Provides a list of chat sessions.
2323
*/
2424
provideChatSessionItems(token: CancellationToken): ProviderResult<ChatSessionItem[]>;
25+
26+
provideChatSessionContent(id: string, token: CancellationToken): Thenable<ChatSession>;
2527
}
2628

2729
export interface ChatSessionItem {
@@ -41,6 +43,64 @@ declare module 'vscode' {
4143
iconPath?: IconPath;
4244
}
4345

46+
export class ChatResponseTurn2 {
47+
/**
48+
* The content that was received from the chat participant. Only the stream parts that represent actual content (not metadata) are represented.
49+
*/
50+
readonly response: ReadonlyArray<ChatResponseMarkdownPart | ChatResponseFileTreePart | ChatResponseAnchorPart | ChatResponseCommandButtonPart | ExtendedChatResponsePart>;
51+
52+
/**
53+
* The result that was received from the chat participant.
54+
*/
55+
readonly result: ChatResult;
56+
57+
/**
58+
* The id of the chat participant that this response came from.
59+
*/
60+
readonly participant: string;
61+
62+
/**
63+
* The name of the command that this response came from.
64+
*/
65+
readonly command?: string;
66+
67+
/**
68+
* @hidden
69+
*/
70+
constructor(response: ReadonlyArray<ChatResponseMarkdownPart | ChatResponseFileTreePart | ChatResponseAnchorPart | ChatResponseCommandButtonPart | ExtendedChatResponsePart>, result: ChatResult, participant: string);
71+
}
72+
73+
export interface ChatSession {
74+
75+
/**
76+
* The full history of the session
77+
*
78+
* This should not include any currently active responses
79+
*
80+
* TODO: Are these the right types to use?
81+
* TODO: link request + response to encourage correct usage?
82+
*/
83+
readonly history: ReadonlyArray<ChatRequestTurn | ChatResponseTurn2>;
84+
85+
/**
86+
* Callback invoked by the editor for a currently running response. This allows the session to push items for the
87+
* current response and stream these in as them come in. The current response will be considered complete once the
88+
* callback resolved.
89+
*
90+
* If not provided, the chat session is assumed to not currently be running.
91+
*/
92+
readonly activeResponseCallback?: (stream: ChatResponseStream, token: CancellationToken) => Thenable<void>;
93+
94+
/**
95+
* Handles new request for the session.
96+
*
97+
* If not set, then the session will be considered read-only and no requests can be made.
98+
*
99+
* TODO: Should we introduce our own type for `ChatRequestHandler` since not all field apply to chat sessions?
100+
*/
101+
readonly requestHandler: ChatRequestHandler | undefined;
102+
}
103+
44104
export namespace chat {
45105
export function registerChatSessionItemProvider(chatSessionType: string, provider: ChatSessionItemProvider): Disposable;
46106
}

src/extension.ts

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -411,6 +411,7 @@ async function deferredActivate(context: vscode.ExtensionContext, showPRControll
411411

412412
const copilotRemoteAgentManager = new CopilotRemoteAgentManager(credentialStore, reposManager, telemetry);
413413
context.subscriptions.push(copilotRemoteAgentManager);
414+
<<<<<<< Updated upstream
414415
if (vscode.chat?.registerChatSessionItemProvider) {
415416
context.subscriptions.push(vscode.chat?.registerChatSessionItemProvider(
416417
'copilot-swe-agent',
@@ -424,6 +425,22 @@ async function deferredActivate(context: vscode.ExtensionContext, showPRControll
424425
}
425426
));
426427
}
428+
=======
429+
context.subscriptions.push(vscode.chat.registerChatSessionItemProvider(
430+
'copilot-swe-agent',
431+
{
432+
label: vscode.l10n.t('GitHub Copilot Coding Agent'),
433+
provideChatSessionItems: async (token) => {
434+
return await copilotRemoteAgentManager.provideChatSessions(token);
435+
},
436+
provideChatSessionContent: async (id, token) => {
437+
return await copilotRemoteAgentManager.provideChatSessionContent(id, token);
438+
},
439+
// Events not used yet, but required by interface.
440+
onDidChangeChatSessionItems: new vscode.EventEmitter<void>().event,
441+
}
442+
));
443+
>>>>>>> Stashed changes
427444

428445
const prTree = new PullRequestsTreeDataProvider(telemetry, context, reposManager, copilotRemoteAgentManager);
429446
context.subscriptions.push(prTree);

src/github/copilotRemoteAgent.ts

Lines changed: 198 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -748,4 +748,201 @@ export class CopilotRemoteAgentManager extends Disposable {
748748
}
749749
return [];
750750
}
751-
}
751+
752+
public async provideChatSessionContent(id: string, token: vscode.CancellationToken): Promise<vscode.ChatSession> {
753+
try {
754+
const capi = await this.copilotApi;
755+
if (!capi || token.isCancellationRequested) {
756+
return this.createEmptySession();
757+
}
758+
759+
const pullRequestId = parseInt(id);
760+
if (isNaN(pullRequestId)) {
761+
Logger.error(`Invalid pull request ID: ${id}`, CopilotRemoteAgentManager.ID);
762+
return this.createEmptySession();
763+
}
764+
765+
// Find the pull request model
766+
const pullRequest = this.findPullRequestById(pullRequestId);
767+
if (!pullRequest) {
768+
Logger.error(`Pull request not found: ${pullRequestId}`, CopilotRemoteAgentManager.ID);
769+
return this.createEmptySession();
770+
}
771+
772+
// Get session logs
773+
const sessionLogs = await this.getSessionLogFromPullRequest(pullRequest);
774+
if (!sessionLogs) {
775+
Logger.warn(`No session logs found for pull request ${pullRequestId}`, CopilotRemoteAgentManager.ID);
776+
return this.createEmptySession();
777+
}
778+
779+
// Parse logs and create chat history
780+
const history = await this.parseChatHistoryFromLogs(sessionLogs.logs, pullRequest.title);
781+
782+
return {
783+
history,
784+
requestHandler: undefined // Read-only session
785+
};
786+
} catch (error) {
787+
Logger.error(`Failed to provide chat session content: ${error}`, CopilotRemoteAgentManager.ID);
788+
return this.createEmptySession();
789+
}
790+
}
791+
792+
private createEmptySession(): vscode.ChatSession {
793+
return {
794+
history: [],
795+
requestHandler: undefined
796+
};
797+
}
798+
799+
private findPullRequestById(id: number): PullRequestModel | undefined {
800+
for (const folderManager of this.repositoriesManager.folderManagers) {
801+
for (const githubRepo of folderManager.gitHubRepositories) {
802+
const pullRequest = githubRepo.pullRequestModels.find(pr => pr.id === id);
803+
if (pullRequest) {
804+
return pullRequest;
805+
}
806+
}
807+
}
808+
return undefined;
809+
}
810+
811+
private async parseChatHistoryFromLogs(logs: string, prTitle: string): Promise<ReadonlyArray<vscode.ChatRequestTurn | vscode.ChatResponseTurn2>> {
812+
try {
813+
const logChunks = parseSessionLogs(logs);
814+
815+
const history: Array<vscode.ChatRequestTurn | vscode.ChatResponseTurn2> = [];
816+
817+
// Insert the initial user request with the PR title
818+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
819+
// @ts-ignore - Constructor will be made public
820+
const initialUserRequest = new vscode.ChatRequestTurn(
821+
prTitle,
822+
undefined, // command
823+
[], // references
824+
'copilot-swe-agent',
825+
[] // toolReferences
826+
);
827+
history.push(initialUserRequest);
828+
829+
let currentRequestContent = '';
830+
let currentResponseContent = '';
831+
let isCollectingUserMessage = false;
832+
833+
for (const chunk of logChunks) {
834+
for (const choice of chunk.choices) {
835+
const delta = choice.delta;
836+
837+
if (delta.role === 'user') {
838+
// If we were collecting a response, finalize it
839+
if (currentResponseContent.trim()) {
840+
const responseParts = [new vscode.ChatResponseMarkdownPart(currentResponseContent.trim())];
841+
const responseResult: vscode.ChatResult = {};
842+
history.push(new vscode.ChatResponseTurn2(responseParts, responseResult, 'copilot-swe-agent'));
843+
currentResponseContent = '';
844+
}
845+
846+
isCollectingUserMessage = true;
847+
if (delta.content) {
848+
currentRequestContent += delta.content;
849+
}
850+
} else if (delta.role === 'assistant') {
851+
// If we were collecting a user message, finalize it
852+
if (isCollectingUserMessage && currentRequestContent.trim()) {
853+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
854+
// @ts-ignore - Constructor will be made public
855+
const userMessage = new vscode.ChatRequestTurn(
856+
currentRequestContent.trim(),
857+
undefined, // command
858+
[], // references
859+
'copilot-swe-agent',
860+
[] // toolReferences
861+
);
862+
history.push(userMessage);
863+
currentRequestContent = '';
864+
isCollectingUserMessage = false;
865+
}
866+
867+
if (delta.content) {
868+
currentResponseContent += delta.content;
869+
}
870+
871+
// Handle tool calls as code blocks in the response
872+
if (delta.tool_calls) {
873+
for (const toolCall of delta.tool_calls) {
874+
if (toolCall.function?.name && toolCall.function?.arguments) {
875+
currentResponseContent += `\n\n**Tool Call: ${toolCall.function.name}**\n\`\`\`json\n${toolCall.function.arguments}\n\`\`\`\n`;
876+
}
877+
}
878+
}
879+
}
880+
}
881+
}
882+
883+
// Finalize any remaining content
884+
if (isCollectingUserMessage && currentRequestContent.trim()) {
885+
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
886+
// @ts-ignore - Constructor will be made public
887+
const userMessage = new vscode.ChatRequestTurn(
888+
currentRequestContent.trim(),
889+
undefined, // command
890+
[], // references
891+
'copilot-swe-agent',
892+
[] // toolReferences
893+
);
894+
history.push(userMessage);
895+
} else if (currentResponseContent.trim()) {
896+
const responseParts = [new vscode.ChatResponseMarkdownPart(currentResponseContent.trim())];
897+
const responseResult: vscode.ChatResult = {};
898+
history.push(new vscode.ChatResponseTurn2(responseParts, responseResult, 'copilot-swe-agent'));
899+
}
900+
901+
return history;
902+
} catch (error) {
903+
Logger.error(`Failed to parse chat history from logs: ${error}`, CopilotRemoteAgentManager.ID);
904+
return [];
905+
}
906+
}
907+
}
908+
909+
function parseSessionLogs(rawText: string): SessionResponseLogChunk[] {
910+
const parts = rawText
911+
.split(/\r?\n/)
912+
.filter(part => part.startsWith('data: '))
913+
.map(part => part.slice('data: '.length).trim())
914+
.map(part => JSON.parse(part));
915+
916+
return parts as SessionResponseLogChunk[];
917+
}
918+
919+
export interface SessionResponseLogChunk {
920+
choices: Array<{
921+
finish_reason: string;
922+
delta: {
923+
content?: string;
924+
role: string;
925+
tool_calls?: Array<{
926+
function: {
927+
arguments: string;
928+
name: string;
929+
};
930+
id: string;
931+
type: string;
932+
index: number;
933+
}>;
934+
};
935+
}>;
936+
created: number;
937+
id: string;
938+
usage: {
939+
completion_tokens: number;
940+
prompt_tokens: number;
941+
prompt_tokens_details: {
942+
cached_tokens: number;
943+
};
944+
total_tokens: number;
945+
};
946+
model: string;
947+
object: string;
948+
}

0 commit comments

Comments
 (0)