checkpoint: introduce copilotSDK as utility process

This commit is contained in:
Josh Spicer
2026-02-13 16:14:19 -08:00
parent 10c264593e
commit 81c0e1abc2
12 changed files with 2171 additions and 0 deletions

146
package-lock.json generated
View File

@@ -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",

View File

@@ -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",

View File

@@ -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);

View 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

View 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();
}
}

View 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();
});
}

View 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. |

View 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;
}
}

View 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);
}

View File

@@ -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);
}
});

View 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);

View File

@@ -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';