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
501 changes: 7 additions & 494 deletions package-lock.json

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4427,7 +4427,7 @@
"dependencies": {
"@anthropic-ai/claude-code": "^1.0.120",
"@anthropic-ai/sdk": "^0.63.0",
"@github/copilot": "^0.0.343",
"@github/copilot": "^0.0.351",
"@google/genai": "^1.22.0",
"@humanwhocodes/gitignore-to-minimatch": "1.0.2",
"@microsoft/tiktokenizer": "^1.0.10",
Expand Down
12 changes: 12 additions & 0 deletions src/extension/agents/copilotcli/node/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
Instructions for development with the sdk source
* Build the cli tool (e.g. `npm run build:package`)
* Open `vscode-copilot-chat` in VS Code Insiders
* Open a terminal in the `vscode-copilot-chat` folder
* `fnm use` or `nvm use` to switch to the right version of node
* Run `npm install /Users/donjayamanne/Development/vsc/sweagentd/runtime/dist-cli` to install the sdk package from local path
* Run `npm run postinstall` (sometimes you might need to run this manually, for now do this always after installing from local path)
* Exit VS Code completely (don't reload window, you must exit completely, this is important to shutdown the tasks, unless you want to do this manually)
* Start VS Code again
* Build tasks will automatically start running (or start manually using `ctrl+shift+b`)
* Start debugging using the debug view and selecting `Launch Copilot Extension`
* Add breakpoints in extension code & step into the sdk code
166 changes: 166 additions & 0 deletions src/extension/agents/copilotcli/node/copilotCli.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,166 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { ModelProvider, SessionOptions } from '@github/copilot/sdk';
import type { ChatSessionProviderOptionItem } from 'vscode';
import { IAuthenticationService } from '../../../../platform/authentication/common/authentication';
import { IVSCodeExtensionContext } from '../../../../platform/extContext/common/extensionContext';
import { ILogService } from '../../../../platform/log/common/logService';
import { IWorkspaceService } from '../../../../platform/workspace/common/workspaceService';
import { createServiceIdentifier } from '../../../../util/common/services';
import { Lazy } from '../../../../util/vs/base/common/lazy';
import { Disposable, IDisposable, toDisposable } from '../../../../util/vs/base/common/lifecycle';
import { getCopilotLogger } from './logger';

const COPILOT_CLI_MODEL_MEMENTO_KEY = 'github.copilot.cli.sessionModel';
const DEFAULT_CLI_MODEL: ModelProvider = {
type: 'anthropic',
model: 'claude-sonnet-4.5'
};

/**
* Convert a model ID to a ModelProvider object for the Copilot CLI SDK
*/
function getModelProvider(modelId: string): ModelProvider {
// Map model IDs to their provider and model name
if (modelId.startsWith('claude-')) {
return {
type: 'anthropic',
model: modelId
};
} else if (modelId.startsWith('gpt-')) {
return {
type: 'openai',
model: modelId
};
}

return DEFAULT_CLI_MODEL;
}

export interface ICopilotCLIModels {
_serviceBrand: undefined;
toModelProvider(modelId: string): ModelProvider;
getDefaultModel(): Promise<ChatSessionProviderOptionItem>;
setDefaultModel(model: ChatSessionProviderOptionItem): Promise<void>;
getAvailableModels(): Promise<ChatSessionProviderOptionItem[]>;
}

export const ICopilotCLIModels = createServiceIdentifier<ICopilotCLIModels>('ICopilotCLIModels');

export class CopilotCLIModels implements ICopilotCLIModels {
declare _serviceBrand: undefined;
private readonly _availalbeModels: Lazy<Promise<ChatSessionProviderOptionItem[]>>;
constructor(
@IVSCodeExtensionContext private readonly extensionContext: IVSCodeExtensionContext,
) {
this._availalbeModels = new Lazy<Promise<ChatSessionProviderOptionItem[]>>(() => this._getAvailableModels());
}
public toModelProvider(modelId: string) {
// TODO @This is wrong and missing mapping for type=openai.
// A better approach is to find the model against the sdk.
return getModelProvider(modelId);
}
public async getDefaultModel() {
// We control this
const models = await this.getAvailableModels();
const defaultModel = models.find(m => m.id.toLowerCase().includes(DEFAULT_CLI_MODEL.model.toLowerCase())) ?? models[0];
const preferredModelId = this.extensionContext.globalState.get<string>(COPILOT_CLI_MODEL_MEMENTO_KEY, defaultModel.id);

return models.find(m => m.id === preferredModelId) ?? defaultModel;
}

public async setDefaultModel(model: ChatSessionProviderOptionItem): Promise<void> {
await this.extensionContext.globalState.update(COPILOT_CLI_MODEL_MEMENTO_KEY, model.id);
}

public async getAvailableModels(): Promise<ChatSessionProviderOptionItem[]> {
// No need to query sdk multiple times, cache the result, this cannot change during a vscode session.
return this._availalbeModels.value;
}

private async _getAvailableModels(): Promise<ChatSessionProviderOptionItem[]> {
return [{
id: 'claude-sonnet-4.5',
name: 'Claude Sonnet 4.5'
},
{
id: 'claude-sonnet-4',
name: 'Claude Sonnet 4'
},
{
id: 'gpt-5',
name: 'GPT-5'
}];
}
}

export class CopilotCLISessionOptionsService {
constructor(
@IWorkspaceService private readonly workspaceService: IWorkspaceService,
@IAuthenticationService private readonly _authenticationService: IAuthenticationService,
@ILogService private readonly logService: ILogService,
) { }

public async createOptions(options: SessionOptions, permissionHandler: CopilotCLIPermissionsHandler) {
const copilotToken = await this._authenticationService.getCopilotToken();
const workingDirectory = await this.getWorkspaceFolderPath();
const allOptions: SessionOptions = {
copilotToken: copilotToken.token,
env: {
...process.env,
COPILOTCLI_DISABLE_NONESSENTIAL_TRAFFIC: '1'
},
logger: getCopilotLogger(this.logService),
requestPermission: async (permissionRequest) => {
return await permissionHandler.getPermissions(permissionRequest);
},
...options
};

if (workingDirectory) {
allOptions.workingDirectory = workingDirectory;
}
return allOptions;
}
private async getWorkspaceFolderPath() {
if (this.workspaceService.getWorkspaceFolders().length === 0) {
return undefined;
}
if (this.workspaceService.getWorkspaceFolders().length === 1) {
return this.workspaceService.getWorkspaceFolders()[0].fsPath;
}
const folder = await this.workspaceService.showWorkspaceFolderPicker();
return folder?.uri?.fsPath;
}
}

/**
* Perhaps temporary interface to handle permission requests from the Copilot CLI SDK
* Perhaps because the SDK needs a better way to handle this in long term per session.
*/
export interface ICopilotCLIPermissions {
onDidRequestPermissions(handler: SessionOptions['requestPermission']): IDisposable;
}

export class CopilotCLIPermissionsHandler extends Disposable implements ICopilotCLIPermissions {
private _handler: SessionOptions['requestPermission'] | undefined;

public onDidRequestPermissions(handler: SessionOptions['requestPermission']): IDisposable {
this._handler = handler;
return this._register(toDisposable(() => {
this._handler = undefined;
}));
}

public async getPermissions(permission: Parameters<NonNullable<SessionOptions['requestPermission']>>[0]): Promise<ReturnType<NonNullable<SessionOptions['requestPermission']>>> {
if (!this._handler) {
return {
kind: "denied-interactively-by-user"
};
}
return await this._handler(permission);
}
}
Loading
Loading