Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 45 additions & 0 deletions src/vs/workbench/api/browser/mainThreadChatAgents2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { IUriIdentityService } from '../../../platform/uriIdentity/common/uriIde
import { IChatWidgetService } from '../../contrib/chat/browser/chat.js';
import { AddDynamicVariableAction, IAddDynamicVariableContext } from '../../contrib/chat/browser/contrib/chatDynamicVariables.js';
import { IChatAgentHistoryEntry, IChatAgentImplementation, IChatAgentRequest, IChatAgentService } from '../../contrib/chat/common/chatAgents.js';
import { ICustomAgentQueryOptions, IPromptsService } from '../../contrib/chat/common/promptSyntax/service/promptsService.js';
import { IChatEditingService, IChatRelatedFileProviderMetadata } from '../../contrib/chat/common/chatEditingService.js';
import { IChatModel } from '../../contrib/chat/common/chatModel.js';
import { ChatRequestAgentPart } from '../../contrib/chat/common/chatParserTypes.js';
Expand Down Expand Up @@ -96,6 +97,9 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA

private readonly _chatRelatedFilesProviders = this._register(new DisposableMap<number, IDisposable>());

private readonly _customAgentsProviders = this._register(new DisposableMap<number, IDisposable>());
private readonly _customAgentsProviderEmitters = this._register(new DisposableMap<number, Emitter<void>>());

private readonly _pendingProgress = new Map<string, { progress: (parts: IChatProgress[]) => void; chatSession: IChatModel | undefined }>();
private readonly _proxy: ExtHostChatAgentsShape2;

Expand All @@ -115,6 +119,7 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
@ILogService private readonly _logService: ILogService,
@IExtensionService private readonly _extensionService: IExtensionService,
@IUriIdentityService private readonly _uriIdentityService: IUriIdentityService,
@IPromptsService private readonly _promptsService: IPromptsService,
) {
super();
this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatAgents2);
Expand Down Expand Up @@ -427,6 +432,46 @@ export class MainThreadChatAgents2 extends Disposable implements MainThreadChatA
$unregisterRelatedFilesProvider(handle: number): void {
this._chatRelatedFilesProviders.deleteAndDispose(handle);
}

async $registerCustomAgentsProvider(handle: number, extensionId: ExtensionIdentifier): Promise<void> {
const extension = await this._extensionService.getExtension(extensionId.value);
if (!extension) {
this._logService.error(`[MainThreadChatAgents2] Could not find extension for CustomAgentsProvider: ${extensionId.value}`);
return;
}

const emitter = new Emitter<void>();
this._customAgentsProviderEmitters.set(handle, emitter);

const disposable = this._promptsService.registerCustomAgentsProvider(extension, {
onDidChangeCustomAgents: emitter.event,
provideCustomAgents: async (options: ICustomAgentQueryOptions, token: CancellationToken) => {
const agents = await this._proxy.$provideCustomAgents(handle, options, token);
if (!agents) {
return undefined;
}
// Convert UriComponents to URI
return agents.map(agent => ({
...agent,
uri: URI.revive(agent.uri)
}));
}
});

this._customAgentsProviders.set(handle, disposable);
}

$unregisterCustomAgentsProvider(handle: number): void {
this._customAgentsProviders.deleteAndDispose(handle);
this._customAgentsProviderEmitters.deleteAndDispose(handle);
}

$onDidChangeCustomAgents(handle: number): void {
const emitter = this._customAgentsProviderEmitters.get(handle);
if (emitter) {
emitter.fire();
}
}
}


Expand Down
7 changes: 6 additions & 1 deletion src/vs/workbench/api/common/extHost.api.impl.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1541,6 +1541,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
checkProposedApiEnabled(extension, 'chatContextProvider');
return extHostChatContext.registerChatContextProvider(selector ? checkSelector(selector) : undefined, `${extension.id}-${id}`, provider);
},
registerCustomAgentsProvider(provider: vscode.CustomAgentsProvider): vscode.Disposable {
checkProposedApiEnabled(extension, 'chatParticipantPrivate');
return extHostChatAgents2.registerCustomAgentsProvider(extension, provider);
},
};

// namespace: lm
Expand Down Expand Up @@ -1942,7 +1946,8 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
McpStdioServerDefinition: extHostTypes.McpStdioServerDefinition,
McpStdioServerDefinition2: extHostTypes.McpStdioServerDefinition,
McpToolAvailability: extHostTypes.McpToolAvailability,
SettingsSearchResultKind: extHostTypes.SettingsSearchResultKind
SettingsSearchResultKind: extHostTypes.SettingsSearchResultKind,
CustomAgentTarget: extHostTypes.CustomAgentTarget,
};
};
}
5 changes: 5 additions & 0 deletions src/vs/workbench/api/common/extHost.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ import { IChatRequestVariableValue } from '../../contrib/chat/common/chatVariabl
import { ChatAgentLocation } from '../../contrib/chat/common/constants.js';
import { IChatMessage, IChatResponsePart, ILanguageModelChatMetadataAndIdentifier, ILanguageModelChatSelector } from '../../contrib/chat/common/languageModels.js';
import { IPreparedToolInvocation, IToolInvocation, IToolInvocationPreparationContext, IToolProgressStep, IToolResult, ToolDataSource } from '../../contrib/chat/common/languageModelToolsService.js';
import { ICustomAgentQueryOptions, IExternalCustomAgent } from '../../contrib/chat/common/promptSyntax/service/promptsService.js';
import { DebugConfigurationProviderTriggerKind, IAdapterDescriptor, IConfig, IDebugSessionReplMode, IDebugTestRunReference, IDebugVisualization, IDebugVisualizationContext, IDebugVisualizationTreeItem, MainThreadDebugVisualization } from '../../contrib/debug/common/debug.js';
import { McpCollectionDefinition, McpConnectionState, McpServerDefinition, McpServerLaunch } from '../../contrib/mcp/common/mcpTypes.js';
import * as notebookCommon from '../../contrib/notebook/common/notebookCommon.js';
Expand Down Expand Up @@ -1393,6 +1394,9 @@ export interface MainThreadChatAgentsShape2 extends IChatAgentProgressShape, IDi
$unregisterChatParticipantDetectionProvider(handle: number): void;
$registerRelatedFilesProvider(handle: number, metadata: IChatRelatedFilesProviderMetadata): void;
$unregisterRelatedFilesProvider(handle: number): void;
$registerCustomAgentsProvider(handle: number, extension: ExtensionIdentifier): void;
$unregisterCustomAgentsProvider(handle: number): void;
$onDidChangeCustomAgents(handle: number): void;
$registerAgentCompletionsProvider(handle: number, id: string, triggerCharacters: string[]): void;
$unregisterAgentCompletionsProvider(handle: number, id: string): void;
$updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void;
Expand Down Expand Up @@ -1457,6 +1461,7 @@ export interface ExtHostChatAgentsShape2 {
$releaseSession(sessionId: string): void;
$detectChatParticipant(handle: number, request: Dto<IChatAgentRequest>, context: { history: IChatAgentHistoryEntryDto[] }, options: { participants: IChatParticipantMetadata[]; location: ChatAgentLocation }, token: CancellationToken): Promise<IChatParticipantDetectionResult | null | undefined>;
$provideRelatedFiles(handle: number, request: Dto<IChatRequestDraft>, token: CancellationToken): Promise<Dto<IChatRelatedFile>[] | undefined>;
$provideCustomAgents(handle: number, options: ICustomAgentQueryOptions, token: CancellationToken): Promise<Dto<IExternalCustomAgent>[] | undefined>;
$setRequestTools(requestId: string, tools: UserSelectedTools): void;
}
export interface IChatParticipantMetadata {
Expand Down
35 changes: 35 additions & 0 deletions src/vs/workbench/api/common/extHostChatAgents2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import { ExtHostLanguageModels } from './extHostLanguageModels.js';
import { ExtHostLanguageModelTools } from './extHostLanguageModelTools.js';
import * as typeConvert from './extHostTypeConverters.js';
import * as extHostTypes from './extHostTypes.js';
import { ICustomAgentQueryOptions, IExternalCustomAgent } from '../../contrib/chat/common/promptSyntax/service/promptsService.js';

export class ChatAgentResponseStream {

Expand Down Expand Up @@ -394,6 +395,9 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
private static _relatedFilesProviderIdPool = 0;
private readonly _relatedFilesProviders = new Map<number, ExtHostRelatedFilesProvider>();

private static _customAgentsProviderIdPool = 0;
private readonly _customAgentsProviders = new Map<number, { extension: IExtensionDescription; provider: vscode.CustomAgentsProvider }>();

private readonly _sessionDisposables: DisposableMap<string, DisposableStore> = this._register(new DisposableMap());
private readonly _completionDisposables: DisposableMap<number, DisposableStore> = this._register(new DisposableMap());

Expand Down Expand Up @@ -471,6 +475,28 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
});
}

registerCustomAgentsProvider(extension: IExtensionDescription, provider: vscode.CustomAgentsProvider): vscode.Disposable {
const handle = ExtHostChatAgents2._customAgentsProviderIdPool++;
this._customAgentsProviders.set(handle, { extension, provider });
this._proxy.$registerCustomAgentsProvider(handle, extension.identifier);

const disposables = new DisposableStore();

// Listen to provider change events and notify main thread
if (provider.onDidChangeCustomAgents) {
disposables.add(provider.onDidChangeCustomAgents(() => {
this._proxy.$onDidChangeCustomAgents(handle);
}));
}

disposables.add(toDisposable(() => {
this._customAgentsProviders.delete(handle);
this._proxy.$unregisterCustomAgentsProvider(handle);
}));

return disposables;
}

async $provideRelatedFiles(handle: number, request: IChatRequestDraft, token: CancellationToken): Promise<Dto<IChatRelatedFile>[] | undefined> {
const provider = this._relatedFilesProviders.get(handle);
if (!provider) {
Expand All @@ -481,6 +507,15 @@ export class ExtHostChatAgents2 extends Disposable implements ExtHostChatAgentsS
return await provider.provider.provideRelatedFiles(extRequestDraft, token) ?? undefined;
}

async $provideCustomAgents(handle: number, options: ICustomAgentQueryOptions, token: CancellationToken): Promise<IExternalCustomAgent[] | undefined> {
const providerData = this._customAgentsProviders.get(handle);
if (!providerData) {
return Promise.resolve(undefined);
}

return await providerData.provider.provideCustomAgents(options, token) ?? undefined;
}

async $detectChatParticipant(handle: number, requestDto: Dto<IChatAgentRequest>, context: { history: IChatAgentHistoryEntryDto[] }, options: { location: ChatAgentLocation; participants?: vscode.ChatParticipantMetadata[] }, token: CancellationToken): Promise<vscode.ChatParticipantDetectionResult | null | undefined> {
const detector = this._participantDetectionProviders.get(handle);
if (!detector) {
Expand Down
5 changes: 5 additions & 0 deletions src/vs/workbench/api/common/extHostTypes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3489,6 +3489,11 @@ export enum ChatErrorLevel {
Error = 2
}

export enum CustomAgentTarget {
GitHubCopilot = 'github-copilot',
VSCode = 'vscode',
}

export class LanguageModelChatMessage implements vscode.LanguageModelChatMessage {

static User(content: string | (LanguageModelTextPart | LanguageModelToolResultPart | LanguageModelToolCallPart | LanguageModelDataPart)[], name?: string): LanguageModelChatMessage {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { IProductService } from '../../../../../platform/product/common/productS
import { IChatAgentService } from '../../common/chatAgents.js';
import { ChatMode, IChatMode, IChatModeService } from '../../common/chatModes.js';
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';
import { PromptsStorage } from '../../common/promptSyntax/service/promptsService.js';
import { ExtensionAgentSourceType, PromptsStorage } from '../../common/promptSyntax/service/promptsService.js';
import { getOpenChatActionIdForMode } from '../actions/chatActions.js';
import { IToggleChatModeArgs, ToggleAgentModeActionId } from '../actions/chatExecuteActions.js';

Expand Down Expand Up @@ -104,7 +104,7 @@ export class ModePickerActionItem extends ActionWidgetDropdownActionViewItem {
const otherBuiltinModes = modes.builtin.filter(mode => mode.id !== ChatMode.Agent.id);
const customModes = groupBy(
modes.custom,
mode => mode.source?.storage === PromptsStorage.extension && mode.source.extensionId.value === productService.defaultChatAgent?.chatExtensionId ?
mode => mode.source?.storage === PromptsStorage.extension && mode.source.extensionId.value === productService.defaultChatAgent?.chatExtensionId && mode.source.type === ExtensionAgentSourceType.contribution ?
'builtin' : 'custom');

const customBuiltinModeActions = customModes.builtin?.map(mode => {
Expand Down
8 changes: 4 additions & 4 deletions src/vs/workbench/contrib/chat/common/chatModes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import { IChatAgentService } from './chatAgents.js';
import { ChatContextKeys } from './chatContextKeys.js';
import { ChatConfiguration, ChatModeKind } from './constants.js';
import { IHandOff } from './promptSyntax/promptFileParser.js';
import { IAgentSource, ICustomAgent, IPromptsService, PromptsStorage } from './promptSyntax/service/promptsService.js';
import { ExtensionAgentSourceType, IAgentSource, ICustomAgent, IPromptsService, PromptsStorage } from './promptSyntax/service/promptsService.js';

export const IChatModeService = createDecorator<IChatModeService>('chatModeService');
export interface IChatModeService {
Expand Down Expand Up @@ -419,7 +419,7 @@ export class CustomChatMode implements IChatMode {
}

type IChatModeSourceData =
| { readonly storage: PromptsStorage.extension; readonly extensionId: string }
| { readonly storage: PromptsStorage.extension; readonly extensionId: string; type?: ExtensionAgentSourceType }
| { readonly storage: PromptsStorage.local | PromptsStorage.user };

function isChatModeSourceData(value: unknown): value is IChatModeSourceData {
Expand All @@ -438,7 +438,7 @@ function serializeChatModeSource(source: IAgentSource | undefined): IChatModeSou
return undefined;
}
if (source.storage === PromptsStorage.extension) {
return { storage: PromptsStorage.extension, extensionId: source.extensionId.value };
return { storage: PromptsStorage.extension, extensionId: source.extensionId.value, type: source.type };
}
return { storage: source.storage };
}
Expand All @@ -448,7 +448,7 @@ function reviveChatModeSource(data: IChatModeSourceData | undefined): IAgentSour
return undefined;
}
if (data.storage === PromptsStorage.extension) {
return { storage: PromptsStorage.extension, extensionId: new ExtensionIdentifier(data.extensionId) };
return { storage: PromptsStorage.extension, extensionId: new ExtensionIdentifier(data.extensionId), type: data.type ?? ExtensionAgentSourceType.contribution };
}
return { storage: data.storage };
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,44 @@ import { PromptsType } from '../promptTypes.js';
import { IHandOff, ParsedPromptFile } from '../promptFileParser.js';
import { ResourceSet } from '../../../../../../base/common/map.js';

/**
* Target environment for custom agents.
*/
export enum CustomAgentTarget {
GitHubCopilot = 'github-copilot',
VSCode = 'vscode',
}

/**
* Options for querying custom agents.
*/
export interface ICustomAgentQueryOptions {
/**
* Filter agents by target environment.
*/
readonly target?: CustomAgentTarget;
}

/**
* Represents a custom agent resource from an external provider.
*/
export interface IExternalCustomAgent {
/**
* The unique identifier/name of the custom agent resource.
*/
readonly name: string;

/**
* A description of what the custom agent resource does.
*/
readonly description: string;

/**
* The URI to the agent or prompt resource file.
*/
readonly uri: URI;
}

/**
* Provides prompt services.
*/
Expand All @@ -29,6 +67,14 @@ export enum PromptsStorage {
extension = 'extension'
}

/**
* The type of source for extension agents.
*/
export enum ExtensionAgentSourceType {
contribution = 'contribution',
provider = 'provider',
}

/**
* Represents a prompt path with its type.
* This is used for both prompt files and prompt source folders.
Expand Down Expand Up @@ -67,6 +113,7 @@ export interface IExtensionPromptPath extends IPromptPathBase {
readonly extension: IExtensionDescription;
readonly name: string;
readonly description: string;
readonly source: ExtensionAgentSourceType;
}
export interface ILocalPromptPath extends IPromptPathBase {
readonly storage: PromptsStorage.local;
Expand All @@ -78,6 +125,7 @@ export interface IUserPromptPath extends IPromptPathBase {
export type IAgentSource = {
readonly storage: PromptsStorage.extension;
readonly extensionId: ExtensionIdentifier;
readonly type: ExtensionAgentSourceType;
} | {
readonly storage: PromptsStorage.local | PromptsStorage.user;
};
Expand Down Expand Up @@ -270,6 +318,18 @@ export interface IPromptsService extends IDisposable {
*/
setDisabledPromptFiles(type: PromptsType, uris: ResourceSet): void;

/**
* Registers a CustomAgentsProvider that can provide custom agents for repositories.
* This is part of the proposed API and requires the chatParticipantPrivate proposal.
* @param extension The extension registering the provider.
* @param provider The provider implementation with optional change event.
* @returns A disposable that unregisters the provider when disposed.
*/
registerCustomAgentsProvider(extension: IExtensionDescription, provider: {
onDidChangeCustomAgents?: Event<void>;
provideCustomAgents: (options: ICustomAgentQueryOptions, token: CancellationToken) => Promise<IExternalCustomAgent[] | undefined>;
}): IDisposable;

/**
* Gets list of claude skills files.
*/
Expand Down
Loading
Loading