From f9ed6387cd634ee9ca35ecfd2157392ef526dbd9 Mon Sep 17 00:00:00 2001 From: Don Jayamanne Date: Fri, 31 Oct 2025 15:23:45 +1100 Subject: [PATCH] Refactor Copilot CLI loading and session management (#1730) * Move Copilot CLI session out into own file * Remvoe a few methods * Updates --- .../copilotcli/node/copilotcliAgentManager.ts | 204 +--------------- .../copilotcli/node/copilotcliSession.ts | 219 ++++++++++++++++++ .../node/copilotcliSessionService.ts | 38 +-- .../copilotCLIChatSessionsContribution.ts | 45 ++-- 4 files changed, 264 insertions(+), 242 deletions(-) create mode 100644 extensions/copilot/src/extension/agents/copilotcli/node/copilotcliSession.ts diff --git a/extensions/copilot/src/extension/agents/copilotcli/node/copilotcliAgentManager.ts b/extensions/copilot/src/extension/agents/copilotcli/node/copilotcliAgentManager.ts index 44fa9b38eb2..a76a2bed0f4 100644 --- a/extensions/copilot/src/extension/agents/copilotcli/node/copilotcliAgentManager.ts +++ b/extensions/copilot/src/extension/agents/copilotcli/node/copilotcliAgentManager.ts @@ -3,24 +3,14 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import type { AgentOptions, Attachment, ModelProvider, PostToolUseHookInput, PreToolUseHookInput, Session, SessionEvent } from '@github/copilot/sdk'; +import type { ModelProvider } from '@github/copilot/sdk'; import type * as vscode from 'vscode'; -import { IAuthenticationService } from '../../../../platform/authentication/common/authentication'; import { ILogService } from '../../../../platform/log/common/logService'; -import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService'; -import { CancellationToken } from '../../../../util/vs/base/common/cancellation'; import { Disposable } from '../../../../util/vs/base/common/lifecycle'; import { IInstantiationService } from '../../../../util/vs/platform/instantiation/common/instantiation'; -import { ChatResponseThinkingProgressPart, LanguageModelTextPart } from '../../../../vscodeTypes'; -import { IToolsService } from '../../../tools/common/toolsService'; -import { ExternalEditTracker } from '../../common/externalEditTracker'; -import { getAffectedUrisForEditTool } from '../common/copilotcliTools'; -import { ICopilotCLISDK } from './copilotCli'; import { CopilotCLIPromptResolver } from './copilotcliPromptResolver'; +import { CopilotCLISession } from './copilotcliSession'; import { ICopilotCLISessionService } from './copilotcliSessionService'; -import { processToolExecutionComplete, processToolExecutionStart } from './copilotcliToolInvocationFormatter'; -import { getCopilotLogger } from './logger'; -import { getConfirmationToolParams, PermissionRequest } from './permissionHelpers'; export class CopilotCLIAgentManager extends Disposable { constructor( @@ -47,7 +37,6 @@ export class CopilotCLIAgentManager extends Disposable { modelId: ModelProvider | undefined, token: vscode.CancellationToken ): Promise<{ copilotcliSessionId: string | undefined }> { - const isNewSession = !copilotcliSessionId; const sessionIdForLog = copilotcliSessionId ?? 'new'; this.logService.trace(`[CopilotCLIAgentManager] Handling request for sessionId=${sessionIdForLog}.`); @@ -63,197 +52,8 @@ export class CopilotCLIAgentManager extends Disposable { this.sessionService.trackSessionWrapper(sdkSession.sessionId, session); } - if (isNewSession) { - this.sessionService.setPendingRequest(session.sessionId); - } - await session.invoke(prompt, attachments, request.toolInvocationToken, stream, modelId, token); return { copilotcliSessionId: session.sessionId }; } } - -export class CopilotCLISession extends Disposable { - private _abortController = new AbortController(); - private _pendingToolInvocations = new Map(); - private _editTracker = new ExternalEditTracker(); - public readonly sessionId: string; - - constructor( - private readonly _sdkSession: Session, - @ILogService private readonly logService: ILogService, - @IWorkspaceService private readonly workspaceService: IWorkspaceService, - @IAuthenticationService private readonly _authenticationService: IAuthenticationService, - @IToolsService private readonly toolsService: IToolsService, - @ICopilotCLISDK private readonly copilotCLISDK: ICopilotCLISDK - ) { - super(); - this.sessionId = _sdkSession.sessionId; - } - - public override dispose(): void { - this._abortController.abort(); - super.dispose(); - } - - async *query(prompt: string, attachments: Attachment[], options: AgentOptions): AsyncGenerator { - // Dynamically import the SDK - const { Agent } = await this.copilotCLISDK.getPackage(); - const agent = new Agent(options); - yield* agent.query(prompt, attachments); - } - - public async invoke( - prompt: string, - attachments: Attachment[], - toolInvocationToken: vscode.ChatParticipantToolToken, - stream: vscode.ChatResponseStream, - modelId: ModelProvider | undefined, - token: vscode.CancellationToken - ): Promise { - if (this._store.isDisposed) { - throw new Error('Session disposed'); - } - - this.logService.trace(`[CopilotCLISession] Invoking session ${this.sessionId}`); - const copilotToken = await this._authenticationService.getCopilotToken(); - - const options: AgentOptions = { - modelProvider: modelId ?? { - type: 'anthropic', - model: 'claude-sonnet-4.5', - }, - abortController: this._abortController, - // TODO@rebornix handle workspace properly - workingDirectory: this.workspaceService.getWorkspaceFolders().at(0)?.fsPath, - copilotToken: copilotToken.token, - env: { - ...process.env, - COPILOTCLI_DISABLE_NONESSENTIAL_TRAFFIC: '1' - }, - requestPermission: async (permissionRequest) => { - return await this.requestPermission(permissionRequest, toolInvocationToken); - }, - logger: getCopilotLogger(this.logService), - session: this._sdkSession, - hooks: { - preToolUse: [ - async (input: PreToolUseHookInput) => { - const editKey = getEditOperationKey(input.toolName, input.toolArgs); - await this._onWillEditTool(input, editKey, stream); - } - ], - postToolUse: [ - async (input: PostToolUseHookInput) => { - const editKey = getEditOperationKey(input.toolName, input.toolArgs); - void this._onDidEditTool(editKey); - } - ] - } - }; - - try { - for await (const event of this.query(prompt, attachments, options)) { - if (token.isCancellationRequested) { - break; - } - - this._processEvent(event, stream, toolInvocationToken); - } - } catch (error) { - this.logService.error(`CopilotCLI session error: ${error}`); - stream.markdown(`\n\n❌ Error: ${error instanceof Error ? error.message : String(error)}`); - } - } - - private _toolNames = new Map(); - private _processEvent( - event: SessionEvent, - stream: vscode.ChatResponseStream, - toolInvocationToken: vscode.ChatParticipantToolToken - ): void { - this.logService.trace(`CopilotCLI Event: ${JSON.stringify(event, null, 2)}`); - - switch (event.type) { - case 'assistant.turn_start': - case 'assistant.turn_end': { - this._toolNames.clear(); - break; - } - - case 'assistant.message': { - if (event.data.content.length) { - stream.markdown(event.data.content); - } - break; - } - - case 'tool.execution_start': { - const responsePart = processToolExecutionStart(event, this._toolNames, this._pendingToolInvocations); - const toolName = this._toolNames.get(event.data.toolCallId); - if (responsePart instanceof ChatResponseThinkingProgressPart) { - stream.push(responsePart); - } - this.logService.trace(`Start Tool ${toolName || ''}`); - break; - } - - case 'tool.execution_complete': { - const responsePart = processToolExecutionComplete(event, this._pendingToolInvocations); - if (responsePart && !(responsePart instanceof ChatResponseThinkingProgressPart)) { - stream.push(responsePart); - } - - const toolName = this._toolNames.get(event.data.toolCallId) || ''; - const success = `success: ${event.data.success}`; - const error = event.data.error ? `error: ${event.data.error.code},${event.data.error.message}` : ''; - const result = event.data.result ? `result: ${event.data.result?.content}` : ''; - const parts = [success, error, result].filter(part => part.length > 0).join(', '); - this.logService.trace(`Complete Tool ${toolName}, ${parts}`); - break; - } - - case 'session.error': { - this.logService.error(`CopilotCLI error: (${event.data.errorType}), ${event.data.message}`); - stream.markdown(`\n\n❌ Error: ${event.data.message}`); - break; - } - } - } - - private async requestPermission( - permissionRequest: PermissionRequest, - toolInvocationToken: vscode.ChatParticipantToolToken - ): Promise<{ kind: 'approved' } | { kind: 'denied-interactively-by-user' }> { - try { - const { tool, input } = getConfirmationToolParams(permissionRequest); - const result = await this.toolsService.invokeTool(tool, - { input, toolInvocationToken }, - CancellationToken.None); - - const firstResultPart = result.content.at(0); - if (firstResultPart instanceof LanguageModelTextPart && firstResultPart.value === 'yes') { - return { kind: 'approved' }; - } - } catch (error) { - this.logService.error(`[CopilotCLISession] Permission request error: ${error}`); - } - - return { kind: 'denied-interactively-by-user' }; - } - - private async _onWillEditTool(input: PreToolUseHookInput, editKey: string, stream: vscode.ChatResponseStream): Promise { - const uris = getAffectedUrisForEditTool(input.toolName, input.toolArgs); - return this._editTracker.trackEdit(editKey, uris, stream); - } - - private async _onDidEditTool(editKey: string): Promise { - return this._editTracker.completeEdit(editKey); - } -} - - -function getEditOperationKey(toolName: string, toolArgs: unknown): string { - // todo@connor4312: get copilot CLI to surface the tool call ID instead? - return `${toolName}:${JSON.stringify(toolArgs)}`; -} diff --git a/extensions/copilot/src/extension/agents/copilotcli/node/copilotcliSession.ts b/extensions/copilot/src/extension/agents/copilotcli/node/copilotcliSession.ts new file mode 100644 index 00000000000..90e752f44d4 --- /dev/null +++ b/extensions/copilot/src/extension/agents/copilotcli/node/copilotcliSession.ts @@ -0,0 +1,219 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type { AgentOptions, Attachment, ModelProvider, PostToolUseHookInput, PreToolUseHookInput, Session, SessionEvent } from '@github/copilot/sdk'; +import type * as vscode from 'vscode'; +import { IAuthenticationService } from '../../../../platform/authentication/common/authentication'; +import { ILogService } from '../../../../platform/log/common/logService'; +import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService'; +import { CancellationToken } from '../../../../util/vs/base/common/cancellation'; +import { DisposableStore } from '../../../../util/vs/base/common/lifecycle'; +import { ChatResponseThinkingProgressPart, ChatSessionStatus, EventEmitter, LanguageModelTextPart } from '../../../../vscodeTypes'; +import { IToolsService } from '../../../tools/common/toolsService'; +import { ExternalEditTracker } from '../../common/externalEditTracker'; +import { getAffectedUrisForEditTool } from '../common/copilotcliTools'; +import { ICopilotCLISDK } from './copilotCli'; +import { processToolExecutionComplete, processToolExecutionStart } from './copilotcliToolInvocationFormatter'; +import { getCopilotLogger } from './logger'; +import { getConfirmationToolParams, PermissionRequest } from './permissionHelpers'; + +export class CopilotCLISession extends DisposableStore { + private _abortController = new AbortController(); + private _pendingToolInvocations = new Map(); + private _editTracker = new ExternalEditTracker(); + public readonly sessionId: string; + private _status?: vscode.ChatSessionStatus; + public get status(): vscode.ChatSessionStatus | undefined { + return this._status; + } + private readonly _statusChange = this.add(new EventEmitter()); + + public readonly onDidChangeStatus = this._statusChange.event; + + constructor( + private readonly _sdkSession: Session, + @ILogService private readonly logService: ILogService, + @IWorkspaceService private readonly workspaceService: IWorkspaceService, + @IAuthenticationService private readonly _authenticationService: IAuthenticationService, + @IToolsService private readonly toolsService: IToolsService, + @ICopilotCLISDK private readonly copilotCLISDK: ICopilotCLISDK + ) { + super(); + this.sessionId = _sdkSession.sessionId; + } + + public override dispose(): void { + this._abortController.abort(); + super.dispose(); + } + + async *query(prompt: string, attachments: Attachment[], options: AgentOptions): AsyncGenerator { + // Dynamically import the SDK + const { Agent } = await this.copilotCLISDK.getPackage(); + const agent = new Agent(options); + yield* agent.query(prompt, attachments); + } + + public async invoke( + prompt: string, + attachments: Attachment[], + toolInvocationToken: vscode.ChatParticipantToolToken, + stream: vscode.ChatResponseStream, + modelId: ModelProvider | undefined, + token: vscode.CancellationToken + ): Promise { + if (this.isDisposed) { + throw new Error('Session disposed'); + } + + this._status = ChatSessionStatus.InProgress; + this._statusChange.fire(this._status); + + this.logService.trace(`[CopilotCLISession] Invoking session ${this.sessionId}`); + const copilotToken = await this._authenticationService.getCopilotToken(); + + const options: AgentOptions = { + modelProvider: modelId ?? { + type: 'anthropic', + model: 'claude-sonnet-4.5', + }, + abortController: this._abortController, + // TODO@rebornix handle workspace properly + workingDirectory: this.workspaceService.getWorkspaceFolders().at(0)?.fsPath, + copilotToken: copilotToken.token, + env: { + ...process.env, + COPILOTCLI_DISABLE_NONESSENTIAL_TRAFFIC: '1' + }, + requestPermission: async (permissionRequest) => { + return await this.requestPermission(permissionRequest, toolInvocationToken); + }, + logger: getCopilotLogger(this.logService), + session: this._sdkSession, + hooks: { + preToolUse: [ + async (input: PreToolUseHookInput) => { + const editKey = getEditOperationKey(input.toolName, input.toolArgs); + await this._onWillEditTool(input, editKey, stream); + } + ], + postToolUse: [ + async (input: PostToolUseHookInput) => { + const editKey = getEditOperationKey(input.toolName, input.toolArgs); + void this._onDidEditTool(editKey); + } + ] + } + }; + + try { + for await (const event of this.query(prompt, attachments, options)) { + if (token.isCancellationRequested) { + break; + } + + this._processEvent(event, stream, toolInvocationToken); + } + this._status = ChatSessionStatus.Completed; + this._statusChange.fire(this._status); + } catch (error) { + this._status = ChatSessionStatus.Failed; + this._statusChange.fire(this._status); + this.logService.error(`CopilotCLI session error: ${error}`); + stream.markdown(`\n\n❌ Error: ${error instanceof Error ? error.message : String(error)}`); + } + } + + private _toolNames = new Map(); + private _processEvent( + event: SessionEvent, + stream: vscode.ChatResponseStream, + toolInvocationToken: vscode.ChatParticipantToolToken + ): void { + this.logService.trace(`CopilotCLI Event: ${JSON.stringify(event, null, 2)}`); + + switch (event.type) { + case 'assistant.turn_start': + case 'assistant.turn_end': { + this._toolNames.clear(); + break; + } + + case 'assistant.message': { + if (event.data.content.length) { + stream.markdown(event.data.content); + } + break; + } + + case 'tool.execution_start': { + const responsePart = processToolExecutionStart(event, this._toolNames, this._pendingToolInvocations); + const toolName = this._toolNames.get(event.data.toolCallId); + if (responsePart instanceof ChatResponseThinkingProgressPart) { + stream.push(responsePart); + } + this.logService.trace(`Start Tool ${toolName || ''}`); + break; + } + + case 'tool.execution_complete': { + const responsePart = processToolExecutionComplete(event, this._pendingToolInvocations); + if (responsePart && !(responsePart instanceof ChatResponseThinkingProgressPart)) { + stream.push(responsePart); + } + + const toolName = this._toolNames.get(event.data.toolCallId) || ''; + const success = `success: ${event.data.success}`; + const error = event.data.error ? `error: ${event.data.error.code},${event.data.error.message}` : ''; + const result = event.data.result ? `result: ${event.data.result?.content}` : ''; + const parts = [success, error, result].filter(part => part.length > 0).join(', '); + this.logService.trace(`Complete Tool ${toolName}, ${parts}`); + break; + } + + case 'session.error': { + this.logService.error(`CopilotCLI error: (${event.data.errorType}), ${event.data.message}`); + stream.markdown(`\n\n❌ Error: ${event.data.message}`); + break; + } + } + } + + private async requestPermission( + permissionRequest: PermissionRequest, + toolInvocationToken: vscode.ChatParticipantToolToken + ): Promise<{ kind: 'approved' } | { kind: 'denied-interactively-by-user' }> { + try { + const { tool, input } = getConfirmationToolParams(permissionRequest); + const result = await this.toolsService.invokeTool(tool, + { input, toolInvocationToken }, + CancellationToken.None); + + const firstResultPart = result.content.at(0); + if (firstResultPart instanceof LanguageModelTextPart && firstResultPart.value === 'yes') { + return { kind: 'approved' }; + } + } catch (error) { + this.logService.error(`[CopilotCLISession] Permission request error: ${error}`); + } + + return { kind: 'denied-interactively-by-user' }; + } + + private async _onWillEditTool(input: PreToolUseHookInput, editKey: string, stream: vscode.ChatResponseStream): Promise { + const uris = getAffectedUrisForEditTool(input.toolName, input.toolArgs); + return this._editTracker.trackEdit(editKey, uris, stream); + } + + private async _onDidEditTool(editKey: string): Promise { + return this._editTracker.completeEdit(editKey); + } +} + + +function getEditOperationKey(toolName: string, toolArgs: unknown): string { + // todo@connor4312: get copilot CLI to surface the tool call ID instead? + return `${toolName}:${JSON.stringify(toolArgs)}`; +} diff --git a/extensions/copilot/src/extension/agents/copilotcli/node/copilotcliSessionService.ts b/extensions/copilot/src/extension/agents/copilotcli/node/copilotcliSessionService.ts index 13d66c9fb17..4b7589d56bc 100644 --- a/extensions/copilot/src/extension/agents/copilotcli/node/copilotcliSessionService.ts +++ b/extensions/copilot/src/extension/agents/copilotcli/node/copilotcliSessionService.ts @@ -15,12 +15,13 @@ import { ICopilotCLISDK } from './copilotCli'; import { stripReminders } from './copilotcliToolInvocationFormatter'; import { getCopilotLogger } from './logger'; -export interface ICopilotCLISession { +export interface ICopilotCLISessionItem { readonly id: string; readonly sdkSession: Session; readonly label: string; readonly isEmpty: boolean; readonly timestamp: Date; + readonly status?: ChatSessionStatus; } export type ExtendedChatRequest = ChatRequest & { prompt: string }; @@ -31,23 +32,20 @@ export interface ICopilotCLISessionService { onDidChangeSessions: Event; // Session metadata querying - getAllSessions(token: CancellationToken): Promise; - getSession(sessionId: string, token: CancellationToken): Promise; + getAllSessions(token: CancellationToken): Promise; + getSession(sessionId: string, token: CancellationToken): Promise; // SDK session management getSessionManager(): Promise; getOrCreateSDKSession(sessionId: string | undefined, prompt: string): Promise; deleteSession(sessionId: string): Promise; setSessionStatus(sessionId: string, status: ChatSessionStatus): void; - getSessionStatus(sessionId: string): ChatSessionStatus | undefined; // Session wrapper tracking trackSessionWrapper(sessionId: string, wrapper: T): void; findSessionWrapper(sessionId: string): T | undefined; // Pending request tracking (for untitled sessions) - setPendingRequest(sessionId: string): void; - isPendingRequest(sessionId: string): boolean; clearPendingRequest(sessionId: string): void; } @@ -58,7 +56,7 @@ export class CopilotCLISessionService implements ICopilotCLISessionService { private _sessionManager: SessionManager | undefined; private _sessionWrappers = new DisposableMap(); - private _sessions = new Map(); + private _sessions = new Map(); private _pendingRequests = new Set(); @@ -81,14 +79,17 @@ export class CopilotCLISessionService implements ICopilotCLISessionService { return this._sessionManager; } - async getAllSessions(token: CancellationToken): Promise { + async getAllSessions(token: CancellationToken): Promise { try { const sessionManager = await this.getSessionManager(); const sessionMetadataList = await sessionManager.listSessions(); // Convert SessionMetadata to ICopilotCLISession - const diskSessions: ICopilotCLISession[] = coalesce(await Promise.all( + const diskSessions: ICopilotCLISessionItem[] = coalesce(await Promise.all( sessionMetadataList.map(async (metadata) => { + if (this.isPendingRequest(metadata.sessionId)) { + return undefined; + } try { // Get the full session to access chat messages const sdkSession = await sessionManager.getSession(metadata.sessionId); @@ -129,14 +130,21 @@ export class CopilotCLISessionService implements ICopilotCLISessionService { const cachedSessions = Array.from(this._sessions.values()).filter(s => !diskSessionIds.has(s.id)); const allSessions = [...diskSessions, ...cachedSessions]; - return allSessions; + return allSessions + .filter(session => !this.isPendingRequest(session.id) && !session.isEmpty) + .map(session => { + return { + ...session, + status: this._sessionStatuses.get(session.id) + }; + }); } catch (error) { this.logService.error(`Failed to get all sessions: ${error}`); return Array.from(this._sessions.values()); } } - async getSession(sessionId: string, token: CancellationToken): Promise { + async getSession(sessionId: string, token: CancellationToken): Promise { const cached = this._sessions.get(sessionId); if (cached) { return cached; @@ -167,12 +175,12 @@ export class CopilotCLISessionService implements ICopilotCLISessionService { } const sdkSession = await sessionManager.createSession(); - + this.setPendingRequest(sdkSession.sessionId); // Cache the new session immediately const chatMessages = await sdkSession.getChatMessages(); const noUserMessages = !chatMessages.find(message => message.role === 'user'); const label = await this._generateSessionLabel(sdkSession.sessionId, chatMessages, prompt); - const newSession: ICopilotCLISession = { + const newSession: ICopilotCLISessionItem = { id: sdkSession.sessionId, sdkSession, label, @@ -189,10 +197,6 @@ export class CopilotCLISessionService implements ICopilotCLISessionService { this._onDidChangeSessions.fire(); } - public getSessionStatus(sessionId: string): ChatSessionStatus | undefined { - return this._sessionStatuses.get(sessionId); - } - public trackSessionWrapper(sessionId: string, wrapper: T): void { this._sessionWrappers.set(sessionId, wrapper); } diff --git a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts index 9f91f08eb96..7fea09eba2f 100644 --- a/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts +++ b/extensions/copilot/src/extension/chatSessions/vscode-node/copilotCLIChatSessionsContribution.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import type { Session } from '@github/copilot/sdk'; import * as vscode from 'vscode'; import { ChatExtendedRequestHandler, l10n, Uri } from 'vscode'; import { IGitService } from '../../../platform/git/common/gitService'; @@ -98,14 +99,14 @@ export class CopilotCLIChatSessionItemProvider extends Disposable implements vsc public async provideChatSessionItems(token: vscode.CancellationToken): Promise { const sessions = await this.copilotcliSessionService.getAllSessions(token); - const diskSessions = sessions.filter(session => !this.copilotcliSessionService.isPendingRequest(session.id) && !session.isEmpty).map(session => ({ + const diskSessions = sessions.map(session => ({ resource: SessionIdForCLI.getResource(session.id), label: session.label, tooltip: `Copilot CLI session: ${session.label}`, timing: { startTime: session.timestamp.getTime() }, - status: this.copilotcliSessionService.getSessionStatus(session.id) ?? vscode.ChatSessionStatus.Completed, + status: session.status ?? vscode.ChatSessionStatus.Completed, } satisfies vscode.ChatSessionItem)); const count = diskSessions.length; @@ -238,23 +239,26 @@ export class CopilotCLIChatSessionParticipant { const { resource } = chatSessionContext.chatSessionItem; const id = SessionIdForCLI.parse(resource); - - if (request.acceptedConfirmationData || request.rejectedConfirmationData) { - return await this.handleConfirmationData(id, request, context, stream, token); - } - - if (request.prompt.startsWith('/delegate')) { - await this.handleDelegateCommand(id, request, context, stream, token); + const session = await this.sessionService.getSession(id, token); + if (!session) { + stream.warning(vscode.l10n.t('Chat session not found.')); + return {}; + } + + if (request.acceptedConfirmationData || request.rejectedConfirmationData) { + return await this.handleConfirmationData(session.sdkSession, request, context, stream, token); + } + + if (request.prompt.startsWith('/delegate')) { + await this.handleDelegateCommand(session.sdkSession, request, context, stream, token); return {}; } - this.sessionService.setSessionStatus(id, vscode.ChatSessionStatus.InProgress); await this.copilotcliAgentManager.handleRequest(id, request, context, stream, getModelProvider(_sessionModel.get(id)?.id), token); - this.sessionService.setSessionStatus(id, vscode.ChatSessionStatus.Completed); return {}; } - private async handleDelegateCommand(id: string, request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken) { + private async handleDelegateCommand(session: Session, request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken) { if (!this.cloudSessionProvider) { stream.warning(localize('copilotcli.missingCloudAgent', "No cloud agent available")); return {}; @@ -281,12 +285,12 @@ export class CopilotCLIChatSessionParticipant { chatContext: context }, stream, token); if (prInfo) { - await this.recordPushToSession(id, request.prompt, prInfo, token); + await this.recordPushToSession(session, request.prompt, prInfo, token); } } } - private async handleConfirmationData(id: string, request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken) { + private async handleConfirmationData(session: Session, request: vscode.ChatRequest, context: vscode.ChatContext, stream: vscode.ChatResponseStream, token: vscode.CancellationToken) { const results: ConfirmationResult[] = []; results.push(...(request.acceptedConfirmationData?.map(data => ({ step: data.step, accepted: true, metadata: data?.metadata })) ?? [])); results.push(...((request.rejectedConfirmationData ?? []).filter(data => !results.some(r => r.step === data.step)).map(data => ({ step: data.step, accepted: false, metadata: data?.metadata })))); @@ -305,7 +309,7 @@ export class CopilotCLIChatSessionParticipant { chatContext: context }, stream, token); if (prInfo) { - await this.recordPushToSession(id, request.prompt, prInfo, token); + await this.recordPushToSession(session, request.prompt, prInfo, token); } return {}; } @@ -335,18 +339,13 @@ export class CopilotCLIChatSessionParticipant { } private async recordPushToSession( - sessionId: string, + session: Session, userPrompt: string, prInfo: { uri: string; title: string; description: string; author: string; linkTag: string }, token: vscode.CancellationToken ): Promise { - const session = await this.sessionService.getSession(sessionId, token); - if (!session) { - return; - } - // Add user message event - session.sdkSession.addEvent({ + session.addEvent({ type: 'user.message', data: { content: userPrompt @@ -355,7 +354,7 @@ export class CopilotCLIChatSessionParticipant { // Add assistant message event with embedded PR metadata const assistantMessage = `GitHub Copilot cloud agent has begun working on your request. Follow its progress in the associated chat and pull request.\n`; - session.sdkSession.addEvent({ + session.addEvent({ type: 'assistant.message', data: { messageId: `msg_${Date.now()}`,