mirror of
https://github.com/microsoft/vscode.git
synced 2026-05-29 19:59:19 +01:00
Refactor Copilot CLI loading and session management (#1730)
* Move Copilot CLI session out into own file * Remvoe a few methods * Updates
This commit is contained in:
@@ -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<string, vscode.ChatToolInvocationPart>();
|
||||
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<SessionEvent> {
|
||||
// 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<void> {
|
||||
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<string, string>();
|
||||
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 || '<unknown>'}`);
|
||||
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) || '<unknown>';
|
||||
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<void> {
|
||||
const uris = getAffectedUrisForEditTool(input.toolName, input.toolArgs);
|
||||
return this._editTracker.trackEdit(editKey, uris, stream);
|
||||
}
|
||||
|
||||
private async _onDidEditTool(editKey: string): Promise<void> {
|
||||
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)}`;
|
||||
}
|
||||
|
||||
@@ -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<string, vscode.ChatToolInvocationPart>();
|
||||
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<vscode.ChatSessionStatus | undefined>());
|
||||
|
||||
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<SessionEvent> {
|
||||
// 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<void> {
|
||||
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<string, string>();
|
||||
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 || '<unknown>'}`);
|
||||
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) || '<unknown>';
|
||||
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<void> {
|
||||
const uris = getAffectedUrisForEditTool(input.toolName, input.toolArgs);
|
||||
return this._editTracker.trackEdit(editKey, uris, stream);
|
||||
}
|
||||
|
||||
private async _onDidEditTool(editKey: string): Promise<void> {
|
||||
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)}`;
|
||||
}
|
||||
+21
-17
@@ -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<void>;
|
||||
|
||||
// Session metadata querying
|
||||
getAllSessions(token: CancellationToken): Promise<readonly ICopilotCLISession[]>;
|
||||
getSession(sessionId: string, token: CancellationToken): Promise<ICopilotCLISession | undefined>;
|
||||
getAllSessions(token: CancellationToken): Promise<readonly ICopilotCLISessionItem[]>;
|
||||
getSession(sessionId: string, token: CancellationToken): Promise<ICopilotCLISessionItem | undefined>;
|
||||
|
||||
// SDK session management
|
||||
getSessionManager(): Promise<SessionManager>;
|
||||
getOrCreateSDKSession(sessionId: string | undefined, prompt: string): Promise<Session>;
|
||||
deleteSession(sessionId: string): Promise<boolean>;
|
||||
setSessionStatus(sessionId: string, status: ChatSessionStatus): void;
|
||||
getSessionStatus(sessionId: string): ChatSessionStatus | undefined;
|
||||
|
||||
// Session wrapper tracking
|
||||
trackSessionWrapper<T extends IDisposable>(sessionId: string, wrapper: T): void;
|
||||
findSessionWrapper<T extends IDisposable>(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<string, IDisposable>();
|
||||
private _sessions = new Map<string, ICopilotCLISession>();
|
||||
private _sessions = new Map<string, ICopilotCLISessionItem>();
|
||||
private _pendingRequests = new Set<string>();
|
||||
|
||||
|
||||
@@ -81,14 +79,17 @@ export class CopilotCLISessionService implements ICopilotCLISessionService {
|
||||
return this._sessionManager;
|
||||
}
|
||||
|
||||
async getAllSessions(token: CancellationToken): Promise<readonly ICopilotCLISession[]> {
|
||||
async getAllSessions(token: CancellationToken): Promise<readonly ICopilotCLISessionItem[]> {
|
||||
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<ICopilotCLISession | undefined> {
|
||||
async getSession(sessionId: string, token: CancellationToken): Promise<ICopilotCLISessionItem | undefined> {
|
||||
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<T extends IDisposable>(sessionId: string, wrapper: T): void {
|
||||
this._sessionWrappers.set(sessionId, wrapper);
|
||||
}
|
||||
|
||||
+22
-23
@@ -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<vscode.ChatSessionItem[]> {
|
||||
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<void> {
|
||||
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<pr_metadata uri="${prInfo.uri}" title="${escapeXml(prInfo.title)}" description="${escapeXml(prInfo.description)}" author="${escapeXml(prInfo.author)}" linkTag="${escapeXml(prInfo.linkTag)}"/>`;
|
||||
session.sdkSession.addEvent({
|
||||
session.addEvent({
|
||||
type: 'assistant.message',
|
||||
data: {
|
||||
messageId: `msg_${Date.now()}`,
|
||||
|
||||
Reference in New Issue
Block a user