diff --git a/extensions/markdown-language-features/package.json b/extensions/markdown-language-features/package.json index 543d2e127199c..08592dde710e5 100644 --- a/extensions/markdown-language-features/package.json +++ b/extensions/markdown-language-features/package.json @@ -15,6 +15,9 @@ "categories": [ "Programming Languages" ], + "enabledApiProposals": [ + "chatSessionProvider" + ], "activationEvents": [ "onLanguage:markdown", "onLanguage:prompt", @@ -22,7 +25,10 @@ "onLanguage:chatmode", "onCommand:markdown.api.render", "onCommand:markdown.api.reloadPlugins", - "onWebviewPanel:markdown.preview" + "onCommand:markdown.openChatSession", + "onWebviewPanel:markdown.preview", + "onChatSessionContentProvider:markdown", + "onChatSessionContentProvider:markdown" ], "capabilities": { "virtualWorkspaces": true, @@ -120,6 +126,11 @@ } ], "commands": [ + { + "command": "markdown.openChatSession", + "title": "Open test Chat Session", + "category": "Markdown" + }, { "command": "_markdown.copyImage", "title": "%markdown.copyImage.title%", diff --git a/extensions/markdown-language-features/src/extension.ts b/extensions/markdown-language-features/src/extension.ts index 98ea87df06990..0c60ab9f96518 100644 --- a/extensions/markdown-language-features/src/extension.ts +++ b/extensions/markdown-language-features/src/extension.ts @@ -12,6 +12,78 @@ import { IMdParser, MarkdownItEngine } from './markdownEngine'; import { getMarkdownExtensionContributions } from './markdownExtensions'; import { githubSlugifier } from './slugify'; +// Chat session provider implementation +const sessionProviderType = 'fake-session-provider'; +function registerMarkdownChatProvider(context: vscode.ExtensionContext) { + // Create sample history with a request and a response + const sampleHistory: Array = [ + // Simple request turn with proper ChatRequestTurn structure + new vscode.ChatRequestTurn('Hello, this is a test request to the markdown chat provider', undefined, [], 'markdown', []), + // Simple response turn with proper ChatResponseTurn structure + new vscode.ChatResponseTurn( + [new vscode.ChatResponseMarkdownPart(new vscode.MarkdownString('Hello! I am a simple markdown chat provider. I can help with markdown-related questions.'))], + {}, + 'markdown' + ) + ]; + + // Request handler that reverses the input text + const requestHandler: vscode.ChatRequestHandler = async ( + request: vscode.ChatRequest, + _context: unknown, + stream: vscode.ChatResponseStream, + _token: vscode.CancellationToken + ): Promise => { + // Extract the text from the request + const text = request.prompt || ''; + + if (text) { + // Reverse the input text and stream it back + const reversedText = text.split('').reverse().join(''); + + // First send the original text + await stream.progress('You said: ' + text); + + // Wait a bit to simulate processing + await new Promise(resolve => setTimeout(resolve, 500)); + + // Then send the reversed text + await stream.progress('Here is your text reversed: '); + await stream.progress('```\n' + reversedText + '\n```'); + + // Complete the response by resolving the promise + return Promise.resolve(); + } else { + // Handle empty messages + await stream.progress('I can only process text messages right now.'); + return Promise.resolve(); + } + }; + + // Register the chat session provider + const chatSessionProvider: vscode.ChatSessionContentProvider = { + provideChatSessionContent: async (_id: string, _token: vscode.CancellationToken): Promise => { + // Create a chat session with our sample history and request handler + return { + history: sampleHistory, + requestHandler: requestHandler + }; + } + }; + + // Register the provider with the 'markdown' type + context.subscriptions.push( + vscode.chat.registerChatSessionContentProvider(sessionProviderType, chatSessionProvider) + ); + + // Register a command to open a markdown chat session + context.subscriptions.push( + vscode.commands.registerCommand('markdown.openChatSession', async () => { + await vscode.window.openChatSession(sessionProviderType, '123'); + }) + ); +} + export async function activate(context: vscode.ExtensionContext) { const contributions = getMarkdownExtensionContributions(context); context.subscriptions.push(contributions); @@ -24,6 +96,9 @@ export async function activate(context: vscode.ExtensionContext) { const client = await startServer(context, engine); context.subscriptions.push(client); activateShared(context, client, engine, logger, contributions); + + // Register our markdown chat provider + registerMarkdownChatProvider(context); } function startServer(context: vscode.ExtensionContext, parser: IMdParser): Promise { diff --git a/extensions/markdown-language-features/tsconfig.json b/extensions/markdown-language-features/tsconfig.json index fcd79775de5f8..0bf0e22aaa175 100644 --- a/extensions/markdown-language-features/tsconfig.json +++ b/extensions/markdown-language-features/tsconfig.json @@ -5,6 +5,7 @@ }, "include": [ "src/**/*", - "../../src/vscode-dts/vscode.d.ts" + "../../src/vscode-dts/vscode.d.ts", + "../../src/vscode-dts/vscode.proposed.chatSessionProvider.d.ts", ] } diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index f640014499e52..70d7944c4ec4f 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -82,7 +82,9 @@ export namespace Schemas { export const vscodeChatCodeCompareBlock = 'vscode-chat-code-compare-block'; /** Scheme used for the chat input editor. */ - export const vscodeChatSesssion = 'vscode-chat-editor'; + export const vscodeChatEditor = 'vscode-chat-editor'; + + export const vscodeChatSession = 'vscode-chat-session'; /** Scheme used for the chat input part */ export const vscodeChatInput = 'chatSessionInput'; diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 93e050e83f091..0f7c79dc54154 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -53,6 +53,9 @@ const _allApiProposals = { chatReferenceDiagnostic: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatReferenceDiagnostic.d.ts', }, + chatSessionProvider: { + proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatSessionProvider.d.ts', + }, chatStatusItem: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.chatStatusItem.d.ts', }, diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 45ab13fd348fd..75d3704194621 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -92,6 +92,7 @@ import './mainThreadAiSettingsSearch.js'; import './mainThreadMcp.js'; import './mainThreadChatStatus.js'; import './mainThreadDataChannels.js'; +import './mainThreadChatSessionContentProviders.js'; export class ExtensionPoints implements IWorkbenchContribution { diff --git a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts index 6c231b0f15f6e..eae3aaad14408 100644 --- a/src/vs/workbench/api/browser/mainThreadChatAgents2.ts +++ b/src/vs/workbench/api/browser/mainThreadChatAgents2.ts @@ -424,7 +424,7 @@ namespace ChatNotebookEdit { export function fromChatEdit(part: IChatNotebookEditDto): IChatNotebookEdit { return { kind: 'notebookEdit', - uri: part.uri, + uri: URI.revive(part.uri), done: part.done, edits: part.edits.map(NotebookDto.fromCellEditOperationDto) }; diff --git a/src/vs/workbench/api/browser/mainThreadChatSessionContentProviders.ts b/src/vs/workbench/api/browser/mainThreadChatSessionContentProviders.ts new file mode 100644 index 0000000000000..548ecdcc4ac3e --- /dev/null +++ b/src/vs/workbench/api/browser/mainThreadChatSessionContentProviders.ts @@ -0,0 +1,77 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { Disposable, DisposableMap } from '../../../base/common/lifecycle.js'; +import { revive } from '../../../base/common/marshalling.js'; +import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; +import { ChatModel, IChatModel } from '../../contrib/chat/common/chatModel.js'; +import { ChatRequestParser } from '../../contrib/chat/common/chatRequestParser.js'; +import { IChatProgress } from '../../contrib/chat/common/chatService.js'; +import { IChatSessionContentProviderService } from '../../contrib/chat/common/chatSessionContentProviderService.js'; +import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; +import { extHostNamedCustomer } from '../../services/extensions/common/extHostCustomers.js'; +import { ExtHostChatSessionContentProvidersShape, ExtHostContext, MainContext, MainThreadChatSessionContentProvidersShape } from '../common/extHost.protocol.js'; + +@extHostNamedCustomer(MainContext.MainThreadChatSessionContentProviders) +export class MainThreadChatSessionContentProviders implements MainThreadChatSessionContentProvidersShape { + + private readonly _providers = new DisposableMap(); + private readonly _proxy: ExtHostChatSessionContentProvidersShape; + + constructor( + extHostContext: import('../../services/extensions/common/extHostCustomers.js').IExtHostContext, + @IInstantiationService private readonly instantiationService: IInstantiationService, + @IChatSessionContentProviderService private readonly chatSessionContentProviderService: IChatSessionContentProviderService + ) { + this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatSessionContentProviders); + } + + dispose(): void { + this._providers.dispose(); + } + + $registerChatSessionContentProvider(handle: number, _chatSessionType: string): void { + this._providers.set(handle, Disposable.None); + this.chatSessionContentProviderService.registerChatSessionContentProvider(_chatSessionType, { + provideChatSessionContent: async (id, token): Promise => { + const parser = this.instantiationService.createInstance(ChatRequestParser); + + // TODO: use real data + const results = await this._proxy.$provideChatSessionContent(handle, id, token); + const model = this.instantiationService.createInstance(ChatModel, undefined, ChatAgentLocation.Panel); + + for (let i = 0; i < results.history.length; i++) { + const item = results.history[i]; + if (item.type === 'response') { + // We can only create request response pairs :( + // Figure out how to add this in the UI + continue; + } + const parsedRequest = parser.parseChatRequest('sessionId', item.prompt).parts; + const request = model.addRequest({ + text: item.prompt, + parts: parsedRequest, + }, { variables: [] }, 0); + + const next = results.history[i + 1]; + if (next && next.type === 'response') { + i++; + for (const responsePart of next.parts) { + const revivedProgress = revive(responsePart) as IChatProgress; + model.acceptResponseProgress(request, revivedProgress); + } + model.completeResponse(request); + } + } + + return model; + } + }); + } + $unregisterChatSessionContentProvider(handle: number): void { + this._providers.deleteAndDispose(handle); + } + +} diff --git a/src/vs/workbench/api/browser/mainThreadWindow.ts b/src/vs/workbench/api/browser/mainThreadWindow.ts index 393b182479bc2..e8dd4de38968f 100644 --- a/src/vs/workbench/api/browser/mainThreadWindow.ts +++ b/src/vs/workbench/api/browser/mainThreadWindow.ts @@ -3,15 +3,16 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ +import { encodeBase64 } from '../../../base/common/buffer.js'; import { Event } from '../../../base/common/event.js'; import { DisposableStore } from '../../../base/common/lifecycle.js'; +import { Schemas } from '../../../base/common/network.js'; import { URI, UriComponents } from '../../../base/common/uri.js'; import { IOpenerService } from '../../../platform/opener/common/opener.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; -import { ExtHostContext, ExtHostWindowShape, IOpenUriOptions, MainContext, MainThreadWindowShape } from '../common/extHost.protocol.js'; import { IHostService } from '../../services/host/browser/host.js'; import { IUserActivityService } from '../../services/userActivity/common/userActivityService.js'; -import { encodeBase64 } from '../../../base/common/buffer.js'; +import { ExtHostContext, ExtHostWindowShape, IOpenUriOptions, MainContext, MainThreadWindowShape } from '../common/extHost.protocol.js'; @extHostNamedCustomer(MainContext.MainThreadWindow) export class MainThreadWindow implements MainThreadWindowShape { @@ -73,7 +74,24 @@ export class MainThreadWindow implements MainThreadWindowShape { } async $asExternalUri(uriComponents: UriComponents, options: IOpenUriOptions): Promise { - const result = await this.openerService.resolveExternalUri(URI.revive(uriComponents), options); + const uri = URI.revive(uriComponents); + const result = await this.openerService.resolveExternalUri(uri, options); return result.resolved; } + + async $openChatSession(sessionType: string, id: string): Promise { + // TODO: should live in chat instead + + // Create a URI with the chat session scheme + const chatSessionUri = URI.from({ + scheme: Schemas.vscodeChatSession, + authority: sessionType, + path: `/${id}` + }); + + + // TODO: Integrate with the chat service to open the session in the chat view + // For now, we'll just open the URI + await this.openerService.open(chatSessionUri); + } } diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 9ecae0591ae58..04225aceaa35d 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -112,6 +112,7 @@ import { ExtHostWebviewViews } from './extHostWebviewView.js'; import { IExtHostWindow } from './extHostWindow.js'; import { IExtHostWorkspace } from './extHostWorkspace.js'; import { ExtHostAiSettingsSearch } from './extHostAiSettingsSearch.js'; +import { ExtHostChatSessionContentProviders } from './extHostChatSessionContentProviders.js'; export interface IExtensionRegistries { mine: ExtensionDescriptionRegistry; @@ -229,6 +230,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostSpeech = rpcProtocol.set(ExtHostContext.ExtHostSpeech, new ExtHostSpeech(rpcProtocol)); const extHostEmbeddings = rpcProtocol.set(ExtHostContext.ExtHostEmbeddings, new ExtHostEmbeddings(rpcProtocol)); rpcProtocol.set(ExtHostContext.ExtHostMcp, accessor.get(IExtHostMpcService)); + const extHostChatSessionContentProviders = rpcProtocol.set(ExtHostContext.ExtHostChatSessionContentProviders, new ExtHostChatSessionContentProviders(rpcProtocol, extHostCommands.converter)); // Check that no named customers are missing const expected = Object.values>(ExtHostContext); @@ -949,6 +951,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatStatusItem'); return extHostChatStatus.createChatStatusItem(extension, id); }, + openChatSession: (sessionType: string, id: string) => { + checkProposedApiEnabled(extension, 'chatSessionProvider'); + return extHostWindow.openChatSession(sessionType, id); + }, }; // namespace: workspace @@ -1498,6 +1504,10 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I onDidDisposeChatSession: (listeners, thisArgs?, disposables?) => { checkProposedApiEnabled(extension, 'chatParticipantPrivate'); return _asExtensionEvent(extHostChatAgents2.onDidDisposeChatSession)(listeners, thisArgs, disposables); + }, + registerChatSessionContentProvider(id: string, provider: vscode.ChatSessionContentProvider) { + checkProposedApiEnabled(extension, 'chatSessionProvider'); + return extHostChatSessionContentProviders.registerChatSessionContentProvider(id, provider); } }; diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index 69c75559aeee6..c21c730be389a 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -57,7 +57,7 @@ import { IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from '../../c import { ICodeMapperRequest, ICodeMapperResult } from '../../contrib/chat/common/chatCodeMapperService.js'; import { IChatRelatedFile, IChatRelatedFileProviderMetadata as IChatRelatedFilesProviderMetadata, IChatRequestDraft } from '../../contrib/chat/common/chatEditingService.js'; import { IChatProgressHistoryResponseContent } from '../../contrib/chat/common/chatModel.js'; -import { IChatContentInlineReference, IChatFollowup, IChatNotebookEdit, IChatProgress, IChatResponseErrorDetails, IChatTask, IChatTaskDto, IChatUserActionEvent, IChatVoteAction } from '../../contrib/chat/common/chatService.js'; +import { IChatContentInlineReference, IChatFollowup, IChatNotebookEdit, IChatProgress, IChatTask, IChatTaskDto, IChatUserActionEvent, IChatVoteAction } from '../../contrib/chat/common/chatService.js'; import { IChatRequestVariableValue } from '../../contrib/chat/common/chatVariables.js'; import { ChatAgentLocation } from '../../contrib/chat/common/constants.js'; import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata, ILanguageModelChatSelector, ILanguageModelsChangeEvent } from '../../contrib/chat/common/languageModels.js'; @@ -1418,22 +1418,6 @@ export interface MainThreadUrlsShape extends IDisposable { $createAppUri(uri: UriComponents): Promise; } -export interface IChatDto { -} - -export interface IChatRequestDto { - message: string; - variables?: Record; -} - -export interface IChatResponseDto { - errorDetails?: IChatResponseErrorDetails; - timings: { - firstProgress: number; - totalElapsed: number; - }; -} - export interface IChatResponseProgressFileTreeData { label: string; uri: URI; @@ -1745,6 +1729,7 @@ export interface MainThreadWindowShape extends IDisposable { $getInitialState(): Promise<{ isFocused: boolean; isActive: boolean }>; $openUri(uri: UriComponents, uriString: string | undefined, options: IOpenUriOptions): Promise; $asExternalUri(uri: UriComponents, options: IOpenUriOptions): Promise; + $openChatSession(sessionType: string, id: string): Promise; } export enum CandidatePortSource { @@ -2189,7 +2174,7 @@ export interface IWorkspaceEditEntryMetadataDto { } export interface IChatNotebookEditDto { - uri: URI; + uri: UriComponents; edits: ICellEditOperationDto[]; kind: 'notebookEdit'; done?: boolean; @@ -3114,6 +3099,29 @@ export interface MainThreadChatStatusShape { $disposeEntry(id: string): void; } +// --- chat session content providers + +export interface MainThreadChatSessionContentProvidersShape extends IDisposable { + $registerChatSessionContentProvider(handle: number, chatSessionType: string): void; + $unregisterChatSessionContentProvider(handle: number): void; +} + +export interface ExtHostChatSessionContentProvidersShape { + $provideChatSessionContent(handle: number, id: string, token: CancellationToken): Promise; +} + +export interface IChatRequestTurnDto { + +} + +export interface ChatSessionDto { + id: string; + + history: Array< + | { type: 'request'; prompt: string } + | { type: 'response'; parts: IChatProgressDto[] }>; +} + // --- proxy identifiers export const MainContext = { @@ -3191,6 +3199,7 @@ export const MainContext = { MainThreadChatStatus: createProxyIdentifier('MainThreadChatStatus'), MainThreadAiSettingsSearch: createProxyIdentifier('MainThreadAiSettingsSearch'), MainThreadDataChannels: createProxyIdentifier('MainThreadDataChannels'), + MainThreadChatSessionContentProviders: createProxyIdentifier('MainThreadChatSessionContentProviders'), }; export const ExtHostContext = { @@ -3264,4 +3273,5 @@ export const ExtHostContext = { ExtHostLocalization: createProxyIdentifier('ExtHostLocalization'), ExtHostMcp: createProxyIdentifier('ExtHostMcp'), ExtHostDataChannels: createProxyIdentifier('ExtHostDataChannels'), + ExtHostChatSessionContentProviders: createProxyIdentifier('ExtHostChatSessionContentProviders'), }; diff --git a/src/vs/workbench/api/common/extHostChatSessionContentProviders.ts b/src/vs/workbench/api/common/extHostChatSessionContentProviders.ts new file mode 100644 index 0000000000000..cf3cb180336e8 --- /dev/null +++ b/src/vs/workbench/api/common/extHostChatSessionContentProviders.ts @@ -0,0 +1,70 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import type * as vscode from 'vscode'; +import { coalesce } from '../../../base/common/arrays.js'; +import { CancellationToken } from '../../../base/common/cancellation.js'; +import { DisposableStore, toDisposable } from '../../../base/common/lifecycle.js'; +import { ChatSessionDto, ExtHostChatSessionContentProvidersShape, IMainContext, MainContext, MainThreadChatSessionContentProvidersShape } from './extHost.protocol.js'; +import { CommandsConverter } from './extHostCommands.js'; +import * as typeConvert from './extHostTypeConverters.js'; +import * as extHostTypes from './extHostTypes.js'; + +export class ExtHostChatSessionContentProviders implements ExtHostChatSessionContentProvidersShape { + + private static _providerHandlePool = 0; + private static _sessionHandlePool = 0; + + private readonly _providers = new Map(); + private readonly _proxy: MainThreadChatSessionContentProvidersShape; + + constructor( + mainContext: IMainContext, + private readonly _commandsConverter: CommandsConverter, + ) { + this._proxy = mainContext.getProxy(MainContext.MainThreadChatSessionContentProviders); + } + + registerChatSessionContentProvider(chatSessionType: string, provider: vscode.ChatSessionContentProvider): vscode.Disposable { + const handle = ExtHostChatSessionContentProviders._providerHandlePool++; + this._providers.set(handle, provider); + this._proxy.$registerChatSessionContentProvider(handle, chatSessionType); + return toDisposable(() => { + if (this._providers.delete(handle)) { + this._proxy.$unregisterChatSessionContentProvider(handle); + } + }); + } + + async $provideChatSessionContent(handle: number, id: string, token: CancellationToken): Promise { + const provider = this._providers.get(handle); + if (!provider) { + throw new Error(`No provider for handle ${handle}`); + } + + const session = await provider.provideChatSessionContent(id, token); + + // TODO: leaked + const sessionDisposables = new DisposableStore(); + + const sessionId = ExtHostChatSessionContentProviders._sessionHandlePool++; + return { + id: sessionId + '', + history: session.history.map(turn => { + if (turn instanceof extHostTypes.ChatRequestTurn) { + return { type: 'request', prompt: turn.prompt }; + } else { + const responseTurn = turn as extHostTypes.ChatResponseTurn; + const parts = coalesce(responseTurn.response.map(r => typeConvert.ChatResponsePart.from(r, this._commandsConverter, sessionDisposables))); + + return { + type: 'response', + parts + }; + } + }) + }; + } +} diff --git a/src/vs/workbench/api/common/extHostWindow.ts b/src/vs/workbench/api/common/extHostWindow.ts index 2919b1ecd3eb1..36035df8d7eac 100644 --- a/src/vs/workbench/api/common/extHostWindow.ts +++ b/src/vs/workbench/api/common/extHostWindow.ts @@ -110,6 +110,10 @@ export class ExtHostWindow implements ExtHostWindowShape { const result = await this._proxy.$asExternalUri(uri, options); return URI.from(result); } + + async openChatSession(sessionType: string, id: string): Promise { + return this._proxy.$openChatSession(sessionType, id); + } } export const IExtHostWindow = createDecorator('IExtHostWindow'); diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index ded8ff37a6f15..f65b06eefff13 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -114,6 +114,8 @@ import { ChatDynamicVariableModel } from './contrib/chatDynamicVariables.js'; import { ChatAttachmentResolveService, IChatAttachmentResolveService } from './chatAttachmentResolveService.js'; import { registerLanguageModelActions } from './actions/chatLanguageModelActions.js'; import { PromptUrlHandler } from './promptSyntax/promptUrlHandler.js'; +import { IChatSessionContentProviderService } from '../common/chatSessionContentProviderService.js'; +import { ChatSessionContentProviderService } from './chatSessionContentProviderServiceImpl.js'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -527,7 +529,8 @@ class ChatResolverContribution extends Disposable { super(); this._register(editorResolverService.registerEditor( - `${Schemas.vscodeChatSesssion}:**/**`, + // TODO: support both + `${Schemas.vscodeChatSession}:**/**`, { id: ChatEditorInput.EditorID, label: nls.localize('chat', "Chat"), @@ -535,14 +538,24 @@ class ChatResolverContribution extends Disposable { }, { singlePerResource: true, - canSupportResource: resource => resource.scheme === Schemas.vscodeChatSesssion + canSupportResource: resource => resource.scheme === Schemas.vscodeChatEditor || resource.scheme === Schemas.vscodeChatSession }, { createEditorInput: ({ resource, options }) => { - return { editor: instantiationService.createInstance(ChatEditorInput, resource, options as IChatEditorOptions), options }; + if (resource.scheme === Schemas.vscodeChatEditor) { + return { editor: instantiationService.createInstance(ChatEditorInput, resource, options as IChatEditorOptions), options }; + } else { + return { + editor: instantiationService.createInstance(ChatEditorInput, resource, { + ...options, + target: { chatSessionProviderId: resource.authority, sessionId: resource.path.slice(1) } + }), options + }; + } } } )); + } } @@ -753,6 +766,7 @@ registerEditorFeature(ChatPasteProvidersFeature); registerSingleton(IChatTransferService, ChatTransferService, InstantiationType.Delayed); registerSingleton(IChatService, ChatService, InstantiationType.Delayed); +registerSingleton(IChatSessionContentProviderService, ChatSessionContentProviderService, InstantiationType.Delayed); registerSingleton(IChatWidgetService, ChatWidgetService, InstantiationType.Delayed); registerSingleton(IQuickChatService, QuickChatService, InstantiationType.Delayed); registerSingleton(IChatAccessibilityService, ChatAccessibilityService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditor.ts b/src/vs/workbench/contrib/chat/browser/chatEditor.ts index 8cb0a4dfc19c8..8997daf8dbf7d 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditor.ts @@ -27,7 +27,11 @@ import { ChatEditorInput } from './chatEditorInput.js'; import { ChatWidget, IChatViewState } from './chatWidget.js'; export interface IChatEditorOptions extends IEditorOptions { - target?: { sessionId: string } | { data: IExportableChatData | ISerializableChatData }; + target?: + | { sessionId: string } + | { data: IExportableChatData | ISerializableChatData } + | { chatSessionProviderId: string; sessionId: string } + ; } export class ChatEditor extends EditorPane { diff --git a/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts b/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts index e4ff9246db0fb..2d10ddb97d6d3 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditorInput.ts @@ -58,10 +58,11 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler ) { super(); - const parsed = ChatUri.parse(resource); - if (typeof parsed?.handle !== 'number') { - throw new Error('Invalid chat URI'); - } + // TODO: temp + // const parsed = ChatUri.parse(resource); + // if (typeof parsed?.handle !== 'number') { + // throw new Error('Invalid chat URI'); + // } this.sessionId = (options.target && 'sessionId' in options.target) ? options.target.sessionId : @@ -113,18 +114,20 @@ export class ChatEditorInput extends EditorInput implements IEditorCloseHandler } override async resolve(): Promise { - if (typeof this.sessionId === 'string') { + if (this.options.target && 'chatSessionProviderId' in this.options.target) { + this.model = await this.chatService.loadSessionForProvider(this.options.target.chatSessionProviderId, this.options.target.sessionId); + } else if (typeof this.sessionId === 'string') { this.model = await this.chatService.getOrRestoreSession(this.sessionId) ?? this.chatService.startSession(ChatAgentLocation.Panel, CancellationToken.None); } else if (!this.options.target) { this.model = this.chatService.startSession(ChatAgentLocation.Panel, CancellationToken.None); } else if ('data' in this.options.target) { this.model = this.chatService.loadSessionFromContent(this.options.target.data); - } + } else - if (!this.model) { - return null; - } + if (!this.model) { + return null; + } this.sessionId = this.model.sessionId; this._register(this.model.onDidChange(() => this._onDidChangeLabel.fire())); @@ -171,7 +174,7 @@ export class ChatEditorModel extends Disposable { export namespace ChatUri { - export const scheme = Schemas.vscodeChatSesssion; + export const scheme = Schemas.vscodeChatEditor; export function generate(handle: number): URI { diff --git a/src/vs/workbench/contrib/chat/browser/chatSessionContentProviderServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatSessionContentProviderServiceImpl.ts new file mode 100644 index 0000000000000..440d4bd89741f --- /dev/null +++ b/src/vs/workbench/contrib/chat/browser/chatSessionContentProviderServiceImpl.ts @@ -0,0 +1,38 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { IChatModel } from '../common/chatModel.js'; +import { IChatSessionContentProvider, IChatSessionContentProviderService } from '../common/chatSessionContentProviderService.js'; + +export class ChatSessionContentProviderService extends Disposable implements IChatSessionContentProviderService { + _serviceBrand: undefined; + private readonly _providers = new Map(); + + registerChatSessionContentProvider(type: string, provider: IChatSessionContentProvider): void { + this._providers.set(type, provider); + } + + unregisterChatSessionContentProvider(type: string): void { + this._providers.delete(type); + } + + getChatSessionContentProvider(type: string): IChatSessionContentProvider | undefined { + return this._providers.get(type); + } + + async getChatSession(type: string, id: string, token: CancellationToken): Promise { + const provider = this.getChatSessionContentProvider(type); + if (!provider) { + throw new Error(`No chat session content provider found for type: ${type}`); + } + + const model = await provider.provideChatSessionContent(id, token); + + return model; + } +} + diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index fbaaeae7670b1..7d85438019705 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -546,6 +546,7 @@ export interface IChatService { getOrRestoreSession(sessionId: string): Promise; isPersistedSessionEmpty(sessionId: string): boolean; loadSessionFromContent(data: IExportableChatData | ISerializableChatData): IChatModel | undefined; + loadSessionForProvider(chatSessionProviderId: string, sessionId: string): Promise; /** * Returns whether the request was accepted. diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index a9f81d447c670..360f1ccc1cbe5 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -32,6 +32,7 @@ import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, import { ChatRequestParser } from './chatRequestParser.js'; import { IChatCompleteResponse, IChatDetail, IChatFollowup, IChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatSendRequestResponseState, IChatService, IChatTransferredSessionData, IChatUserActionEvent } from './chatService.js'; import { ChatServiceTelemetry } from './chatServiceTelemetry.js'; +import { IChatSessionContentProviderService } from './chatSessionContentProviderService.js'; import { ChatSessionStore, IChatTransfer2 } from './chatSessionStore.js'; import { IChatSlashCommandService } from './chatSlashCommands.js'; import { IChatTransferService } from './chatTransferService.js'; @@ -162,6 +163,7 @@ export class ChatService extends Disposable implements IChatService { @IConfigurationService private readonly configurationService: IConfigurationService, @IChatTransferService private readonly chatTransferService: IChatTransferService, @ILanguageModelsService private readonly languageModelsService: ILanguageModelsService, + @IChatSessionContentProviderService private readonly chatSessionContentProviderService: IChatSessionContentProviderService, ) { super(); @@ -528,6 +530,8 @@ export class ChatService extends Disposable implements IChatService { return model; } + return undefined; + let sessionData: ISerializableChatData | undefined; if (!this.useFileStorage || this.transferredSessionData?.sessionId === sessionId) { sessionData = revive(this._persistedSessions[sessionId]); @@ -567,6 +571,10 @@ export class ChatService extends Disposable implements IChatService { return this._startSession(data, data.initialLocation ?? ChatAgentLocation.Panel, true, CancellationToken.None); } + loadSessionForProvider(chatSessionProviderId: string, id: string): Promise { + return this.chatSessionContentProviderService.getChatSession(chatSessionProviderId, id, CancellationToken.None); + } + async resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions): Promise { const model = this._sessionModels.get(request.session.sessionId); if (!model && model !== request.session) { diff --git a/src/vs/workbench/contrib/chat/common/chatSessionContentProviderService.ts b/src/vs/workbench/contrib/chat/common/chatSessionContentProviderService.ts new file mode 100644 index 0000000000000..13e2f7ba8c804 --- /dev/null +++ b/src/vs/workbench/contrib/chat/common/chatSessionContentProviderService.ts @@ -0,0 +1,24 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { IChatModel } from './chatModel.js'; + +export interface IChatSessionContentProvider { + provideChatSessionContent(id: string, token: CancellationToken): Promise; +} + + +export interface IChatSessionContentProviderService { + _serviceBrand: undefined; + registerChatSessionContentProvider(type: string, provider: IChatSessionContentProvider): void; + unregisterChatSessionContentProvider(type: string): void; + getChatSessionContentProvider(type: string): IChatSessionContentProvider | undefined; + + getChatSession(type: string, id: string, token: CancellationToken): Promise; +} + +export const IChatSessionContentProviderService = createDecorator('IChatSessionContentProviderService'); diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts index 99e32b76c0f4f..1d88309566d52 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatService.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatService.ts @@ -46,6 +46,9 @@ export class MockChatService implements IChatService { loadSessionFromContent(data: ISerializableChatData): IChatModel | undefined { throw new Error('Method not implemented.'); } + loadSessionForProvider(chatSessionProviderId: string, sessionId: string): Promise { + throw new Error('Method not implemented.'); + } /** * Returns whether the request was accepted. */ diff --git a/src/vscode-dts/vscode.d.ts b/src/vscode-dts/vscode.d.ts index 16cb98ca7c946..a1a1a7bfa13db 100644 --- a/src/vscode-dts/vscode.d.ts +++ b/src/vscode-dts/vscode.d.ts @@ -19429,7 +19429,7 @@ declare module 'vscode' { /** * @hidden */ - private constructor(prompt: string, command: string | undefined, references: ChatPromptReference[], participant: string, toolReferences: ChatLanguageModelToolReference[]); + constructor(prompt: string, command: string | undefined, references: ChatPromptReference[], participant: string, toolReferences: ChatLanguageModelToolReference[]); } /** @@ -19459,7 +19459,7 @@ declare module 'vscode' { /** * @hidden */ - private constructor(response: ReadonlyArray, result: ChatResult, participant: string); + constructor(response: ReadonlyArray, result: ChatResult, participant: string); } /** diff --git a/src/vscode-dts/vscode.proposed.chatSessionProvider.d.ts b/src/vscode-dts/vscode.proposed.chatSessionProvider.d.ts new file mode 100644 index 0000000000000..10274780482be --- /dev/null +++ b/src/vscode-dts/vscode.proposed.chatSessionProvider.d.ts @@ -0,0 +1,65 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +declare module 'vscode' { + + export interface ChatSessionContentProvider { + + /** + * Resolves a chat session into a full `ChatSession` object. + * + * @param uri The URI of the chat session to open. Uris as structured as `vscode-chat-session:/id` + * @param token A cancellation token that can be used to cancel the operation. + */ + provideChatSessionContent(id: string, token: CancellationToken): Thenable; + } + + export namespace chat { + /** + * @param chatSessionType A unique identifier for the chat session type. This is used to differentiate between different chat session providers. + */ + export function registerChatSessionContentProvider(chatSessionType: string, provider: ChatSessionContentProvider): Disposable; + } + + export namespace window { + /** + * Some API to open a chat session with a given id + */ + export function openChatSession(sessionType: string, id: string): Thenable; + } + + // TODO: Should we call this something like ChatDocument or ChatData? + // TODO: How much control should extensions have? Can we let them modify a chat that is already rendered? + export interface ChatSession { + + /** + * The full history of the session + * + * This should not include any currently active responses + * + * TODO: Are these the right types to use? + * TODO: link request + response to encourage correct usage? + */ + readonly history: ReadonlyArray; + + /** + * Callback invoked by the editor for a currently running response. This allows the session to push items for the + * current response and stream these in as them come in. The current response will be considered complete once the + * callback resolved. + * + * If not provided, the chat session is assumed to not currently be running. + */ + readonly activeResponseCallback?: (stream: ChatResponseStream, token: CancellationToken) => Thenable; + + /** + * Handles new request for the session. + * + * If not set, then the session will be considered read-only and no requests can be made. + * + * TODO: Should we introduce our own type for `ChatRequestHandler` since not all field apply to chat sessions? + */ + readonly requestHandler: ChatRequestHandler | undefined; + } +}