mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-15 07:28:05 +00:00
checkpoint: introduce copilotSDK as utility process
This commit is contained in:
146
package-lock.json
generated
146
package-lock.json
generated
@@ -11,6 +11,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sandbox-runtime": "0.0.23",
|
||||
"@github/copilot-sdk": "0.1.23",
|
||||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
"@microsoft/1ds-post-js": "^3.2.13",
|
||||
"@parcel/watcher": "^2.5.6",
|
||||
@@ -1042,6 +1043,142 @@
|
||||
"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot": {
|
||||
"version": "0.0.403",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot/-/copilot-0.0.403.tgz",
|
||||
"integrity": "sha512-v5jUdtGJReLmE1rmff/LZf+50nzmYQYAaSRNtVNr9g0j0GkCd/noQExe31i1+PudvWU0ZJjltR0B8pUfDRdA9Q==",
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"bin": {
|
||||
"copilot": "npm-loader.js"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"@github/copilot-darwin-arm64": "0.0.403",
|
||||
"@github/copilot-darwin-x64": "0.0.403",
|
||||
"@github/copilot-linux-arm64": "0.0.403",
|
||||
"@github/copilot-linux-x64": "0.0.403",
|
||||
"@github/copilot-win32-arm64": "0.0.403",
|
||||
"@github/copilot-win32-x64": "0.0.403"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-darwin-arm64": {
|
||||
"version": "0.0.403",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-arm64/-/copilot-darwin-arm64-0.0.403.tgz",
|
||||
"integrity": "sha512-dOw8IleA0d1soHnbr/6wc6vZiYWNTKMgfTe/NET1nCfMzyKDt/0F0I7PT5y+DLujJknTla/ZeEmmBUmliTW4Cg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"bin": {
|
||||
"copilot-darwin-arm64": "copilot"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-darwin-x64": {
|
||||
"version": "0.0.403",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-darwin-x64/-/copilot-darwin-x64-0.0.403.tgz",
|
||||
"integrity": "sha512-aK2jSNWgY8eiZ+TmrvGhssMCPDTKArc0ip6Ul5OaslpytKks8hyXoRbxGD0N9sKioSUSbvKUf+1AqavbDpJO+w==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"bin": {
|
||||
"copilot-darwin-x64": "copilot"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-linux-arm64": {
|
||||
"version": "0.0.403",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-linux-arm64/-/copilot-linux-arm64-0.0.403.tgz",
|
||||
"integrity": "sha512-KhoR2iR70O6vCkzf0h8/K+p82qAgOvMTgAPm9bVEHvbdGFR7Py9qL5v03bMbPxsA45oNaZAkzDhfTAqWhIAZsQ==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"bin": {
|
||||
"copilot-linux-arm64": "copilot"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-linux-x64": {
|
||||
"version": "0.0.403",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-linux-x64/-/copilot-linux-x64-0.0.403.tgz",
|
||||
"integrity": "sha512-eoswUc9vo4TB+/9PgFJLVtzI4dPjkpJXdCsAioVuoqPdNxHxlIHFe9HaVcqMRZxUNY1YHEBZozy+IpUEGjgdfQ==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"linux"
|
||||
],
|
||||
"bin": {
|
||||
"copilot-linux-x64": "copilot"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-sdk": {
|
||||
"version": "0.1.23",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-sdk/-/copilot-sdk-0.1.23.tgz",
|
||||
"integrity": "sha512-0by81bsBQlDKE5VbcegZfUMvPyPm1aXwSGS2rGaMAFxv3ps+dACf1Voruxik7hQTae0ziVFJjuVrlxZoRaXBLw==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@github/copilot": "^0.0.403",
|
||||
"vscode-jsonrpc": "^8.2.1",
|
||||
"zod": "^4.3.6"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=24.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-sdk/node_modules/zod": {
|
||||
"version": "4.3.6",
|
||||
"resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
|
||||
"integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
|
||||
"license": "MIT",
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/colinhacks"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-win32-arm64": {
|
||||
"version": "0.0.403",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-win32-arm64/-/copilot-win32-arm64-0.0.403.tgz",
|
||||
"integrity": "sha512-djWjzCsp2xPNafMyOZ/ivU328/WvWhdroGie/DugiJBTgQL2SP0quWW1fhTlDwE81a3g9CxfJonaRgOpFTJTcg==",
|
||||
"cpu": [
|
||||
"arm64"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"bin": {
|
||||
"copilot-win32-arm64": "copilot.exe"
|
||||
}
|
||||
},
|
||||
"node_modules/@github/copilot-win32-x64": {
|
||||
"version": "0.0.403",
|
||||
"resolved": "https://registry.npmjs.org/@github/copilot-win32-x64/-/copilot-win32-x64-0.0.403.tgz",
|
||||
"integrity": "sha512-lju8cHy2E6Ux7R7tWyLZeksYC2MVZu9i9ocjiBX/qfG2/pNJs7S5OlkwKJ0BSXSbZEHQYq7iHfEWp201bVfk9A==",
|
||||
"cpu": [
|
||||
"x64"
|
||||
],
|
||||
"license": "SEE LICENSE IN LICENSE.md",
|
||||
"optional": true,
|
||||
"os": [
|
||||
"win32"
|
||||
],
|
||||
"bin": {
|
||||
"copilot-win32-x64": "copilot.exe"
|
||||
}
|
||||
},
|
||||
"node_modules/@gulp-sourcemaps/identity-map": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@gulp-sourcemaps/identity-map/-/identity-map-2.0.1.tgz",
|
||||
@@ -17610,6 +17747,15 @@
|
||||
"node": ">= 0.10"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-jsonrpc": {
|
||||
"version": "8.2.1",
|
||||
"resolved": "https://registry.npmjs.org/vscode-jsonrpc/-/vscode-jsonrpc-8.2.1.tgz",
|
||||
"integrity": "sha512-kdjOSJ2lLIn7r1rtrMbbNCHjyMPfRnowdKjBQ+mGq6NAW5QY2bEZC/khaC5OR8svbbjvLEaIXkOq45e2X9BIbQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=14.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/vscode-oniguruma": {
|
||||
"version": "1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/vscode-oniguruma/-/vscode-oniguruma-1.7.0.tgz",
|
||||
|
||||
@@ -76,6 +76,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sandbox-runtime": "0.0.23",
|
||||
"@github/copilot-sdk": "0.1.23",
|
||||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
"@microsoft/1ds-post-js": "^3.2.13",
|
||||
"@parcel/watcher": "^2.5.6",
|
||||
|
||||
@@ -117,6 +117,8 @@ import { ipcUtilityProcessWorkerChannelName } from '../../platform/utilityProces
|
||||
import { ILocalPtyService, LocalReconnectConstants, TerminalIpcChannels, TerminalSettingId } from '../../platform/terminal/common/terminal.js';
|
||||
import { ElectronPtyHostStarter } from '../../platform/terminal/electron-main/electronPtyHostStarter.js';
|
||||
import { PtyHostService } from '../../platform/terminal/node/ptyHostService.js';
|
||||
import { CopilotSdkMainService } from '../../platform/copilotSdk/electron-main/copilotSdkStarter.js';
|
||||
import { ICopilotSdkMainService, CopilotSdkChannel } from '../../platform/copilotSdk/common/copilotSdkService.js';
|
||||
import { NODE_REMOTE_RESOURCE_CHANNEL_NAME, NODE_REMOTE_RESOURCE_IPC_METHOD_NAME, NodeRemoteResourceResponse, NodeRemoteResourceRouter } from '../../platform/remote/common/electronRemoteResources.js';
|
||||
import { Lazy } from '../../base/common/lazy.js';
|
||||
import { IAuxiliaryWindowsMainService } from '../../platform/auxiliaryWindow/electron-main/auxiliaryWindows.js';
|
||||
@@ -1082,6 +1084,9 @@ export class CodeApplication extends Disposable {
|
||||
);
|
||||
services.set(ILocalPtyService, ptyHostService);
|
||||
|
||||
// Copilot SDK (utility process host for sessions window)
|
||||
services.set(ICopilotSdkMainService, new SyncDescriptor(CopilotSdkMainService, undefined, true));
|
||||
|
||||
// External terminal
|
||||
if (isWindows) {
|
||||
services.set(IExternalTerminalMainService, new SyncDescriptor(WindowsExternalTerminalService));
|
||||
@@ -1260,6 +1265,9 @@ export class CodeApplication extends Disposable {
|
||||
const mcpGatewayChannel = this._register(new McpGatewayChannel(mainProcessElectronServer, accessor.get(IMcpGatewayService)));
|
||||
mainProcessElectronServer.registerChannel(McpGatewayChannelName, mcpGatewayChannel);
|
||||
|
||||
// Copilot SDK (for sessions window)
|
||||
mainProcessElectronServer.registerChannel(CopilotSdkChannel, accessor.get(ICopilotSdkMainService).getServerChannel());
|
||||
|
||||
// Logger
|
||||
const loggerChannel = new LoggerChannel(accessor.get(ILoggerMainService),);
|
||||
mainProcessElectronServer.registerChannel('logger', loggerChannel);
|
||||
|
||||
247
src/vs/platform/copilotSdk/common/copilotSdkService.ts
Normal file
247
src/vs/platform/copilotSdk/common/copilotSdkService.ts
Normal file
@@ -0,0 +1,247 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from '../../../base/common/event.js';
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import type { IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
|
||||
// #region Service Identifiers
|
||||
|
||||
export const ICopilotSdkService = createDecorator<ICopilotSdkService>('copilotSdkService');
|
||||
|
||||
/**
|
||||
* Main process service identifier. The main process implementation manages
|
||||
* the utility process lifecycle and proxies the channel.
|
||||
*/
|
||||
export const ICopilotSdkMainService = createDecorator<ICopilotSdkMainService>('copilotSdkMainService');
|
||||
|
||||
/**
|
||||
* IPC channel name used to register the Copilot SDK service.
|
||||
* Defined in the common layer so both main and renderer can reference it
|
||||
* without importing the utility process host module.
|
||||
*/
|
||||
export const CopilotSdkChannel = 'copilotSdk';
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Session Types
|
||||
|
||||
export interface ICopilotSessionConfig {
|
||||
readonly model?: string;
|
||||
readonly reasoningEffort?: 'low' | 'medium' | 'high' | 'xhigh';
|
||||
readonly streaming?: boolean;
|
||||
readonly systemMessage?: { readonly content: string; readonly mode?: 'append' | 'replace' };
|
||||
readonly workingDirectory?: string;
|
||||
}
|
||||
|
||||
export interface ICopilotResumeSessionConfig {
|
||||
readonly streaming?: boolean;
|
||||
}
|
||||
|
||||
export interface ICopilotSendOptions {
|
||||
readonly attachments?: readonly ICopilotAttachment[];
|
||||
readonly mode?: 'enqueue' | 'immediate';
|
||||
}
|
||||
|
||||
export interface ICopilotAttachment {
|
||||
readonly type: 'file';
|
||||
readonly path: string;
|
||||
readonly displayName?: string;
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Session Metadata
|
||||
|
||||
export interface ICopilotSessionMetadata {
|
||||
readonly sessionId: string;
|
||||
readonly workspacePath?: string;
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Events
|
||||
|
||||
/**
|
||||
* Event types emitted by the Copilot SDK session.
|
||||
*
|
||||
* These map directly to the SDK's event types:
|
||||
* - `user.message` -- user prompt added
|
||||
* - `assistant.message` -- complete assistant response
|
||||
* - `assistant.message_delta` -- streaming response chunk
|
||||
* - `assistant.reasoning` -- complete reasoning content
|
||||
* - `assistant.reasoning_delta` -- streaming reasoning chunk
|
||||
* - `tool.execution_start` -- tool call started
|
||||
* - `tool.execution_complete` -- tool call finished
|
||||
* - `session.idle` -- session finished processing
|
||||
* - `session.compaction_start` -- context compaction started
|
||||
* - `session.compaction_complete` -- context compaction finished
|
||||
*/
|
||||
export type CopilotSessionEventType =
|
||||
| 'user.message'
|
||||
| 'assistant.message'
|
||||
| 'assistant.message_delta'
|
||||
| 'assistant.reasoning'
|
||||
| 'assistant.reasoning_delta'
|
||||
| 'tool.execution_start'
|
||||
| 'tool.execution_complete'
|
||||
| 'session.idle'
|
||||
| 'session.compaction_start'
|
||||
| 'session.compaction_complete';
|
||||
|
||||
export interface ICopilotSessionEvent {
|
||||
readonly sessionId: string;
|
||||
readonly type: CopilotSessionEventType;
|
||||
/** Event payload. Shape varies by type -- matches the SDK's event.data. */
|
||||
readonly data: ICopilotSessionEventData;
|
||||
}
|
||||
|
||||
export interface ICopilotSessionEventData {
|
||||
/** For `assistant.message`: the complete content. */
|
||||
readonly content?: string;
|
||||
/** For `assistant.message_delta`: the incremental content chunk. */
|
||||
readonly deltaContent?: string;
|
||||
/** For `tool.execution_start` / `tool.execution_complete`: the tool name. */
|
||||
readonly toolName?: string;
|
||||
/** Generic data passthrough for fields not explicitly typed. */
|
||||
readonly [key: string]: unknown;
|
||||
}
|
||||
|
||||
/**
|
||||
* Session lifecycle events fired by the SDK client (not per-session).
|
||||
*/
|
||||
export type CopilotSessionLifecycleType =
|
||||
| 'session.created'
|
||||
| 'session.deleted'
|
||||
| 'session.updated';
|
||||
|
||||
export interface ICopilotSessionLifecycleEvent {
|
||||
readonly type: CopilotSessionLifecycleType;
|
||||
readonly sessionId: string;
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Model Info
|
||||
|
||||
export interface ICopilotModelInfo {
|
||||
readonly id: string;
|
||||
readonly name?: string;
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Assistant Message
|
||||
|
||||
export interface ICopilotAssistantMessage {
|
||||
readonly content: string;
|
||||
}
|
||||
|
||||
export interface ICopilotProcessOutput {
|
||||
readonly stream: 'stdout' | 'stderr';
|
||||
readonly data: string;
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Service Interface
|
||||
|
||||
export interface ICopilotSdkService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
// --- Lifecycle ---
|
||||
|
||||
/**
|
||||
* Start the SDK client. Spawns the Copilot CLI if not already running.
|
||||
* Called automatically on first use if the utility process is alive.
|
||||
*/
|
||||
start(): Promise<void>;
|
||||
|
||||
/**
|
||||
* Stop the SDK client and the underlying CLI process.
|
||||
*/
|
||||
stop(): Promise<void>;
|
||||
|
||||
// --- Sessions ---
|
||||
|
||||
/** Create a new session. Returns the session ID. */
|
||||
createSession(config: ICopilotSessionConfig): Promise<string>;
|
||||
|
||||
/** Resume an existing session by ID. */
|
||||
resumeSession(sessionId: string, config?: ICopilotResumeSessionConfig): Promise<void>;
|
||||
|
||||
/** Destroy a session (free resources, but don't delete from disk). */
|
||||
destroySession(sessionId: string): Promise<void>;
|
||||
|
||||
/** List all available sessions. */
|
||||
listSessions(): Promise<ICopilotSessionMetadata[]>;
|
||||
|
||||
/** Delete a session and its data from disk. */
|
||||
deleteSession(sessionId: string): Promise<void>;
|
||||
|
||||
// --- Messaging ---
|
||||
|
||||
/** Send a message to a session. Returns the message ID. */
|
||||
send(sessionId: string, prompt: string, options?: ICopilotSendOptions): Promise<string>;
|
||||
|
||||
/** Send a message and wait until the session is idle. */
|
||||
sendAndWait(sessionId: string, prompt: string, options?: ICopilotSendOptions): Promise<ICopilotAssistantMessage | undefined>;
|
||||
|
||||
/** Abort the active response in a session. */
|
||||
abort(sessionId: string): Promise<void>;
|
||||
|
||||
/** Get all events/messages from a session. */
|
||||
getMessages(sessionId: string): Promise<ICopilotSessionEvent[]>;
|
||||
|
||||
// --- Events ---
|
||||
|
||||
/**
|
||||
* Fires for all session events (streaming deltas, tool calls, idle, etc.).
|
||||
* Multiplexed by sessionId -- consumers filter by the session they care about.
|
||||
*/
|
||||
readonly onSessionEvent: Event<ICopilotSessionEvent>;
|
||||
|
||||
/**
|
||||
* Fires for session lifecycle changes (created, deleted, updated).
|
||||
*/
|
||||
readonly onSessionLifecycle: Event<ICopilotSessionLifecycleEvent>;
|
||||
|
||||
/**
|
||||
* Fires for raw CLI process output (stdout/stderr from the utility process).
|
||||
* Used for debugging -- shows the Copilot CLI's raw output.
|
||||
*/
|
||||
readonly onProcessOutput: Event<ICopilotProcessOutput>;
|
||||
|
||||
// --- Models ---
|
||||
|
||||
/** List available models. */
|
||||
listModels(): Promise<ICopilotModelInfo[]>;
|
||||
|
||||
// --- Authentication ---
|
||||
|
||||
/** Set the GitHub token used by the SDK for authentication. */
|
||||
setGitHubToken(token: string): Promise<void>;
|
||||
}
|
||||
|
||||
// #endregion
|
||||
|
||||
// #region Main Process Service Interface
|
||||
|
||||
/**
|
||||
* Main process service that manages the Copilot SDK utility process.
|
||||
* Registered as a DI service in the main process and exposed via
|
||||
* `ProxyChannel.fromService()` for the renderer to consume.
|
||||
*/
|
||||
export interface ICopilotSdkMainService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
/**
|
||||
* Get the IServerChannel for registering on the Electron IPC server.
|
||||
* The channel lazily spawns the utility process on first use.
|
||||
*/
|
||||
getServerChannel(): IServerChannel<string>;
|
||||
}
|
||||
|
||||
// #endregion
|
||||
113
src/vs/platform/copilotSdk/electron-main/copilotSdkStarter.ts
Normal file
113
src/vs/platform/copilotSdk/electron-main/copilotSdkStarter.ts
Normal file
@@ -0,0 +1,113 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Event } from '../../../base/common/event.js';
|
||||
import { Disposable, DisposableStore, toDisposable } from '../../../base/common/lifecycle.js';
|
||||
import { ILogService } from '../../log/common/log.js';
|
||||
import { ILifecycleMainService } from '../../lifecycle/electron-main/lifecycleMainService.js';
|
||||
import { UtilityProcess } from '../../utilityProcess/electron-main/utilityProcess.js';
|
||||
import { NullTelemetryService } from '../../telemetry/common/telemetryUtils.js';
|
||||
import { Client as MessagePortClient } from '../../../base/parts/ipc/electron-main/ipc.mp.js';
|
||||
import type { IServerChannel, IChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { CopilotSdkChannel, ICopilotSdkMainService } from '../common/copilotSdkService.js';
|
||||
import { Schemas } from '../../../base/common/network.js';
|
||||
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
|
||||
import { deepClone } from '../../../base/common/objects.js';
|
||||
import { CancellationToken } from '../../../base/common/cancellation.js';
|
||||
|
||||
/**
|
||||
* Manages the Copilot SDK utility process in the main process.
|
||||
*
|
||||
* Follows the terminal (pty host) pattern: spawns a UtilityProcess lazily
|
||||
* on first use, connects via MessagePort, and exposes an IServerChannel.
|
||||
*/
|
||||
export class CopilotSdkMainService extends Disposable implements ICopilotSdkMainService {
|
||||
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private _utilityProcess: UtilityProcess | undefined;
|
||||
private _channel: IChannel | undefined;
|
||||
private _connectionStore: DisposableStore | undefined;
|
||||
|
||||
constructor(
|
||||
@ILogService private readonly _logService: ILogService,
|
||||
@ILifecycleMainService private readonly _lifecycleMainService: ILifecycleMainService,
|
||||
@IEnvironmentMainService private readonly _environmentMainService: IEnvironmentMainService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this._register(this._lifecycleMainService.onWillShutdown(() => {
|
||||
this._teardown();
|
||||
}));
|
||||
}
|
||||
|
||||
private _ensureChannel(): IChannel {
|
||||
if (this._channel) {
|
||||
return this._channel;
|
||||
}
|
||||
|
||||
this._logService.info('[CopilotSdkMainService] Starting Copilot SDK utility process');
|
||||
|
||||
this._connectionStore = new DisposableStore();
|
||||
|
||||
this._utilityProcess = new UtilityProcess(this._logService, NullTelemetryService, this._lifecycleMainService);
|
||||
this._connectionStore.add(toDisposable(() => {
|
||||
this._utilityProcess?.kill();
|
||||
this._utilityProcess?.dispose();
|
||||
this._utilityProcess = undefined;
|
||||
}));
|
||||
|
||||
this._connectionStore.add(this._utilityProcess.onStdout(data => this._logService.info(`[CopilotSdkHost:stdout] ${data}`)));
|
||||
this._connectionStore.add(this._utilityProcess.onStderr(data => this._logService.warn(`[CopilotSdkHost:stderr] ${data}`)));
|
||||
this._connectionStore.add(this._utilityProcess.onExit(e => this._logService.error(`[CopilotSdkHost] Process exited with code ${e.code}`)));
|
||||
this._connectionStore.add(this._utilityProcess.onCrash(e => this._logService.error(`[CopilotSdkHost] Process crashed with code ${e.code}`)));
|
||||
|
||||
this._utilityProcess.start({
|
||||
type: 'copilotSdkHost',
|
||||
name: 'copilot-sdk-host',
|
||||
entryPoint: 'vs/platform/copilotSdk/node/copilotSdkHost',
|
||||
args: ['--logsPath', this._environmentMainService.logsHome.with({ scheme: Schemas.file }).fsPath],
|
||||
env: {
|
||||
...deepClone(process.env) as Record<string, string>,
|
||||
VSCODE_ESM_ENTRYPOINT: 'vs/platform/copilotSdk/node/copilotSdkHost',
|
||||
VSCODE_PIPE_LOGGING: 'true',
|
||||
VSCODE_VERBOSE_LOGGING: 'true',
|
||||
},
|
||||
});
|
||||
|
||||
const port = this._utilityProcess.connect();
|
||||
const client = new MessagePortClient(port, 'copilotSdkHost');
|
||||
this._connectionStore.add(client);
|
||||
|
||||
this._channel = client.getChannel(CopilotSdkChannel);
|
||||
|
||||
this._logService.info('[CopilotSdkMainService] Copilot SDK utility process started');
|
||||
|
||||
return this._channel;
|
||||
}
|
||||
|
||||
getServerChannel(): IServerChannel<string> {
|
||||
return {
|
||||
listen: <T>(_ctx: string, event: string, arg?: unknown): Event<T> => {
|
||||
return this._ensureChannel().listen(event, arg);
|
||||
},
|
||||
call: <T>(_ctx: string, command: string, arg?: unknown, cancellationToken?: CancellationToken): Promise<T> => {
|
||||
return this._ensureChannel().call<T>(command, arg, cancellationToken);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private _teardown(): void {
|
||||
this._connectionStore?.dispose();
|
||||
this._connectionStore = undefined;
|
||||
this._channel = undefined;
|
||||
this._utilityProcess = undefined;
|
||||
}
|
||||
|
||||
override dispose(): void {
|
||||
this._teardown();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
||||
255
src/vs/platform/copilotSdk/node/copilotSdkHost.ts
Normal file
255
src/vs/platform/copilotSdk/node/copilotSdkHost.ts
Normal file
@@ -0,0 +1,255 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { Disposable, DisposableStore } from '../../../base/common/lifecycle.js';
|
||||
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { Server as UtilityProcessServer } from '../../../base/parts/ipc/node/ipc.mp.js';
|
||||
import {
|
||||
CopilotSdkChannel,
|
||||
type ICopilotAssistantMessage,
|
||||
type ICopilotModelInfo,
|
||||
type ICopilotResumeSessionConfig,
|
||||
type ICopilotSdkService,
|
||||
type ICopilotSendOptions,
|
||||
type ICopilotSessionConfig,
|
||||
type ICopilotSessionEvent,
|
||||
type ICopilotSessionLifecycleEvent,
|
||||
type ICopilotSessionMetadata,
|
||||
} from '../common/copilotSdkService.js';
|
||||
// eslint-disable-next-line local/code-import-patterns
|
||||
import type { CopilotClient, CopilotSession, SessionEvent, SessionLifecycleEvent } from '@github/copilot-sdk';
|
||||
|
||||
/**
|
||||
* The Copilot SDK host runs in a utility process and wraps the
|
||||
* `@github/copilot-sdk` `CopilotClient`. It implements `ICopilotSdkService`
|
||||
* so that `ProxyChannel.fromService()` can auto-generate an IPC channel
|
||||
* from it -- all methods become RPC calls and all `onFoo` events are
|
||||
* forwarded over the channel automatically.
|
||||
*/
|
||||
class CopilotSdkHost extends Disposable implements ICopilotSdkService {
|
||||
declare readonly _serviceBrand: undefined;
|
||||
|
||||
private _client: CopilotClient | undefined;
|
||||
private readonly _sessions = new Map<string, CopilotSession>();
|
||||
private _githubToken: string | undefined;
|
||||
|
||||
// --- Events ---
|
||||
private readonly _onSessionEvent = this._register(new Emitter<ICopilotSessionEvent>());
|
||||
readonly onSessionEvent: Event<ICopilotSessionEvent> = this._onSessionEvent.event;
|
||||
|
||||
private readonly _onSessionLifecycle = this._register(new Emitter<ICopilotSessionLifecycleEvent>());
|
||||
readonly onSessionLifecycle: Event<ICopilotSessionLifecycleEvent> = this._onSessionLifecycle.event;
|
||||
|
||||
private readonly _onProcessOutput = this._register(new Emitter<{ stream: 'stdout' | 'stderr'; data: string }>());
|
||||
readonly onProcessOutput: Event<{ stream: 'stdout' | 'stderr'; data: string }> = this._onProcessOutput.event;
|
||||
|
||||
// --- Lifecycle ---
|
||||
|
||||
async start(): Promise<void> {
|
||||
if (this._client) {
|
||||
return;
|
||||
}
|
||||
|
||||
const sdk = await import('@github/copilot-sdk');
|
||||
this._client = new sdk.CopilotClient({
|
||||
autoStart: true,
|
||||
autoRestart: true,
|
||||
useStdio: true,
|
||||
...(this._githubToken ? { githubToken: this._githubToken } : {}),
|
||||
});
|
||||
await this._client.start();
|
||||
|
||||
// Intercept stderr to capture CLI subprocess output and forward as events.
|
||||
// The SDK writes CLI stderr lines to process.stderr via its internal
|
||||
// `[CLI subprocess]` handler.
|
||||
const originalStderrWrite = process.stderr.write.bind(process.stderr);
|
||||
process.stderr.write = (chunk: string | Uint8Array, ...args: unknown[]): boolean => {
|
||||
const text = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf-8');
|
||||
if (text.trim()) {
|
||||
this._onProcessOutput.fire({ stream: 'stderr', data: text.trimEnd() });
|
||||
}
|
||||
return originalStderrWrite(chunk, ...args as [BufferEncoding?, ((err?: Error | null) => void)?]);
|
||||
};
|
||||
|
||||
// Forward client lifecycle events
|
||||
this._client.on('session.created', (event: SessionLifecycleEvent) => {
|
||||
this._onSessionLifecycle.fire({ type: 'session.created', sessionId: event.sessionId });
|
||||
});
|
||||
this._client.on('session.deleted', (event: SessionLifecycleEvent) => {
|
||||
this._onSessionLifecycle.fire({ type: 'session.deleted', sessionId: event.sessionId });
|
||||
});
|
||||
this._client.on('session.updated', (event: SessionLifecycleEvent) => {
|
||||
this._onSessionLifecycle.fire({ type: 'session.updated', sessionId: event.sessionId });
|
||||
});
|
||||
}
|
||||
|
||||
async stop(): Promise<void> {
|
||||
if (!this._client) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [, session] of this._sessions) {
|
||||
try { await session.destroy(); } catch { /* best-effort */ }
|
||||
}
|
||||
this._sessions.clear();
|
||||
|
||||
await this._client.stop();
|
||||
this._client = undefined;
|
||||
}
|
||||
|
||||
// --- Sessions ---
|
||||
|
||||
async createSession(config: ICopilotSessionConfig): Promise<string> {
|
||||
const client = await this._ensureClient();
|
||||
const session = await client.createSession({
|
||||
model: config.model,
|
||||
reasoningEffort: config.reasoningEffort,
|
||||
streaming: config.streaming ?? true,
|
||||
systemMessage: config.systemMessage,
|
||||
workingDirectory: config.workingDirectory,
|
||||
});
|
||||
|
||||
this._sessions.set(session.sessionId, session);
|
||||
this._attachSessionEvents(session);
|
||||
|
||||
return session.sessionId;
|
||||
}
|
||||
|
||||
async resumeSession(sessionId: string, config?: ICopilotResumeSessionConfig): Promise<void> {
|
||||
const client = await this._ensureClient();
|
||||
const session = await client.resumeSession(sessionId, {
|
||||
streaming: config?.streaming ?? true,
|
||||
});
|
||||
|
||||
this._sessions.set(session.sessionId, session);
|
||||
this._attachSessionEvents(session);
|
||||
}
|
||||
|
||||
async destroySession(sessionId: string): Promise<void> {
|
||||
const session = this._sessions.get(sessionId);
|
||||
if (session) {
|
||||
await session.destroy();
|
||||
this._sessions.delete(sessionId);
|
||||
}
|
||||
}
|
||||
|
||||
async listSessions(): Promise<ICopilotSessionMetadata[]> {
|
||||
const client = await this._ensureClient();
|
||||
const sessions = await client.listSessions();
|
||||
return sessions.map((s: { sessionId: string; workspacePath?: string }) => ({
|
||||
sessionId: s.sessionId,
|
||||
workspacePath: s.workspacePath,
|
||||
}));
|
||||
}
|
||||
|
||||
async deleteSession(sessionId: string): Promise<void> {
|
||||
const client = await this._ensureClient();
|
||||
this._sessions.delete(sessionId);
|
||||
await client.deleteSession(sessionId);
|
||||
}
|
||||
|
||||
// --- Messaging ---
|
||||
|
||||
async send(sessionId: string, prompt: string, options?: ICopilotSendOptions): Promise<string> {
|
||||
const session = this._getSession(sessionId);
|
||||
return session.send({
|
||||
prompt,
|
||||
attachments: options?.attachments?.map(a => ({ type: a.type as 'file', path: a.path, displayName: a.displayName })),
|
||||
mode: options?.mode,
|
||||
});
|
||||
}
|
||||
|
||||
async sendAndWait(sessionId: string, prompt: string, options?: ICopilotSendOptions): Promise<ICopilotAssistantMessage | undefined> {
|
||||
const session = this._getSession(sessionId);
|
||||
const result = await session.sendAndWait({
|
||||
prompt,
|
||||
attachments: options?.attachments?.map(a => ({ type: a.type as 'file', path: a.path, displayName: a.displayName })),
|
||||
mode: options?.mode,
|
||||
});
|
||||
if (!result) {
|
||||
return undefined;
|
||||
}
|
||||
return { content: result.data.content };
|
||||
}
|
||||
|
||||
async abort(sessionId: string): Promise<void> {
|
||||
const session = this._getSession(sessionId);
|
||||
await session.abort();
|
||||
}
|
||||
|
||||
async getMessages(sessionId: string): Promise<ICopilotSessionEvent[]> {
|
||||
const session = this._getSession(sessionId);
|
||||
const events = await session.getMessages();
|
||||
return events.map((e: SessionEvent) => ({
|
||||
sessionId,
|
||||
type: e.type as ICopilotSessionEvent['type'],
|
||||
data: (e as { data?: Record<string, unknown> }).data ?? {},
|
||||
}));
|
||||
}
|
||||
|
||||
// --- Models ---
|
||||
|
||||
async listModels(): Promise<ICopilotModelInfo[]> {
|
||||
const client = await this._ensureClient();
|
||||
const models = await client.listModels();
|
||||
return models.map((m: { id: string; name?: string }) => ({
|
||||
id: m.id,
|
||||
name: m.name,
|
||||
}));
|
||||
}
|
||||
|
||||
// --- Authentication ---
|
||||
|
||||
async setGitHubToken(token: string): Promise<void> {
|
||||
this._githubToken = token;
|
||||
}
|
||||
|
||||
// --- Private helpers ---
|
||||
|
||||
private async _ensureClient(): Promise<CopilotClient> {
|
||||
if (!this._client) {
|
||||
await this.start();
|
||||
}
|
||||
return this._client!;
|
||||
}
|
||||
|
||||
private _getSession(sessionId: string): CopilotSession {
|
||||
const session = this._sessions.get(sessionId);
|
||||
if (!session) {
|
||||
throw new Error(`No active session with ID: ${sessionId}`);
|
||||
}
|
||||
return session;
|
||||
}
|
||||
|
||||
private _attachSessionEvents(session: CopilotSession): void {
|
||||
const sessionId = session.sessionId;
|
||||
|
||||
session.on((event: SessionEvent) => {
|
||||
this._onSessionEvent.fire({
|
||||
sessionId,
|
||||
type: event.type as ICopilotSessionEvent['type'],
|
||||
data: (event as { data?: Record<string, unknown> }).data ?? {},
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// --- Entry point ---
|
||||
// Only start when running as an Electron utility process (not when imported by the main process).
|
||||
import { isUtilityProcess } from '../../../base/parts/sandbox/node/electronTypes.js';
|
||||
if (isUtilityProcess(process)) {
|
||||
const disposables = new DisposableStore();
|
||||
const host = new CopilotSdkHost();
|
||||
disposables.add(host);
|
||||
const channel = ProxyChannel.fromService(host, disposables);
|
||||
const server = new UtilityProcessServer();
|
||||
server.registerChannel(CopilotSdkChannel, channel);
|
||||
|
||||
process.once('exit', () => {
|
||||
host.stop().catch(() => { /* best-effort cleanup */ });
|
||||
disposables.dispose();
|
||||
});
|
||||
}
|
||||
832
src/vs/sessions/COPILOT_SDK_PLAN.md
Normal file
832
src/vs/sessions/COPILOT_SDK_PLAN.md
Normal file
@@ -0,0 +1,832 @@
|
||||
# Copilot SDK Adoption Plan — Sessions Window
|
||||
|
||||
> **Scope:** Sessions window (`src/vs/sessions/`) only. Regular VS Code chat (sidebar, inline, editor) stays entirely on the copilot-chat extension and is untouched.
|
||||
|
||||
---
|
||||
|
||||
## 1. Motivation
|
||||
|
||||
The sessions window currently depends on the **copilot-chat extension** for Background (worktree) and Cloud sessions. This creates a deep coupling:
|
||||
|
||||
- Session creation goes through VS Code's extension API layer (`chatSessionsProvider` proposed API)
|
||||
- The chat UI reuses `ChatWidget` / `ChatInputPart` / `ChatModel` from `vs/workbench/contrib/chat/`
|
||||
- Session listing depends on `ChatSessionItemProvider` registered by the extension
|
||||
- The extension owns the CLI process lifecycle, agent orchestration, tool execution, and streaming
|
||||
|
||||
By adopting the **[Copilot SDK](https://github.com/github/copilot-sdk)** (`@github/copilot-sdk`), the sessions window can own the full stack: CLI management, session lifecycle, chat UI, and tool execution — without going through the extension API layer.
|
||||
|
||||
---
|
||||
|
||||
## 2. Architecture Overview
|
||||
|
||||
### Process Model (Terminal Pattern)
|
||||
|
||||
The sessions window renderer is **sandboxed** (`sandbox: true`, no Node.js). The Copilot SDK
|
||||
needs Node.js to spawn the CLI child process. We follow the **terminal (pty host) pattern** —
|
||||
the most proven architecture in VS Code for long-running child processes streaming data to
|
||||
the renderer.
|
||||
|
||||
Three-layer architecture:
|
||||
|
||||
```
|
||||
┌────────────────────────────────────────────────────────────────┐
|
||||
│ RENDERER (sessions window) │
|
||||
│ │
|
||||
│ @ICopilotSdkService ← registerMainProcessRemoteService(...) │
|
||||
│ (automatic proxy — all calls/events go over IPC transparently)│
|
||||
│ │
|
||||
│ Chat UI, session list, etc. just inject ICopilotSdkService │
|
||||
└──────────────────────┬─────────────────────────────────────────┘
|
||||
│ Electron IPC (automatic via ProxyChannel)
|
||||
┌──────────────────────┴─────────────────────────────────────────┐
|
||||
│ MAIN PROCESS │
|
||||
│ │
|
||||
│ CopilotSdkMainService — thin proxy to the utility process │
|
||||
│ • Spawns utility process on first use (lazy) │
|
||||
│ • Forwards the IPC channel via MessagePort │
|
||||
│ • Handles restart if utility process crashes │
|
||||
│ Registers: server.registerChannel('copilotSdk', channel) │
|
||||
└──────────────────────┬─────────────────────────────────────────┘
|
||||
│ MessagePort (UtilityProcess.connect())
|
||||
┌──────────────────────┴─────────────────────────────────────────┐
|
||||
│ UTILITY PROCESS (copilotSdkHost) │
|
||||
│ │
|
||||
│ CopilotClient (@github/copilot-sdk) │
|
||||
│ → spawns copilot CLI (--server --stdio) │
|
||||
│ → manages sessions, streams events, handles tool callbacks │
|
||||
│ │
|
||||
│ Wrapped as IServerChannel via ProxyChannel.fromService() │
|
||||
│ Events (onSessionEvent, etc.) auto-forwarded over the channel │
|
||||
└────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### Why This Pattern
|
||||
|
||||
The renderer is sandboxed — the SDK **cannot** run there. The extension host is risky
|
||||
because restarts kill all child processes. The terminal (pty host) pattern is battle-tested
|
||||
for exactly this: long-running child processes streaming high-frequency data.
|
||||
|
||||
| Concern | How It's Handled |
|
||||
|---------|-----------------|
|
||||
| Renderer has no Node.js | `registerMainProcessRemoteService` creates a transparent proxy — renderer code just injects `@ICopilotSdkService` |
|
||||
| SDK needs child_process | Utility process has full Node.js — spawns CLI via the SDK |
|
||||
| High-frequency streaming | `ProxyChannel` auto-forwards `Event<T>` properties over IPC with buffering (same as terminal data streaming) |
|
||||
| Process isolation | Utility process crash doesn't affect main process or renderer |
|
||||
| Lifecycle | Utility process tied to sessions window — spawned on first use, killed on window close |
|
||||
| Adding new methods | Add to interface + implement in host — proxy layers auto-handle via `ProxyChannel` |
|
||||
|
||||
### Key Decisions
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| SDK vs raw JSON-RPC | **SDK** (`@github/copilot-sdk`) | Handles CLI lifecycle, typed events, tool dispatch, streaming, auth. Avoids reimplementing the raw JSON-RPC protocol layer. |
|
||||
| SDK hosting | **Utility process** via terminal pattern | Full isolation. `registerMainProcessRemoteService` gives trivial renderer-side DI. Main process is a thin proxy. |
|
||||
| CLI binary | **Bundled** (npm package or build-time download) + **PATH fallback** for dev | Production users can't be expected to install the CLI. Dev workflow uses PATH discovery. |
|
||||
| Chat UI | **New, purpose-built** in `src/vs/sessions/` | No dependency on `ChatWidget` / `ChatInputPart` / `ChatModel`. Clean slate. |
|
||||
| Session list | **SDK-based** (`listSessions()`) | Full decoupling from copilot-chat's `ChatSessionItemProvider`. |
|
||||
| Worktrees | **CLI creates, git extension manages** | CLI creates worktrees autonomously. We discover the path from `session.workspacePath`. Git extension (in the session window's extension host) handles diff stats, apply changes, commits. |
|
||||
| Extension host | **Kept** for git extension, language services, terminal | Sessions window already has its own extension host. We keep it for git operations, not for the SDK. |
|
||||
|
||||
### Event Multiplexing
|
||||
|
||||
Sessions are multiplexed by session ID through a single IPC channel (matching the terminal
|
||||
pattern of multiplexing by terminal ID):
|
||||
|
||||
```typescript
|
||||
// Single event carrying all session events, demuxed by sessionId in the renderer
|
||||
onSessionEvent: Event<{ sessionId: string; event: ISessionEvent }>
|
||||
```
|
||||
|
||||
`ProxyChannel.fromService()` auto-detects `Event<T>` properties and buffers them. No manual
|
||||
batching needed — the terminal streams data at higher frequency and works fine with this.
|
||||
|
||||
### Reference Patterns
|
||||
|
||||
| VS Code component | Our equivalent | Pattern file |
|
||||
|-------------------|---------------|-------------|
|
||||
| `ILocalPtyService` | `ICopilotSdkService` | `src/vs/platform/terminal/common/terminal.ts` |
|
||||
| `ElectronPtyHostStarter` | `CopilotSdkMainService` | `src/vs/platform/terminal/electron-main/electronPtyHostStarter.ts` |
|
||||
| `ptyHostMain.ts` | `copilotSdkHost.ts` | `src/vs/platform/terminal/node/ptyHostMain.ts` |
|
||||
| `registerMainProcessRemoteService(ILocalPtyService, ...)` | `registerMainProcessRemoteService(ICopilotSdkService, 'copilotSdk')` | `src/vs/platform/ipc/electron-browser/services.ts` |
|
||||
| `LocalTerminalBackend` (event demuxing) | Session event routing in chat UI | `src/vs/workbench/contrib/terminal/electron-browser/localTerminalBackend.ts` |
|
||||
|
||||
---
|
||||
|
||||
## 3. What the Copilot SDK Provides
|
||||
|
||||
From the [Node.js SDK docs](https://github.com/github/copilot-sdk/blob/main/nodejs/README.md):
|
||||
|
||||
### CopilotClient
|
||||
|
||||
```typescript
|
||||
const client = new CopilotClient({
|
||||
cliPath: '/path/to/bundled/copilot', // or omit for PATH discovery
|
||||
githubToken: '<from IAuthenticationService>',
|
||||
autoStart: true,
|
||||
autoRestart: true,
|
||||
});
|
||||
```
|
||||
|
||||
**Methods:** `start()`, `stop()`, `createSession(config)`, `resumeSession(id, config)`, `listSessions()`, `deleteSession(id)`, `ping()`, `getState()`, `listModels()`
|
||||
|
||||
### CopilotSession
|
||||
|
||||
```typescript
|
||||
const session = await client.createSession({
|
||||
model: 'gpt-4.1',
|
||||
streaming: true,
|
||||
tools: [myTool],
|
||||
systemMessage: { content: '...' },
|
||||
infiniteSessions: { enabled: true },
|
||||
mcpServers: { github: { type: 'http', url: '...' } },
|
||||
customAgents: [{ name: 'reviewer', prompt: '...' }],
|
||||
onUserInputRequest: async (request) => ({ answer: '...' }),
|
||||
hooks: { onPreToolUse: async (input) => ({ permissionDecision: 'allow' }) },
|
||||
});
|
||||
```
|
||||
|
||||
**Methods:** `send(options)`, `sendAndWait(options)`, `abort()`, `getMessages()`, `destroy()`
|
||||
|
||||
**Events (via `session.on()`):**
|
||||
- `user.message` — user prompt added
|
||||
- `assistant.message` — complete response
|
||||
- `assistant.message_delta` — streaming chunk
|
||||
- `assistant.reasoning_delta` — reasoning/chain-of-thought chunk
|
||||
- `tool.execution_start` — tool call begun
|
||||
- `tool.execution_complete` — tool call finished
|
||||
- `session.idle` — session finished processing
|
||||
- `session.compaction_start/complete` — context compaction (infinite sessions)
|
||||
|
||||
### Tools
|
||||
|
||||
```typescript
|
||||
import { defineTool } from '@github/copilot-sdk';
|
||||
|
||||
const myTool = defineTool('tool_name', {
|
||||
description: '...',
|
||||
parameters: { type: 'object', properties: { ... } },
|
||||
handler: async (args) => { return result; },
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Implementation Phases
|
||||
|
||||
### Phase 0: Copilot CLI Binary Bundling
|
||||
|
||||
**Goal:** Ship the `copilot` CLI binary with VS Code so end users don't need to install it separately.
|
||||
|
||||
**Options:**
|
||||
|
||||
| Option | Approach | Pros | Cons |
|
||||
|--------|----------|------|------|
|
||||
| **A. npm package** | Create/use a `@github/copilot-cli` npm package (like `@vscode/ripgrep`) | Follows existing VS Code pattern exactly. Build pipeline just adds ASAR unpack rule in `gulpfile.vscode.ts`. | Requires publishing the CLI as an npm package. Need to coordinate with CLI team. |
|
||||
| **B. Build-time download** | Add gulp task to download CLI binary per platform/arch (like Node.js binary in `gulpfile.reh.ts`) | Full control over version. Can pin checksums. | More build infrastructure. Need download URLs per platform/arch. |
|
||||
| **C. PATH discovery only** | Find `copilot` on PATH at launch | Zero build changes. Works immediately for dev. | Not viable for production — can't ship to users without CLI installed. |
|
||||
|
||||
**Recommended:** Option A for production + Option C for development.
|
||||
|
||||
**Development workflow:** The SDK can find `copilot` on PATH by default (no `cliPath` needed). Developers just need the CLI installed.
|
||||
|
||||
**Production:** The CLI binary is bundled via npm package, excluded from ASAR (like ripgrep), and the path is resolved at runtime:
|
||||
```typescript
|
||||
// Similar to ripgrep path resolution
|
||||
import { cliPath } from '@github/copilot-cli';
|
||||
const copilotDiskPath = cliPath.replace(/\bnode_modules\.asar\b/, 'node_modules.asar.unpacked');
|
||||
```
|
||||
|
||||
**Build pipeline changes:**
|
||||
- Add `@github/copilot-cli` (or equivalent) to `package.json`
|
||||
- Add ASAR unpack rule in `build/gulpfile.vscode.ts`: `'**/@github/copilot-cli/bin/*'`
|
||||
- Add to `cgmanifest.json` for third-party declaration
|
||||
- Add checksum validation
|
||||
|
||||
**Files to modify:**
|
||||
- `package.json` — add dependency
|
||||
- `build/gulpfile.vscode.ts` — ASAR unpack rule
|
||||
- `cgmanifest.json` — third-party declaration
|
||||
- `product.json` — potentially add `copilotCliPath` configuration
|
||||
|
||||
---
|
||||
|
||||
### Phase 1: SDK Host Process (Terminal Pattern)
|
||||
|
||||
**Goal:** Create a utility process hosting the `CopilotClient`, exposed via `registerMainProcessRemoteService` so any workbench code can inject `@ICopilotSdkService`.
|
||||
|
||||
**New files (4 files + 1 registration line):**
|
||||
|
||||
| # | File | Layer | Purpose |
|
||||
|---|------|-------|---------|
|
||||
| 1 | `src/vs/sessions/common/copilotSdkService.ts` | Common | `ICopilotSdkService` interface + all types. The contract. |
|
||||
| 2 | `src/vs/sessions/node/copilotSdkHost.ts` | Utility process | Entry point. Creates `CopilotClient`, wraps as `IServerChannel` via `ProxyChannel.fromService()`. |
|
||||
| 3 | `src/vs/sessions/electron-main/copilotSdkMainService.ts` | Main process | Spawns utility process, proxies the channel to the Electron IPC server. Handles restart on crash. |
|
||||
| 4 | `src/vs/sessions/electron-browser/copilotSdkService.ts` | Renderer | One-liner: `registerMainProcessRemoteService(ICopilotSdkService, 'copilotSdk')` |
|
||||
|
||||
**Service interface:**
|
||||
|
||||
```typescript
|
||||
interface ICopilotSdkService {
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
// Lifecycle
|
||||
start(): Promise<void>;
|
||||
stop(): Promise<void>;
|
||||
|
||||
// Sessions
|
||||
createSession(config: ISessionConfig): Promise<string>; // returns sessionId
|
||||
resumeSession(sessionId: string, config?: IResumeSessionConfig): Promise<void>;
|
||||
destroySession(sessionId: string): Promise<void>;
|
||||
listSessions(): Promise<ISessionMetadata[]>;
|
||||
deleteSession(sessionId: string): Promise<void>;
|
||||
|
||||
// Messaging
|
||||
send(sessionId: string, prompt: string, options?: ISendOptions): Promise<string>;
|
||||
sendAndWait(sessionId: string, prompt: string, options?: ISendOptions): Promise<IAssistantMessage | undefined>;
|
||||
abort(sessionId: string): Promise<void>;
|
||||
getMessages(sessionId: string): Promise<ISessionEvent[]>;
|
||||
|
||||
// Events (auto-forwarded over IPC by ProxyChannel)
|
||||
readonly onSessionEvent: Event<{ sessionId: string; event: ISessionEvent }>;
|
||||
readonly onSessionLifecycle: Event<ISessionLifecycleEvent>;
|
||||
|
||||
// Models
|
||||
listModels(): Promise<IModelInfo[]>;
|
||||
|
||||
// Authentication
|
||||
setGitHubToken(token: string): Promise<void>;
|
||||
}
|
||||
```
|
||||
|
||||
**How the layers connect (terminal pattern):**
|
||||
|
||||
```
|
||||
Renderer:
|
||||
registerMainProcessRemoteService(ICopilotSdkService, 'copilotSdk')
|
||||
→ ProxyChannel.toService(mainProcessService.getChannel('copilotSdk'))
|
||||
→ Workbench code just does: @ICopilotSdkService private sdk: ICopilotSdkService
|
||||
|
||||
Main Process (CopilotSdkMainService):
|
||||
1. On first call: spawns UtilityProcess with entryPoint 'vs/sessions/node/copilotSdkHost'
|
||||
2. Calls utilityProcess.connect() → gets MessagePort
|
||||
3. Wraps port as IPCClient → gets channel to utility process
|
||||
4. Registers that channel on the Electron IPC server as 'copilotSdk'
|
||||
→ All renderer calls transparently flow through to the utility process
|
||||
|
||||
Utility Process (copilotSdkHost.ts):
|
||||
1. Creates CopilotClient from @github/copilot-sdk
|
||||
2. Implements ICopilotSdkService (wrapping SDK methods + forwarding events)
|
||||
3. Wraps as IServerChannel via ProxyChannel.fromService(service)
|
||||
4. Registers on UtilityProcessMessagePortServer
|
||||
```
|
||||
|
||||
**Authentication:** The renderer gets the GitHub/Copilot token from `IAuthenticationService` and calls `sdk.setGitHubToken(token)`. This flows through to the utility process, which passes it to the SDK's `githubToken` option.
|
||||
|
||||
**Lifecycle:** The utility process is spawned lazily on first use. It is killed when the sessions window closes. If it crashes, the main process service restarts it (with a max restart count, matching the pty host pattern).
|
||||
|
||||
**Reference implementations to follow:**
|
||||
|
||||
| Pty host file | Our equivalent |
|
||||
|--------------|---------------|
|
||||
| `src/vs/platform/terminal/common/terminal.ts` (service interface) | `src/vs/sessions/common/copilotSdkService.ts` |
|
||||
| `src/vs/platform/terminal/node/ptyHostMain.ts` (utility process entry) | `src/vs/sessions/node/copilotSdkHost.ts` |
|
||||
| `src/vs/platform/terminal/electron-main/electronPtyHostStarter.ts` (spawner) | `src/vs/sessions/electron-main/copilotSdkMainService.ts` |
|
||||
| `registerMainProcessRemoteService(ILocalPtyService, ...)` | `registerMainProcessRemoteService(ICopilotSdkService, 'copilotSdk')` |
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: New Chat UI
|
||||
|
||||
**Goal:** Build a purpose-built chat UI under `src/vs/sessions/` that does NOT depend on `ChatWidget`, `ChatInputPart`, or `ChatModel` from `vs/workbench/contrib/chat/`.
|
||||
|
||||
**New files:**
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `src/vs/sessions/browser/chatRenderer.ts` | Streaming chat renderer — displays messages, tool calls, progress |
|
||||
| `src/vs/sessions/browser/parts/chatbar/chatInputEditor.ts` | New input editor widget (CodeEditorWidget-based) |
|
||||
| `src/vs/sessions/browser/parts/chatbar/chatToolCallView.ts` | Tool call status visualization |
|
||||
| `src/vs/sessions/browser/parts/chatbar/chatConfirmationView.ts` | User input request / tool approval UI |
|
||||
|
||||
**What to build:**
|
||||
1. **Input editor** — a `CodeEditorWidget` for the prompt, with send button, attachment support
|
||||
2. **Streaming renderer** — renders `assistant.message_delta` events into a scrolling view using `IMarkdownRendererService`
|
||||
3. **Tool call UI** — shows tool execution status from `tool.execution_start` / `tool.execution_complete` events
|
||||
4. **Confirmation UI** — handles `onUserInputRequest` / `ask_user` for tool approval prompts
|
||||
5. **Model picker** — uses `listModels()` to show available models
|
||||
|
||||
**What can be reused from existing VS Code codebase:**
|
||||
- `IMarkdownRendererService` / `MarkdownRenderer` for rendering responses
|
||||
- `CodeEditorWidget` for the input box
|
||||
- `IHoverService`, `IContextMenuService` for UI chrome
|
||||
- The existing sessions window layout (chat bar part, editor modal, sidebar, panel, etc.)
|
||||
- `IAuthenticationService` for token management
|
||||
|
||||
**What gets removed from the sessions window (eventually):**
|
||||
- `AgentSessionsChatWidget` wrapper (`src/vs/sessions/browser/widget/agentSessionsChatWidget.ts`)
|
||||
- `AgentSessionsChatTargetConfig` (`src/vs/sessions/browser/widget/agentSessionsChatTargetConfig.ts`)
|
||||
- `AgentSessionsChatWelcomePart` and related welcome view code
|
||||
- All imports of `ChatWidget`, `ChatInputPart`, `ChatModel`, `ChatService` in `src/vs/sessions/`
|
||||
- Dependency on `IChatSessionsService` for session creation/content
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: Tool Registration
|
||||
|
||||
**Goal:** Register VS Code's workspace tools (file edit, terminal, search, etc.) as SDK tools so the Copilot agent can operate on the workspace.
|
||||
|
||||
The SDK's `defineTool()` mechanism allows the host to register tools that the CLI agent can invoke. External tools like `rename_session`, `create_pull_request`, etc. are registered by the host — the CLI sends JSON-RPC requests back when the agent invokes them.
|
||||
|
||||
For VS Code, we'd register tools that bridge to existing VS Code services:
|
||||
|
||||
| Tool | Description | VS Code Service |
|
||||
|------|-------------|-----------------|
|
||||
| `read_file` | Read file contents | `IFileService` |
|
||||
| `write_file` | Write/create file | `IFileService` |
|
||||
| `list_directory` | List directory contents | `IFileService` |
|
||||
| `run_terminal_command` | Execute shell command | `ITerminalService` |
|
||||
| `search_files` | Search workspace | `ISearchService` |
|
||||
| `open_editor` | Open a file in the editor modal | `IEditorService` |
|
||||
| `rename_session` | Rename the current session | Session metadata |
|
||||
| `ask_user` | Ask for user confirmation | Built-in via SDK's `onUserInputRequest` |
|
||||
|
||||
Note: The CLI already has built-in tools (file read/write, bash, etc.) that work directly on the filesystem. We may not need to re-register all of these — the CLI handles them natively. We primarily need to register tools for UI actions (open editor, show notification, etc.) and tools that need to go through VS Code services.
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: Session List & Management
|
||||
|
||||
**Goal:** Replace `ChatSessionItemProvider` (from copilot-chat extension) with SDK-based session listing.
|
||||
|
||||
**Current flow:**
|
||||
```
|
||||
copilot-chat extension → registerChatSessionItemProvider(type, provider)
|
||||
→ ChatSessionsService._itemControllers
|
||||
→ getChatSessionItems() → sessions view
|
||||
```
|
||||
|
||||
**New flow:**
|
||||
```
|
||||
ICopilotSdkService.listSessions() → ISessionMetadata[]
|
||||
→ sessions view pane
|
||||
```
|
||||
|
||||
**SDK provides:**
|
||||
- `client.listSessions()` → `SessionMetadata[]` (sessionId, creation time, etc.)
|
||||
- `client.on('session.created' | 'session.updated' | 'session.deleted', handler)` — lifecycle events
|
||||
- `session.getMessages()` → full message history for a session
|
||||
|
||||
**What might be missing from the SDK** (compared to current `ChatSessionItem`):
|
||||
- File changes (insertions/deletions) — would need to be computed from git or the CLI's workspace state
|
||||
- Status badges (InProgress, NeedsInput, Failed) — can be derived from session events
|
||||
- Session timing (created, lastRequestStarted, lastRequestEnded) — partially available from SDK
|
||||
- Archive/read state — would need to be tracked locally (Storage)
|
||||
|
||||
**Files to modify:**
|
||||
- `src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts` — consume `ICopilotSdkService` instead of `IChatSessionsService`
|
||||
- `src/vs/sessions/contrib/sessions/browser/activeSessionService.ts` — track active session from SDK
|
||||
|
||||
---
|
||||
|
||||
### Phase 5: Remove Copilot-Chat Dependency
|
||||
|
||||
**Goal:** Once Phases 1-4 are complete, remove all copilot-chat extension dependencies from the sessions window.
|
||||
|
||||
**What gets removed from `src/vs/sessions/`:**
|
||||
- All imports from `vs/workbench/contrib/chat/` (ChatWidget, ChatInputPart, ChatModel, ChatService, etc.)
|
||||
- `AgentSessionsChatWidget` and related widget code
|
||||
- `IChatSessionsService` usage for session creation/content
|
||||
- `AgentSessionProviders` enum mapping to extension scheme strings (`copilotcli`, `copilot-cloud-agent`)
|
||||
- The `chatSessionsProvider` proposed API path (for sessions window use cases)
|
||||
|
||||
**What stays unchanged:**
|
||||
- Regular VS Code chat (sidebar, inline, editor) — still uses copilot-chat for everything
|
||||
- Local sessions — still managed by `ChatService`
|
||||
- Extension-contributed session types (Claude, etc.) — still use the extension API
|
||||
- The `chatSessions` extension point — still works for non-sessions-window consumers
|
||||
|
||||
**Files to modify:**
|
||||
- `src/vs/sessions/sessions.desktop.main.ts` — remove chat contrib imports, add SDK service registration
|
||||
- `src/vs/sessions/sessions.common.main.ts` — remove shared chat imports if any
|
||||
- `src/vs/sessions/browser/parts/chatbar/chatBarPart.ts` — switch from ChatWidget to new SDK-based UI
|
||||
- `src/vs/sessions/contrib/chat/browser/chat.contribution.ts` — remove or gut
|
||||
|
||||
---
|
||||
|
||||
## 5. Copilot SDK API Quick Reference
|
||||
|
||||
### Installation
|
||||
```
|
||||
npm install @github/copilot-sdk
|
||||
```
|
||||
|
||||
### CopilotClient Constructor Options
|
||||
- `cliPath?: string` — path to CLI executable (default: `copilot` from PATH)
|
||||
- `cliUrl?: string` — URL of existing CLI server (skips spawning)
|
||||
- `port?: number` — server port (default: random)
|
||||
- `useStdio?: boolean` — use stdio transport (default: true)
|
||||
- `autoStart?: boolean` — auto-start server (default: true)
|
||||
- `autoRestart?: boolean` — auto-restart on crash (default: true)
|
||||
- `githubToken?: string` — GitHub token for auth
|
||||
- `logLevel?: string` — log level (default: "info")
|
||||
|
||||
### Session Config
|
||||
- `sessionId?: string` — custom session ID
|
||||
- `model?: string` — model to use ("gpt-4.1", "claude-sonnet-4.5", etc.)
|
||||
- `reasoningEffort?: "low" | "medium" | "high" | "xhigh"`
|
||||
- `tools?: Tool[]` — custom tools
|
||||
- `systemMessage?: { content: string; mode?: "append" | "replace" }`
|
||||
- `streaming?: boolean` — enable streaming responses
|
||||
- `infiniteSessions?: { enabled: boolean; thresholds... }` — context compaction
|
||||
- `mcpServers?: { [name]: { type, url } }` — MCP server config
|
||||
- `customAgents?: [{ name, displayName, description, prompt }]`
|
||||
- `onUserInputRequest?: (request) => Promise<{ answer, wasFreeform }>`
|
||||
- `hooks?: { onPreToolUse, onPostToolUse, onUserPromptSubmitted, onSessionStart, onSessionEnd, onErrorOccurred }`
|
||||
- `provider?: ProviderConfig` — BYOK custom provider
|
||||
|
||||
### Event Types
|
||||
- `user.message` — user message added
|
||||
- `assistant.message` — complete assistant response
|
||||
- `assistant.message_delta` — streaming response chunk (has `deltaContent`)
|
||||
- `assistant.reasoning` — complete reasoning content
|
||||
- `assistant.reasoning_delta` — streaming reasoning chunk
|
||||
- `tool.execution_start` — tool execution started
|
||||
- `tool.execution_complete` — tool execution completed
|
||||
- `session.idle` — session finished processing
|
||||
- `session.compaction_start` — context compaction started
|
||||
- `session.compaction_complete` — context compaction finished
|
||||
|
||||
### Client Lifecycle Events
|
||||
- `session.created` — new session created
|
||||
- `session.deleted` — session deleted
|
||||
- `session.updated` — session updated (new messages)
|
||||
|
||||
---
|
||||
|
||||
## 6. Open Questions
|
||||
|
||||
1. **CLI binary packaging:** Does the `@github/copilot-sdk` npm package bundle the CLI, or do we need a separate `@github/copilot-cli` package? Need to check if the SDK can auto-download the CLI.
|
||||
|
||||
2. **SDK in utility process:** The SDK is designed for Node.js. Running in an Electron utility process should work, but needs validation — particularly around process lifecycle and cleanup.
|
||||
|
||||
3. **Event streaming over IPC:** Session events (especially `assistant.message_delta` at high frequency) need efficient IPC. MessagePort should handle this, but may need batching for performance.
|
||||
|
||||
4. **Authentication:** The SDK accepts `githubToken`. VS Code has `IAuthenticationService` with Copilot-specific token flows. Need to bridge these — get the token from the auth service and pass it to the SDK.
|
||||
|
||||
5. **MCP servers:** The sessions window currently has MCP server support via the extension. The SDK also supports MCP servers natively (`mcpServers` config). Need to decide if we use the SDK's MCP support or keep the existing VS Code MCP infrastructure.
|
||||
|
||||
6. **Existing tool infrastructure:** Copilot CLI has built-in tools (file I/O, bash, etc.) that operate directly on the filesystem. We may not need to re-register these as SDK tools. Need to understand which tools are built-in vs need to be provided by the host.
|
||||
|
||||
7. **SDK maturity:** The SDK is in Technical Preview (v0.1.23). API may change. Need to pin a version and track updates carefully.
|
||||
|
||||
8. **File changes / diff support:** The sessions view currently shows file insertions/deletions per session. The SDK doesn't directly provide this. We'd need to compute it from git or rely on the CLI's workspace state.
|
||||
|
||||
---
|
||||
|
||||
## 7. File Structure (New)
|
||||
|
||||
```
|
||||
src/vs/sessions/
|
||||
├── common/
|
||||
│ ├── contextkeys.ts (existing)
|
||||
│ └── copilotSdkService.ts ← NEW: ICopilotSdkService interface + types
|
||||
├── node/
|
||||
│ └── copilotSdkHost.ts ← NEW: Utility process entry point (SDK host)
|
||||
├── electron-main/
|
||||
│ ├── sessions.main.ts (existing)
|
||||
│ └── copilotSdkMainService.ts ← NEW: Main process proxy + utility process spawner
|
||||
├── electron-browser/
|
||||
│ ├── sessions.ts (existing)
|
||||
│ └── copilotSdkService.ts ← NEW: One-liner registerMainProcessRemoteService
|
||||
├── browser/
|
||||
│ ├── workbench.ts (existing)
|
||||
│ ├── menus.ts (existing)
|
||||
│ ├── layoutActions.ts (existing)
|
||||
│ ├── style.css (existing)
|
||||
│ ├── chatRenderer.ts ← NEW: Streaming chat renderer
|
||||
│ ├── parts/
|
||||
│ │ ├── chatbar/
|
||||
│ │ │ ├── chatBarPart.ts (existing, will be modified)
|
||||
│ │ │ ├── chatInputEditor.ts ← NEW: Input editor widget
|
||||
│ │ │ ├── chatToolCallView.ts ← NEW: Tool call visualization
|
||||
│ │ │ └── chatConfirmationView.ts ← NEW: User input / tool approval
|
||||
│ │ ├── titlebarPart.ts (existing)
|
||||
│ │ ├── editorModal.ts (existing)
|
||||
│ │ └── ... (existing)
|
||||
│ └── widget/ (TO BE REMOVED eventually)
|
||||
│ ├── agentSessionsChatWidget.ts ← REMOVE: replaced by SDK integration
|
||||
│ ├── agentSessionsChatTargetConfig.ts ← REMOVE: target concept simplified
|
||||
│ └── ...
|
||||
├── contrib/
|
||||
│ ├── sessions/browser/
|
||||
│ │ ├── sessionsViewPane.ts (existing, will consume ICopilotSdkService)
|
||||
│ │ └── activeSessionService.ts (existing, will track SDK sessions)
|
||||
│ ├── chat/browser/ (TO BE REMOVED eventually)
|
||||
│ └── ... (existing)
|
||||
└── sessions.desktop.main.ts (existing, will import copilotSdkService.ts)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 8. Worktree Strategy
|
||||
|
||||
### How Worktrees Work Today
|
||||
|
||||
The **copilot-chat extension** orchestrates worktrees via `ChatSessionWorktreeService`:
|
||||
1. Calls the **git extension API** (`repository.createWorktree({ branch })`) to create worktrees
|
||||
2. Stores metadata (`worktreePath`, `repositoryPath`, `baseCommit`, `branchName`) in extension global state
|
||||
3. Auto-commits changes after each request via `repository.commit()`
|
||||
4. Computes file changes via `repository.diffBetweenWithStats()`
|
||||
5. Applies changes back to the main repo via `repository.migrateChanges()` or `repository.apply(patch)`
|
||||
|
||||
Everything goes through the **VS Code git extension** (`extensions/git/`) — copilot-chat never runs `git` commands directly.
|
||||
|
||||
### With the SDK: Hybrid Approach
|
||||
|
||||
The CLI creates worktrees autonomously as part of its agent workflow. We discover the path and use the git extension for post-creation operations:
|
||||
|
||||
| Concern | Who handles it |
|
||||
|---------|---------------|
|
||||
| Worktree **creation** | Copilot CLI (built-in git tools, runs `git worktree add`) |
|
||||
| Worktree **path discovery** | Read `session.workspacePath` → parse `workspace.yaml` → extract `cwd` |
|
||||
| File **change stats** | Git extension API: `repository.diffBetweenWithStats()` |
|
||||
| **Apply changes** to main repo | Git extension API: `repository.migrateChanges()` or `repository.apply(patch)` |
|
||||
| Auto-**commit** | Git extension API: `repository.commit()` |
|
||||
| **Open terminal** in worktree | `ITerminalService.createTerminal({ cwd: worktreePath })` |
|
||||
| **Open in VS Code** | `IHostService.openWindow([{ folderUri: worktreeUri }])` |
|
||||
|
||||
### Extension Host
|
||||
|
||||
The sessions window **keeps its own extension host** — it already has one for the git extension, language services, terminal, etc. The SDK adoption does not remove the extension host. It only removes the dependency on the **copilot-chat extension** running in that extension host.
|
||||
|
||||
| Extension | Status | Used for |
|
||||
|-----------|--------|----------|
|
||||
| `vscode.git` (built-in) | **Kept** | `createWorktree`, `deleteWorktree`, `migrateChanges`, `diffBetween*`, `commit`, `apply`, repository state |
|
||||
| `github.vscode-pull-request-github` | **Kept** (if installed) | PR creation from worktree branches |
|
||||
| `github.copilot-chat` | **Removed** from sessions window dependency | No longer needed — SDK handles session lifecycle |
|
||||
| Other built-in extensions | **Kept** | Language services, terminal, file system providers |
|
||||
|
||||
---
|
||||
|
||||
## 8. Concrete Implementation Plan
|
||||
|
||||
This section is the **actionable build order**. Each step produces a compilable, testable checkpoint. Steps within a milestone can be developed in parallel; milestones are sequential.
|
||||
|
||||
---
|
||||
|
||||
### Milestone 1: SDK Client in a Utility Process (Foundation)
|
||||
|
||||
Everything else depends on this. The goal is: send a prompt, get a streaming response, see it logged in the dev console.
|
||||
|
||||
#### Step 1.1 — Add `@github/copilot-sdk` dependency
|
||||
|
||||
- Add `"@github/copilot-sdk": "^0.1.23"` to root `package.json`
|
||||
- Run `npm install`
|
||||
- Add ASAR unpack rule in `build/gulpfile.vscode.ts` for any native bits the SDK ships
|
||||
- Validate: `node -e "require('@github/copilot-sdk')"` succeeds
|
||||
|
||||
#### Step 1.2 — Define the service interface
|
||||
|
||||
Create `src/vs/sessions/common/copilotSdkService.ts`:
|
||||
- `ICopilotSdkService` interface (see Phase 1 in plan above)
|
||||
- All event and data types: `ISessionConfig`, `ISessionEvent`, `ISessionMetadata`, `ISendOptions`, `IModelInfo`, etc.
|
||||
- `createDecorator<ICopilotSdkService>('copilotSdkService')`
|
||||
- **No implementation** — just types. This is the contract between the utility process host and the workbench client.
|
||||
- **Compiles independently** — no dependencies beyond `vs/base` and `vs/platform`.
|
||||
|
||||
#### Step 1.3 — Build the utility process host
|
||||
|
||||
Create `src/vs/sessions/node/copilotSdkHost.ts`:
|
||||
- Utility process entry point (like `src/vs/platform/terminal/node/ptyHostMain.ts`)
|
||||
- Creates a `UtilityProcessMessagePortServer` to accept connections
|
||||
- Implements `ICopilotSdkService`:
|
||||
- Creates a `CopilotClient` from `@github/copilot-sdk` (for dev: omit `cliPath` so the SDK finds `copilot` on PATH)
|
||||
- Maps each SDK method to the service interface
|
||||
- Collects SDK `session.on()` callbacks and emits them as `Event<{ sessionId, event }>` on the service
|
||||
- Wraps the service as an `IServerChannel` via `ProxyChannel.fromService(service)` — this auto-handles all methods and events
|
||||
- Registers the channel on the server
|
||||
|
||||
Key: `ProxyChannel.fromService()` auto-detects all `onFoo: Event<T>` properties and buffers them for IPC. No manual event wiring needed.
|
||||
|
||||
#### Step 1.4 — Build the main process service (proxy layer)
|
||||
|
||||
Create `src/vs/sessions/electron-main/copilotSdkMainService.ts`:
|
||||
- Follows the `ElectronPtyHostStarter` / `PtyHostService` pattern
|
||||
- Spawns a `UtilityProcess` with entryPoint `'vs/sessions/node/copilotSdkHost'` (lazy — on first method call)
|
||||
- Calls `utilityProcess.connect()` → gets MessagePort → wraps as `MessagePortClient` → gets `IChannel`
|
||||
- Proxies that channel to the Electron IPC server via `server.registerChannel('copilotSdk', channel)`
|
||||
- Handles crash/restart: max 5 restarts, then give up
|
||||
- Handles lifecycle: kills utility process when sessions window closes
|
||||
|
||||
Wire into the main process app initialization:
|
||||
- In `src/vs/code/electron-main/app.ts` (or sessions-specific equivalent): instantiate `CopilotSdkMainService`, register the channel on `mainProcessElectronServer`
|
||||
|
||||
#### Step 1.5 — Register the renderer-side proxy
|
||||
|
||||
Create `src/vs/sessions/electron-browser/copilotSdkService.ts`:
|
||||
- **One line**: `registerMainProcessRemoteService(ICopilotSdkService, 'copilotSdk')`
|
||||
- This creates a `ProxyChannel.toService()` wrapper around `mainProcessService.getChannel('copilotSdk')`
|
||||
- Any workbench code can now inject `@ICopilotSdkService` via standard DI
|
||||
|
||||
Modify `src/vs/sessions/sessions.desktop.main.ts`:
|
||||
- Import the registration file
|
||||
|
||||
#### Step 1.6 — Smoke test: end-to-end message round-trip
|
||||
|
||||
Create a temporary test action (or use the dev console):
|
||||
1. `copilotSdkService.start()`
|
||||
2. `const sessionId = await copilotSdkService.createSession({ model: 'gpt-4.1', streaming: true })`
|
||||
3. Subscribe to `copilotSdkService.onSessionEvent` and log to console
|
||||
4. `await copilotSdkService.send(sessionId, 'What is 2+2?')`
|
||||
5. Verify: `assistant.message_delta` events arrive in the renderer console
|
||||
6. Verify: `session.idle` fires when done
|
||||
|
||||
**Checkpoint: The SDK is alive and talking to the renderer. No UI yet.**
|
||||
|
||||
---
|
||||
|
||||
### Milestone 2: Minimal Chat UI (See It Working)
|
||||
|
||||
Replace the current chat bar's content with a new, minimal chat UI powered by `ICopilotSdkService`. Goal: type a prompt, see a streaming response rendered in the chat bar.
|
||||
|
||||
#### Step 2.1 — New chat input editor
|
||||
|
||||
Create `src/vs/sessions/browser/parts/chatbar/chatInputEditor.ts`:
|
||||
- A `Disposable` class that creates a `CodeEditorWidget` (single-line mode, like the existing chat input)
|
||||
- Send button (icon button or keyboard shortcut: Enter to send, Shift+Enter for newline)
|
||||
- Exposes: `onDidSubmit: Event<string>` (fires with the prompt text)
|
||||
- Exposes: `focus()`, `clear()`, `getValue()`
|
||||
- CSS in `src/vs/sessions/browser/parts/chatbar/media/chatInputEditor.css`
|
||||
- **Standalone** — no dependencies on `ChatInputPart` or `ChatWidget`
|
||||
|
||||
#### Step 2.2 — Streaming message renderer
|
||||
|
||||
Create `src/vs/sessions/browser/chatRenderer.ts`:
|
||||
- A `Disposable` that manages a scrollable container
|
||||
- Accepts `ISessionEvent` stream and renders them:
|
||||
- `user.message` → right-aligned user bubble with the prompt text
|
||||
- `assistant.message_delta` → accumulates `deltaContent` into a growing markdown block, rendered via `IMarkdownRendererService`
|
||||
- `assistant.message` → finalizes the markdown block
|
||||
- `tool.execution_start` → shows a tool call indicator (icon + tool name + "running...")
|
||||
- `tool.execution_complete` → updates the indicator to "done"
|
||||
- `session.idle` → scroll to bottom, enable input
|
||||
- Auto-scroll behavior: stick to bottom while streaming, disengage if user scrolls up
|
||||
- **Standalone** — uses `IMarkdownRendererService`, `IHoverService`, standard DOM. No `ChatWidget` dependency.
|
||||
|
||||
#### Step 2.3 — Wire into the ChatBarPart
|
||||
|
||||
Modify `src/vs/sessions/browser/parts/chatBarPart.ts`:
|
||||
- Currently hosts a pane composite (which loads the chat view pane containing `AgentSessionsChatWidget`)
|
||||
- **New approach:** The ChatBarPart directly creates:
|
||||
1. A `StreamingChatRenderer` (from step 2.2) filling the main area
|
||||
2. A `ChatInputEditor` (from step 2.1) at the bottom
|
||||
3. On submit: calls `copilotSdkService.createSession()` (if no session), then `copilotSdkService.send()`
|
||||
4. Pipes `copilotSdkService.onSessionEvent` into the renderer
|
||||
- For now, hardcode model to `'gpt-4.1'` and default streaming config
|
||||
- **Keep the old code path behind a feature flag** so we can switch back if needed
|
||||
|
||||
#### Step 2.4 — Auth token plumbing
|
||||
|
||||
Create a workbench contribution in `src/vs/sessions/contrib/chat/browser/` (or reuse existing):
|
||||
- On activation, get the GitHub Copilot token from `IAuthenticationService`
|
||||
- Call `copilotSdkService.setGitHubToken(token)`
|
||||
- Listen for token refresh events and update
|
||||
- Without this, the SDK can't authenticate with the Copilot service
|
||||
|
||||
**Checkpoint: Type a prompt in the sessions window, see a streaming response. No session list, no tool calls, no polish — but it works.**
|
||||
|
||||
---
|
||||
|
||||
### Milestone 3: Session Management
|
||||
|
||||
#### Step 3.1 — Session lifecycle in the UI
|
||||
|
||||
Extend the ChatBarPart:
|
||||
- Track the active session ID
|
||||
- "New Session" action → destroys current session, creates a new one
|
||||
- Session resume on window reopen: store last session ID, call `resumeSession()` on startup
|
||||
- `abort()` button while response is streaming
|
||||
|
||||
#### Step 3.2 — Session list integration
|
||||
|
||||
Modify `src/vs/sessions/contrib/sessions/browser/sessionsViewPane.ts`:
|
||||
- Add an `ICopilotSdkService` dependency
|
||||
- Call `listSessions()` to populate the sessions list
|
||||
- Subscribe to `onSessionLifecycle` for real-time updates
|
||||
- On session click: call `resumeSession()` and load messages via `getMessages()`
|
||||
- Keep the existing `AgentSessionsControl` and `AgentSessionsFilter` UX — just swap the data source
|
||||
- **Dual mode**: support both SDK sessions and legacy `IChatSessionsService` sessions during transition
|
||||
|
||||
#### Step 3.3 — Active session service update
|
||||
|
||||
Modify `src/vs/sessions/contrib/sessions/browser/activeSessionService.ts`:
|
||||
- Track the active SDK session (session ID + metadata)
|
||||
- Expose the workspace/worktree path from the session's `workspacePath` property
|
||||
- Keep backward compatibility with the existing `IActiveSessionItem` shape
|
||||
|
||||
**Checkpoint: Sessions list shows SDK sessions. Can switch between sessions. Sessions persist across window restarts.**
|
||||
|
||||
---
|
||||
|
||||
### Milestone 4: Tool Calls & Agent Features
|
||||
|
||||
#### Step 4.1 — Tool call visualization
|
||||
|
||||
Extend the `StreamingChatRenderer`:
|
||||
- `tool.execution_start` → expandable panel showing tool name, arguments
|
||||
- `tool.execution_complete` → show result, duration
|
||||
- Special rendering for common tools: file edits (show diff), terminal commands (show output), etc.
|
||||
|
||||
#### Step 4.2 — User input requests
|
||||
|
||||
Handle the SDK's `onUserInputRequest` callback:
|
||||
- When the agent calls `ask_user`, the utility process forwards the request over IPC
|
||||
- The workbench shows a modal or inline prompt (question + optional choices)
|
||||
- User response is sent back over IPC → SDK → CLI
|
||||
- This is critical for tool approval flows (e.g., "Allow file edit?")
|
||||
|
||||
#### Step 4.3 — Session hooks (tool approval)
|
||||
|
||||
Register `onPreToolUse` hook in the SDK:
|
||||
- Show a "Allow tool X?" UI before dangerous operations
|
||||
- Auto-approve safe operations (read_file, search)
|
||||
- User preference: "always allow", "ask each time", "deny"
|
||||
|
||||
#### Step 4.4 — Custom external tools
|
||||
|
||||
Register VS Code-specific tools via `defineTool()`:
|
||||
- `open_editor` → opens a file in the editor modal (`IEditorService`)
|
||||
- `rename_session` → updates the session title in the sessions list
|
||||
- `show_notification` → shows a VS Code notification
|
||||
- These are tools the CLI doesn't have built-in — they need VS Code UI integration
|
||||
|
||||
#### Step 4.5 — Model picker
|
||||
|
||||
Add a model picker dropdown to the ChatInputEditor:
|
||||
- On mount: call `listModels()` to get available models
|
||||
- Show as a dropdown/quick pick
|
||||
- Pass selected model to `createSession({ model: ... })`
|
||||
|
||||
**Checkpoint: Full agent experience — tool calls visible, confirmations working, model selection, custom VS Code tools.**
|
||||
|
||||
---
|
||||
|
||||
### Milestone 5: Polish & Parity
|
||||
|
||||
#### Step 5.1 — Welcome view
|
||||
|
||||
Build a welcome/empty state for the chat bar:
|
||||
- Shown when no session is active
|
||||
- Quick action buttons: "Start a new session", model picker
|
||||
- Branding / mascot (if applicable)
|
||||
|
||||
#### Step 5.2 — File changes view
|
||||
|
||||
The auxiliary bar's changes view currently shows file diffs from the copilot-chat extension:
|
||||
- Compute file changes from git (diff between session branch and base)
|
||||
- Or use the CLI's workspace state if available
|
||||
- Wire into the existing `ChangesView` component
|
||||
|
||||
#### Step 5.3 — Keyboard shortcuts & accessibility
|
||||
|
||||
- Enter to send, Shift+Enter for newline
|
||||
- Ctrl+C / Cmd+C to abort
|
||||
- Screen reader announcements for streaming responses
|
||||
- Focus management between input and message list
|
||||
|
||||
#### Step 5.4 — Remove old code paths
|
||||
|
||||
Once the SDK path is stable:
|
||||
- Remove `src/vs/sessions/browser/widget/` (AgentSessionsChatWidget, etc.)
|
||||
- Remove `src/vs/sessions/contrib/chat/browser/` imports of ChatWidget/ChatService
|
||||
- Remove copilot-chat extension dependency from sessions window entirely
|
||||
- Clean up `sessions.desktop.main.ts` — remove unused chat imports
|
||||
|
||||
**Checkpoint: Feature parity with the current sessions window, fully powered by the Copilot SDK. No copilot-chat extension dependency.**
|
||||
|
||||
---
|
||||
|
||||
### Summary: Build Order at a Glance
|
||||
|
||||
```
|
||||
M1: Foundation M2: See it work M3: Sessions M4: Agent M5: Polish
|
||||
───────────────────── ────────────────── ──────────────── ────────────── ──────────────
|
||||
1.1 npm dependency 2.1 Input editor 3.1 Lifecycle 4.1 Tool viz 5.1 Welcome
|
||||
1.2 Service interface 2.2 Chat renderer 3.2 Sessions list 4.2 User input 5.2 Changes
|
||||
1.3 Utility proc host 2.3 Wire ChatBar 3.3 Active svc 4.3 Tool hooks 5.3 A11y
|
||||
1.4 Main proc starter 2.4 Auth token 4.4 Custom tools 5.4 Remove old
|
||||
1.5 Workbench client 4.5 Model picker
|
||||
1.6 Smoke test
|
||||
```
|
||||
|
||||
Each milestone produces a working, demoable checkpoint. M1+M2 gets us to "type and see a response" — the core wow moment. M3 adds persistence. M4 adds agent power. M5 reaches parity and removes the old dependency.
|
||||
|
||||
---
|
||||
|
||||
## Revision History
|
||||
|
||||
| Date | Change |
|
||||
|------|--------|
|
||||
| 2026-02-13 | Initial plan created. Covers SDK adoption, CLI bundling, utility process hosting, new chat UI, session list migration, and copilot-chat dependency removal. Based on Copilot SDK official documentation. |
|
||||
| 2026-02-13 | Added concrete implementation plan (Section 9) with 5 milestones and detailed step-by-step build order. |
|
||||
| 2026-02-13 | Refined architecture to terminal (pty host) pattern: `registerMainProcessRemoteService` + main process proxy + utility process. Renderer-side code is one line DI. Added worktree strategy (Section 8): CLI creates worktrees, git extension handles post-creation operations. Extension host is kept for git, not removed. |
|
||||
307
src/vs/sessions/browser/copilotSdkDebugPanel.ts
Normal file
307
src/vs/sessions/browser/copilotSdkDebugPanel.ts
Normal file
@@ -0,0 +1,307 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
/**
|
||||
* Temporary RPC debug UI for the Copilot SDK integration.
|
||||
* Shows the raw event stream and provides helper buttons for common RPC calls.
|
||||
* Delete this entire file to remove the debug panel.
|
||||
*/
|
||||
|
||||
import './media/copilotSdkDebugPanel.css';
|
||||
import * as dom from '../../base/browser/dom.js';
|
||||
import { Disposable, DisposableStore } from '../../base/common/lifecycle.js';
|
||||
import { ICopilotSdkService } from '../../platform/copilotSdk/common/copilotSdkService.js';
|
||||
import { IClipboardService } from '../../platform/clipboard/common/clipboardService.js';
|
||||
|
||||
const $ = dom.$;
|
||||
|
||||
export class CopilotSdkDebugPanel extends Disposable {
|
||||
|
||||
readonly element: HTMLElement;
|
||||
|
||||
private readonly _rpcLogContainer: HTMLElement;
|
||||
private readonly _processLogContainer: HTMLElement;
|
||||
private readonly _inputArea: HTMLTextAreaElement;
|
||||
private readonly _statusBar: HTMLElement;
|
||||
private readonly _cwdInput: HTMLInputElement;
|
||||
private readonly _modelSelect: HTMLSelectElement;
|
||||
private _sessionId: string | undefined;
|
||||
private _logCount = 0;
|
||||
private readonly _logLines: string[] = [];
|
||||
private readonly _processLines: string[] = [];
|
||||
private _activeTab: 'rpc' | 'process' = 'rpc';
|
||||
|
||||
private readonly _eventDisposables = this._register(new DisposableStore());
|
||||
|
||||
constructor(
|
||||
container: HTMLElement,
|
||||
@ICopilotSdkService private readonly _sdk: ICopilotSdkService,
|
||||
@IClipboardService private readonly _clipboardService: IClipboardService,
|
||||
) {
|
||||
super();
|
||||
|
||||
this.element = dom.append(container, $('.copilot-sdk-debug-panel'));
|
||||
|
||||
// Header
|
||||
const header = dom.append(this.element, $('.debug-panel-header'));
|
||||
dom.append(header, $('span')).textContent = 'Copilot SDK RPC Debug';
|
||||
const clearBtn = dom.append(header, $('button')) as HTMLButtonElement;
|
||||
clearBtn.textContent = 'Clear';
|
||||
clearBtn.style.cssText = 'margin-left:auto;font-size:11px;padding:2px 8px;background:var(--vscode-button-secondaryBackground);color:var(--vscode-button-secondaryForeground);border:none;border-radius:3px;cursor:pointer;';
|
||||
this._register(dom.addDisposableListener(clearBtn, 'click', () => {
|
||||
if (this._activeTab === 'rpc') {
|
||||
dom.clearNode(this._rpcLogContainer); this._logCount = 0; this._logLines.length = 0;
|
||||
} else {
|
||||
dom.clearNode(this._processLogContainer); this._processLines.length = 0;
|
||||
}
|
||||
}));
|
||||
|
||||
const copyBtn = dom.append(header, $('button')) as HTMLButtonElement;
|
||||
copyBtn.textContent = 'Copy All';
|
||||
copyBtn.style.cssText = 'margin-left:4px;font-size:11px;padding:2px 8px;background:var(--vscode-button-secondaryBackground);color:var(--vscode-button-secondaryForeground);border:none;border-radius:3px;cursor:pointer;';
|
||||
this._register(dom.addDisposableListener(copyBtn, 'click', () => {
|
||||
const lines = this._activeTab === 'rpc' ? this._logLines : this._processLines;
|
||||
this._clipboardService.writeText(lines.join('\n'));
|
||||
copyBtn.textContent = 'Copied!';
|
||||
setTimeout(() => { copyBtn.textContent = 'Copy All'; }, 1500);
|
||||
}));
|
||||
|
||||
// Status
|
||||
this._statusBar = dom.append(this.element, $('.debug-panel-status'));
|
||||
this._statusBar.textContent = 'Not connected';
|
||||
|
||||
// Config row: model + cwd
|
||||
const configRow = dom.append(this.element, $('.debug-panel-model-row'));
|
||||
dom.append(configRow, $('label')).textContent = 'Model:';
|
||||
this._modelSelect = dom.append(configRow, $('select.debug-panel-model-select')) as HTMLSelectElement;
|
||||
|
||||
const cwdRow = dom.append(this.element, $('.debug-panel-model-row'));
|
||||
dom.append(cwdRow, $('label')).textContent = 'CWD:';
|
||||
this._cwdInput = dom.append(cwdRow, $('input.debug-panel-model-select')) as HTMLInputElement;
|
||||
this._cwdInput.type = 'text';
|
||||
this._cwdInput.placeholder = '/path/to/project';
|
||||
this._cwdInput.value = '/tmp';
|
||||
|
||||
// Helper buttons
|
||||
const helpers = dom.append(this.element, $('.debug-panel-helpers'));
|
||||
// allow-any-unicode-next-line
|
||||
const btns: Array<{ label: string; fn: () => void }> = [
|
||||
{ label: '> Start', fn: () => this._rpc('start') },
|
||||
{ label: 'List Models', fn: () => this._rpc('listModels') },
|
||||
{ label: 'List Sessions', fn: () => this._rpc('listSessions') },
|
||||
{ label: '+ Create Session', fn: () => this._rpc('createSession') },
|
||||
{ label: 'Send Message', fn: () => this._rpc('send') },
|
||||
{ label: 'Abort', fn: () => this._rpc('abort') },
|
||||
{ label: 'Destroy Session', fn: () => this._rpc('destroySession') },
|
||||
{ label: 'Stop', fn: () => this._rpc('stop') },
|
||||
];
|
||||
for (const { label, fn } of btns) {
|
||||
const btn = dom.append(helpers, $('button.debug-helper-btn')) as HTMLButtonElement;
|
||||
btn.textContent = label;
|
||||
this._register(dom.addDisposableListener(btn, 'click', fn));
|
||||
}
|
||||
|
||||
// Tab bar
|
||||
const tabBar = dom.append(this.element, $('.debug-panel-tabs'));
|
||||
const rpcTab = dom.append(tabBar, $('button.debug-tab.debug-tab-active')) as HTMLButtonElement;
|
||||
rpcTab.textContent = 'RPC Log';
|
||||
const processTab = dom.append(tabBar, $('button.debug-tab')) as HTMLButtonElement;
|
||||
processTab.textContent = 'Process Output';
|
||||
const switchTab = (tab: 'rpc' | 'process') => {
|
||||
this._activeTab = tab;
|
||||
rpcTab.classList.toggle('debug-tab-active', tab === 'rpc');
|
||||
processTab.classList.toggle('debug-tab-active', tab === 'process');
|
||||
this._rpcLogContainer.style.display = tab === 'rpc' ? '' : 'none';
|
||||
this._processLogContainer.style.display = tab === 'process' ? '' : 'none';
|
||||
};
|
||||
this._register(dom.addDisposableListener(rpcTab, 'click', () => switchTab('rpc')));
|
||||
this._register(dom.addDisposableListener(processTab, 'click', () => switchTab('process')));
|
||||
|
||||
// RPC log stream
|
||||
this._rpcLogContainer = dom.append(this.element, $('.debug-panel-messages'));
|
||||
|
||||
// Process output log
|
||||
this._processLogContainer = dom.append(this.element, $('.debug-panel-messages'));
|
||||
this._processLogContainer.style.display = 'none';
|
||||
|
||||
// Free-form input for sending prompts
|
||||
const inputRow = dom.append(this.element, $('.debug-panel-input-row'));
|
||||
this._inputArea = dom.append(inputRow, $('textarea.debug-panel-input')) as HTMLTextAreaElement;
|
||||
this._inputArea.placeholder = 'Message prompt (used by Send Message)...';
|
||||
this._inputArea.rows = 2;
|
||||
|
||||
// Initialize: subscribe to events immediately
|
||||
this._subscribeToEvents();
|
||||
this._initialize();
|
||||
}
|
||||
|
||||
private async _initialize(): Promise<void> {
|
||||
try {
|
||||
this._setStatus('Starting...');
|
||||
this._logRpc('→', 'start', '');
|
||||
await this._sdk.start();
|
||||
this._logRpc('←', 'start', 'OK');
|
||||
|
||||
this._logRpc('→', 'listModels', '');
|
||||
const models = await this._sdk.listModels();
|
||||
this._logRpc('←', 'listModels', `${models.length} models`);
|
||||
|
||||
dom.clearNode(this._modelSelect);
|
||||
for (const m of models) {
|
||||
const opt = document.createElement('option');
|
||||
opt.value = m.id;
|
||||
opt.textContent = m.name ?? m.id;
|
||||
this._modelSelect.appendChild(opt);
|
||||
}
|
||||
const def = models.find(m => m.id === 'gpt-4.1') ?? models[0];
|
||||
if (def) { this._modelSelect.value = def.id; }
|
||||
|
||||
this._setStatus('Ready');
|
||||
} catch (err) {
|
||||
this._logRpc('X', 'init', String(err));
|
||||
this._setStatus('Error');
|
||||
}
|
||||
}
|
||||
|
||||
private _subscribeToEvents(): void {
|
||||
this._eventDisposables.clear();
|
||||
this._eventDisposables.add(this._sdk.onSessionEvent(event => {
|
||||
const data = JSON.stringify(event.data ?? {});
|
||||
const truncated = data.length > 300 ? data.substring(0, 300) + '…' : data;
|
||||
this._logRpc('!', `event:${event.type}`, truncated, event.sessionId.substring(0, 8));
|
||||
}));
|
||||
this._eventDisposables.add(this._sdk.onSessionLifecycle(event => {
|
||||
this._logRpc('!', `lifecycle:${event.type}`, '', event.sessionId.substring(0, 8));
|
||||
}));
|
||||
this._eventDisposables.add(this._sdk.onProcessOutput(output => {
|
||||
this._logProcess(output.stream, output.data);
|
||||
}));
|
||||
}
|
||||
|
||||
private async _rpc(method: string): Promise<void> {
|
||||
try {
|
||||
switch (method) {
|
||||
case 'start': {
|
||||
this._logRpc('→', 'start', '');
|
||||
await this._sdk.start();
|
||||
this._logRpc('←', 'start', 'OK');
|
||||
break;
|
||||
}
|
||||
case 'stop': {
|
||||
this._logRpc('→', 'stop', '');
|
||||
await this._sdk.stop();
|
||||
this._sessionId = undefined;
|
||||
this._logRpc('←', 'stop', 'OK');
|
||||
break;
|
||||
}
|
||||
case 'listModels': {
|
||||
this._logRpc('→', 'listModels', '');
|
||||
const models = await this._sdk.listModels();
|
||||
this._logRpc('←', 'listModels', JSON.stringify(models.map(m => m.id)));
|
||||
break;
|
||||
}
|
||||
case 'listSessions': {
|
||||
this._logRpc('→', 'listSessions', '');
|
||||
const sessions = await this._sdk.listSessions();
|
||||
this._logRpc('←', 'listSessions', JSON.stringify(sessions));
|
||||
break;
|
||||
}
|
||||
case 'createSession': {
|
||||
const model = this._modelSelect.value;
|
||||
const cwd = this._cwdInput.value.trim() || undefined;
|
||||
this._logRpc('→', 'createSession', JSON.stringify({ model, streaming: true, workingDirectory: cwd }));
|
||||
this._sessionId = await this._sdk.createSession({ model, streaming: true, workingDirectory: cwd });
|
||||
this._logRpc('←', 'createSession', this._sessionId);
|
||||
this._setStatus(`Session: ${this._sessionId.substring(0, 8)}...`);
|
||||
break;
|
||||
}
|
||||
case 'send': {
|
||||
if (!this._sessionId) {
|
||||
this._logRpc('X', 'send', 'No session -- create one first');
|
||||
return;
|
||||
}
|
||||
const prompt = this._inputArea.value.trim() || 'What is 2+2? Answer in one word.';
|
||||
this._logRpc('→', 'send', JSON.stringify({ sessionId: this._sessionId.substring(0, 8), prompt: prompt.substring(0, 100) }));
|
||||
this._setStatus('Sending...');
|
||||
await this._sdk.send(this._sessionId, prompt);
|
||||
this._logRpc('←', 'send', 'queued');
|
||||
break;
|
||||
}
|
||||
case 'abort': {
|
||||
if (!this._sessionId) { this._logRpc('X', 'abort', 'No session'); return; }
|
||||
this._logRpc('→', 'abort', this._sessionId.substring(0, 8));
|
||||
await this._sdk.abort(this._sessionId);
|
||||
this._logRpc('←', 'abort', 'OK');
|
||||
break;
|
||||
}
|
||||
case 'destroySession': {
|
||||
if (!this._sessionId) { this._logRpc('X', 'destroySession', 'No session'); return; }
|
||||
this._logRpc('→', 'destroySession', this._sessionId.substring(0, 8));
|
||||
await this._sdk.destroySession(this._sessionId);
|
||||
this._logRpc('←', 'destroySession', 'OK');
|
||||
this._sessionId = undefined;
|
||||
this._setStatus('Ready');
|
||||
break;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
this._logRpc('X', method, String(err instanceof Error ? err.message : err));
|
||||
}
|
||||
}
|
||||
|
||||
private _logRpc(direction: string, method: string, detail: string, tag?: string): void {
|
||||
this._logCount++;
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
const line = `${String(this._logCount).padStart(3, '0')} ${direction} ${tag ? `[${tag}] ` : ''}${method} ${detail} ${timestamp}`;
|
||||
this._logLines.push(line);
|
||||
|
||||
const el = dom.append(this._rpcLogContainer, $('.debug-rpc-entry'));
|
||||
|
||||
const num = dom.append(el, $('span.debug-rpc-num'));
|
||||
num.textContent = String(this._logCount).padStart(3, '0');
|
||||
|
||||
const dir = dom.append(el, $('span.debug-rpc-dir'));
|
||||
dir.textContent = direction;
|
||||
|
||||
if (tag) {
|
||||
const tagEl = dom.append(el, $('span.debug-rpc-tag'));
|
||||
tagEl.textContent = tag;
|
||||
}
|
||||
|
||||
const meth = dom.append(el, $('span.debug-rpc-method'));
|
||||
meth.textContent = method;
|
||||
|
||||
if (detail) {
|
||||
const det = dom.append(el, $('span.debug-rpc-detail'));
|
||||
det.textContent = detail;
|
||||
}
|
||||
|
||||
const time = dom.append(el, $('span.debug-rpc-time'));
|
||||
time.textContent = new Date().toLocaleTimeString();
|
||||
|
||||
this._rpcLogContainer.scrollTop = this._rpcLogContainer.scrollHeight;
|
||||
}
|
||||
|
||||
private _logProcess(stream: string, data: string): void {
|
||||
const timestamp = new Date().toLocaleTimeString();
|
||||
this._processLines.push(`[${timestamp}] [${stream}] ${data}`);
|
||||
|
||||
const el = dom.append(this._processLogContainer, $('.debug-rpc-entry'));
|
||||
const time = dom.append(el, $('span.debug-rpc-time'));
|
||||
time.textContent = timestamp;
|
||||
const streamTag = dom.append(el, $('span.debug-rpc-tag'));
|
||||
streamTag.textContent = stream;
|
||||
const content = dom.append(el, $('span.debug-rpc-detail'));
|
||||
content.textContent = data;
|
||||
content.style.whiteSpace = 'pre-wrap';
|
||||
content.style.flex = '1';
|
||||
|
||||
this._processLogContainer.scrollTop = this._processLogContainer.scrollHeight;
|
||||
}
|
||||
|
||||
private _setStatus(text: string): void {
|
||||
this._statusBar.textContent = text;
|
||||
}
|
||||
}
|
||||
190
src/vs/sessions/browser/media/copilotSdkDebugPanel.css
Normal file
190
src/vs/sessions/browser/media/copilotSdkDebugPanel.css
Normal file
@@ -0,0 +1,190 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
.copilot-sdk-debug-panel {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
font-family: var(--monaco-monospace-font, monospace);
|
||||
font-size: 12px;
|
||||
color: var(--vscode-foreground);
|
||||
background: var(--vscode-editor-background);
|
||||
}
|
||||
|
||||
.debug-panel-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 6px 10px;
|
||||
font-weight: 600;
|
||||
font-size: 13px;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
background: var(--vscode-sideBarSectionHeader-background);
|
||||
}
|
||||
|
||||
.debug-panel-status {
|
||||
padding: 3px 10px;
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.debug-panel-model-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 4px 10px;
|
||||
}
|
||||
|
||||
.debug-panel-model-row label {
|
||||
font-size: 11px;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
min-width: 40px;
|
||||
}
|
||||
|
||||
.debug-panel-model-select {
|
||||
flex: 1;
|
||||
padding: 2px 4px;
|
||||
font-size: 12px;
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* Helper buttons row */
|
||||
.debug-panel-helpers {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
padding: 6px 10px;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
/* Tab bar */
|
||||
.debug-panel-tabs {
|
||||
display: flex;
|
||||
border-bottom: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.debug-tab {
|
||||
padding: 4px 12px;
|
||||
font-size: 11px;
|
||||
font-family: var(--monaco-monospace-font);
|
||||
background: transparent;
|
||||
color: var(--vscode-descriptionForeground);
|
||||
border: none;
|
||||
border-bottom: 2px solid transparent;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.debug-tab:hover {
|
||||
color: var(--vscode-foreground);
|
||||
}
|
||||
|
||||
.debug-tab-active {
|
||||
color: var(--vscode-foreground);
|
||||
border-bottom-color: var(--vscode-focusBorder);
|
||||
}
|
||||
|
||||
.debug-helper-btn {
|
||||
padding: 3px 8px;
|
||||
font-size: 11px;
|
||||
font-family: var(--monaco-monospace-font);
|
||||
background: var(--vscode-button-secondaryBackground);
|
||||
color: var(--vscode-button-secondaryForeground);
|
||||
border: none;
|
||||
border-radius: 3px;
|
||||
cursor: pointer;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.debug-helper-btn:hover {
|
||||
background: var(--vscode-button-secondaryHoverBackground);
|
||||
}
|
||||
|
||||
/* RPC log stream */
|
||||
.debug-panel-messages {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 4px 0;
|
||||
font-family: var(--monaco-monospace-font, monospace);
|
||||
font-size: 11px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.debug-rpc-entry {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 1px 10px;
|
||||
align-items: baseline;
|
||||
}
|
||||
|
||||
.debug-rpc-entry:hover {
|
||||
background: var(--vscode-list-hoverBackground);
|
||||
}
|
||||
|
||||
.debug-rpc-num {
|
||||
color: var(--vscode-editorLineNumber-foreground);
|
||||
min-width: 24px;
|
||||
}
|
||||
|
||||
.debug-rpc-dir {
|
||||
min-width: 16px;
|
||||
}
|
||||
|
||||
.debug-rpc-tag {
|
||||
color: var(--vscode-editorLineNumber-foreground);
|
||||
font-size: 10px;
|
||||
padding: 0 3px;
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.debug-rpc-method {
|
||||
color: var(--vscode-symbolIcon-methodForeground, #b180d7);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.debug-rpc-detail {
|
||||
color: var(--vscode-descriptionForeground);
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.debug-rpc-time {
|
||||
color: var(--vscode-editorLineNumber-foreground);
|
||||
font-size: 10px;
|
||||
margin-left: auto;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* Input area */
|
||||
.debug-panel-input-row {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
padding: 6px 10px;
|
||||
border-top: 1px solid var(--vscode-panel-border);
|
||||
}
|
||||
|
||||
.debug-panel-input {
|
||||
flex: 1;
|
||||
padding: 4px 6px;
|
||||
font-size: 12px;
|
||||
font-family: var(--monaco-monospace-font, monospace);
|
||||
background: var(--vscode-input-background);
|
||||
color: var(--vscode-input-foreground);
|
||||
border: 1px solid var(--vscode-input-border);
|
||||
border-radius: 3px;
|
||||
resize: none;
|
||||
}
|
||||
|
||||
.debug-panel-input:focus {
|
||||
outline: none;
|
||||
border-color: var(--vscode-focusBorder);
|
||||
}
|
||||
@@ -4,11 +4,15 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Codicon } from '../../../../base/common/codicons.js';
|
||||
import * as dom from '../../../../base/browser/dom.js';
|
||||
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
|
||||
import { localize2 } from '../../../../nls.js';
|
||||
import { Action2, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js';
|
||||
import { IHostService } from '../../../../workbench/services/host/browser/host.js';
|
||||
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../../workbench/common/contributions.js';
|
||||
import { IWorkbenchLayoutService } from '../../../../workbench/services/layout/browser/layoutService.js';
|
||||
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { CopilotSdkDebugPanel } from '../../../browser/copilotSdkDebugPanel.js';
|
||||
import { AgentSessionProviders } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessions.js';
|
||||
import { isAgentSession } from '../../../../workbench/contrib/chat/browser/agentSessions/agentSessionsModel.js';
|
||||
import { IActiveSessionService } from '../../sessions/browser/activeSessionService.js';
|
||||
@@ -110,3 +114,56 @@ registerWorkbenchContribution2(RunScriptContribution.ID, RunScriptContribution,
|
||||
|
||||
// register services
|
||||
registerSingleton(IPromptsService, AgenticPromptsService, InstantiationType.Delayed);
|
||||
|
||||
// --- Temporary debug panel for Copilot SDK (delete this block + copilotSdkDebugPanel.ts to remove) ---
|
||||
let activeDebugBackdrop: HTMLElement | undefined;
|
||||
registerAction2(class CopilotSdkDebugPanelAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'copilotSdk.openDebugPanel',
|
||||
title: localize2('copilotSdkDebugPanel', 'Copilot SDK: Open Debug Panel'),
|
||||
f1: true,
|
||||
icon: Codicon.beaker,
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const layoutService = accessor.get(IWorkbenchLayoutService);
|
||||
const instantiationService = accessor.get(IInstantiationService);
|
||||
|
||||
const container = layoutService.mainContainer;
|
||||
const targetWindow = dom.getWindow(container);
|
||||
|
||||
// Toggle off if already open
|
||||
if (activeDebugBackdrop) {
|
||||
activeDebugBackdrop.remove();
|
||||
activeDebugBackdrop = undefined;
|
||||
return;
|
||||
}
|
||||
|
||||
// Centered modal with backdrop
|
||||
const backdrop = dom.$('.copilot-sdk-debug-backdrop');
|
||||
activeDebugBackdrop = backdrop;
|
||||
backdrop.style.cssText = 'position:absolute;inset:0;z-index:1000;display:flex;align-items:center;justify-content:center;background:rgba(0,0,0,0.5);';
|
||||
container.appendChild(backdrop);
|
||||
|
||||
const modal = dom.$('div');
|
||||
modal.style.cssText = 'width:560px;height:80%;max-height:700px;border-radius:8px;overflow:hidden;box-shadow:0 8px 32px rgba(0,0,0,0.4);';
|
||||
backdrop.appendChild(modal);
|
||||
|
||||
const panel = instantiationService.createInstance(CopilotSdkDebugPanel, modal);
|
||||
|
||||
const close = () => {
|
||||
panel.dispose();
|
||||
backdrop.remove();
|
||||
activeDebugBackdrop = undefined;
|
||||
targetWindow.document.removeEventListener('keydown', onKeyDown);
|
||||
};
|
||||
const onKeyDown = (e: KeyboardEvent) => {
|
||||
if (e.key === 'Escape') { close(); }
|
||||
};
|
||||
backdrop.addEventListener('click', (e) => {
|
||||
if (e.target === backdrop) { close(); }
|
||||
});
|
||||
targetWindow.document.addEventListener('keydown', onKeyDown);
|
||||
}
|
||||
});
|
||||
|
||||
12
src/vs/sessions/electron-browser/copilotSdkService.ts
Normal file
12
src/vs/sessions/electron-browser/copilotSdkService.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
/*---------------------------------------------------------------------------------------------
|
||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { registerMainProcessRemoteService } from '../../platform/ipc/electron-browser/services.js';
|
||||
import { ICopilotSdkService, CopilotSdkChannel } from '../../platform/copilotSdk/common/copilotSdkService.js';
|
||||
|
||||
// Register ICopilotSdkService as a proxy to the main process channel.
|
||||
// The main process forwards calls to the Copilot SDK utility process.
|
||||
// Any workbench code can now inject @ICopilotSdkService via standard DI.
|
||||
registerMainProcessRemoteService(ICopilotSdkService, CopilotSdkChannel);
|
||||
@@ -193,6 +193,9 @@ import './contrib/sessions/browser/sessions.contribution.js';
|
||||
import './contrib/changesView/browser/changesView.contribution.js';
|
||||
import './contrib/configuration/browser/configuration.contribution.js';
|
||||
|
||||
// Copilot SDK service (registers ICopilotSdkService as main process remote service)
|
||||
import './electron-browser/copilotSdkService.js';
|
||||
|
||||
//#endregion
|
||||
|
||||
export { main } from './electron-browser/sessions.main.js';
|
||||
|
||||
Reference in New Issue
Block a user