diff --git a/build/lib/i18n.resources.json b/build/lib/i18n.resources.json index c4ac34cba04..92e74dc0760 100644 --- a/build/lib/i18n.resources.json +++ b/build/lib/i18n.resources.json @@ -186,14 +186,6 @@ "name": "vs/workbench/contrib/astNavigation", "project": "vscode-workbench" }, - { - "name": "vs/workbench/contrib/aideChat", - "project": "vscode-workbench" - }, - { - "name": "vs/workbench/contrib/inlineAideChat", - "project": "vscode-workbench" - }, { "name": "vs/workbench/contrib/aideProbe", "project": "vscode-workbench" diff --git a/build/lib/stylelint/vscode-known-variables.json b/build/lib/stylelint/vscode-known-variables.json index d15a832bb05..56aa02ebfb7 100644 --- a/build/lib/stylelint/vscode-known-variables.json +++ b/build/lib/stylelint/vscode-known-variables.json @@ -17,12 +17,6 @@ "--vscode-activityBarTop-dropBorder", "--vscode-activityBarTop-foreground", "--vscode-activityBarTop-inactiveForeground", - "--vscode-aideChat-avatarBackground", - "--vscode-aideChat-avatarForeground", - "--vscode-aideChat-requestBackground", - "--vscode-aideChat-requestBorder", - "--vscode-aideChat-slashCommandBackground", - "--vscode-aideChat-slashCommandForeground", "--vscode-badge-background", "--vscode-badge-foreground", "--vscode-banner-background", @@ -808,7 +802,6 @@ "--vscode-window-inactiveBorder" ], "others": [ - "--size", "--background-dark", "--background-light", "--dropdown-padding-bottom", diff --git a/src/vs/base/common/network.ts b/src/vs/base/common/network.ts index bc07395d961..7f141568abf 100644 --- a/src/vs/base/common/network.ts +++ b/src/vs/base/common/network.ts @@ -84,9 +84,6 @@ export namespace Schemas { /** Scheme used for the chat input editor. */ export const vscodeChatSesssion = 'vscode-chat-editor'; - /** Scheme used for the aide chat input editor. */ - export const vscodeAideChatSesssion = 'vscode-aidechat-editor'; - /** * Scheme used internally for webviews that aren't linked to a resource (i.e. not custom editors) */ diff --git a/src/vs/platform/actions/common/actions.ts b/src/vs/platform/actions/common/actions.ts index cbda4226d90..1e5c24d6abd 100644 --- a/src/vs/platform/actions/common/actions.ts +++ b/src/vs/platform/actions/common/actions.ts @@ -218,14 +218,6 @@ export class MenuId { static readonly MergeInputResultToolbar = new MenuId('MergeToolbarResultToolbar'); static readonly InlineSuggestionToolbar = new MenuId('InlineSuggestionToolbar'); static readonly InlineEditToolbar = new MenuId('InlineEditToolbar'); - static readonly AideChatContext = new MenuId('AideChatContext'); - static readonly AideChatCodeBlock = new MenuId('AideChatCodeblock'); - static readonly AideChatCompareBlock = new MenuId('AideChatCompareBlock'); - static readonly AideChatMessageTitle = new MenuId('AideChatMessageTitle'); - static readonly AideChatExecute = new MenuId('AideChatExecute'); - static readonly AideChatExecutePrimary = new MenuId('AideChatExecutePrimary'); - static readonly AideChatExecuteSecondary = new MenuId('AideChatExecuteSecondary'); - static readonly AideChatInputSide = new MenuId('AideChatInputSide'); static readonly ChatContext = new MenuId('ChatContext'); static readonly ChatCodeBlock = new MenuId('ChatCodeblock'); static readonly ChatCompareBlock = new MenuId('ChatCompareBlock'); diff --git a/src/vs/platform/extensions/common/extensionsApiProposals.ts b/src/vs/platform/extensions/common/extensionsApiProposals.ts index 4e98e19565b..e7a2eb1b9e5 100644 --- a/src/vs/platform/extensions/common/extensionsApiProposals.ts +++ b/src/vs/platform/extensions/common/extensionsApiProposals.ts @@ -18,12 +18,6 @@ const _allApiProposals = { aiTextSearchProviderNew: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aiTextSearchProviderNew.d.ts', }, - aideChatParticipant: { - proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aideChatParticipant.d.ts', - }, - aideChatVariableResolver: { - proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aideChatVariableResolver.d.ts', - }, aideGetCodeLensProvider: { proposal: 'https://raw.githubusercontent.com/microsoft/vscode/main/src/vscode-dts/vscode.proposed.aideGetCodeLensProvider.d.ts', }, diff --git a/src/vs/workbench/api/browser/extensionHost.contribution.ts b/src/vs/workbench/api/browser/extensionHost.contribution.ts index 62507293a10..c56c311ea4c 100644 --- a/src/vs/workbench/api/browser/extensionHost.contribution.ts +++ b/src/vs/workbench/api/browser/extensionHost.contribution.ts @@ -18,8 +18,6 @@ import { StatusBarItemsExtensionPoint } from './statusBarExtensionPoint.js'; import './mainThreadLocalization.js'; import './mainThreadBulkEdits.js'; import './mainThreadModelSelection.js'; -import './mainThreadAideChatAgents2.js'; -import './mainThreadAideChatVariables.js'; import './mainThreadAideProbeProvider.js'; import './mainThreadLanguageModels.js'; import './mainThreadChatAgents2.js'; diff --git a/src/vs/workbench/api/browser/mainThreadAideChatAgents2.ts b/src/vs/workbench/api/browser/mainThreadAideChatAgents2.ts deleted file mode 100644 index 66075bc8d75..00000000000 --- a/src/vs/workbench/api/browser/mainThreadAideChatAgents2.ts +++ /dev/null @@ -1,327 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { DeferredPromise } from '../../../base/common/async.js'; -import { CancellationToken } from '../../../base/common/cancellation.js'; -import { Emitter, Event } from '../../../base/common/event.js'; -import { IMarkdownString } from '../../../base/common/htmlContent.js'; -import { Disposable, DisposableMap, IDisposable } from '../../../base/common/lifecycle.js'; -import { revive } from '../../../base/common/marshalling.js'; -import { escapeRegExpCharacters } from '../../../base/common/strings.js'; -import { ThemeIcon } from '../../../base/common/themables.js'; -import { URI, UriComponents } from '../../../base/common/uri.js'; -import { Position } from '../../../editor/common/core/position.js'; -import { Range } from '../../../editor/common/core/range.js'; -import { getWordAtText } from '../../../editor/common/core/wordHelper.js'; -import { CompletionContext, CompletionItem, CompletionItemKind, CompletionList } from '../../../editor/common/languages.js'; -import { ITextModel } from '../../../editor/common/model.js'; -import { ILanguageFeaturesService } from '../../../editor/common/services/languageFeatures.js'; -import { ICSAccountService } from '../../../platform/codestoryAccount/common/csAccount.js'; -import { ExtensionIdentifier } from '../../../platform/extensions/common/extensions.js'; -import { IInstantiationService } from '../../../platform/instantiation/common/instantiation.js'; -import { ILogService } from '../../../platform/log/common/log.js'; -import { IAideChatWidgetService } from '../../contrib/aideChat/browser/aideChat.js'; -import { AideChatAgentLocation, IAideChatAgentService, IChatAgentImplementation } from '../../contrib/aideChat/common/aideChatAgents.js'; -import { ChatRequestAgentPart } from '../../contrib/aideChat/common/aideChatParserTypes.js'; -import { ChatRequestParser } from '../../contrib/aideChat/common/aideChatRequestParser.js'; -import { IAideChatContentReference, IAideChatFollowup, IAideChatProgress, IAideChatService, IAideChatTask, IAideChatWarningMessage } from '../../contrib/aideChat/common/aideChatService.js'; -import { ChatInputPart } from '../../contrib/aideChat/browser/aideChatInputPart.js'; -import { AddDynamicVariableAction, IAddDynamicVariableContext } from '../../contrib/aideChat/browser/contrib/aideChatDynamicVariables.js'; -import { IExtensionService } from '../../services/extensions/common/extensions.js'; -import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; -import { ExtHostAideChatAgentsShape2, ExtHostContext, IChatProgressDto, IDynamicChatAgentProps, IExtensionChatAgentMetadata, MainContext, MainThreadAideChatAgentsShape2 } from '../common/extHost.protocol.js'; - -interface AgentData { - dispose: () => void; - id: string; - extensionId: ExtensionIdentifier; - hasFollowups?: boolean; -} - -class MainThreadChatTask implements IAideChatTask { - public readonly kind = 'progressTask'; - - public readonly deferred = new DeferredPromise(); - - private readonly _onDidAddProgress = new Emitter(); - public get onDidAddProgress(): Event { return this._onDidAddProgress.event; } - - public readonly progress: (IAideChatWarningMessage | IAideChatContentReference)[] = []; - - constructor(public content: IMarkdownString) { } - - task() { - return this.deferred.p; - } - - isSettled() { - return this.deferred.isSettled; - } - - complete(v: string | void) { - this.deferred.complete(v); - } - - add(progress: IAideChatWarningMessage | IAideChatContentReference): void { - this.progress.push(progress); - this._onDidAddProgress.fire(progress); - } -} - -@extHostNamedCustomer(MainContext.MainThreadAideChatAgents2) -export class MainThreadAideChatAgents2 extends Disposable implements MainThreadAideChatAgentsShape2 { - - private readonly _agents = this._register(new DisposableMap()); - private readonly _agentCompletionProviders = this._register(new DisposableMap()); - private readonly _agentIdsToCompletionProviders = this._register(new DisposableMap); - - private readonly _pendingProgress = new Map void>(); - private readonly _proxy: ExtHostAideChatAgentsShape2; - - private _responsePartHandlePool = 0; - private readonly _activeTasks = new Map(); - - constructor( - extHostContext: IExtHostContext, - @IAideChatAgentService private readonly _chatAgentService: IAideChatAgentService, - @IAideChatService private readonly _chatService: IAideChatService, - @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, - @IAideChatWidgetService private readonly _chatWidgetService: IAideChatWidgetService, - @IInstantiationService private readonly _instantiationService: IInstantiationService, - @ILogService private readonly _logService: ILogService, - @IExtensionService private readonly _extensionService: IExtensionService, - @ICSAccountService private readonly _csAccountService: ICSAccountService, - ) { - super(); - this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostAideChatAgents2); - - this._register(this._chatService.onDidDisposeSession(e => { - this._proxy.$releaseSession(e.sessionId); - })); - this._register(this._chatService.onDidPerformUserAction(e => { - if (typeof e.agentId === 'string') { - for (const [handle, agent] of this._agents) { - if (agent.id === e.agentId) { - if (e.action.kind === 'vote') { - this._proxy.$acceptFeedback(handle, e.result ?? {}, e.action.direction); - } else { - this._proxy.$acceptAction(handle, e.result || {}, e); - } - break; - } - } - } - })); - } - - $unregisterAgent(handle: number): void { - this._agents.deleteAndDispose(handle); - } - - $transferActiveChatSession(toWorkspace: UriComponents): void { - const widget = this._chatWidgetService.lastFocusedWidget; - const sessionId = widget?.viewModel?.model.sessionId; - if (!sessionId) { - this._logService.error(`MainThreadChat#$transferActiveChatSession: No active chat session found`); - return; - } - - const inputValue = widget?.inputEditor.getValue() ?? ''; - this._chatService.transferChatSession({ sessionId, inputValue }, URI.revive(toWorkspace)); - } - - $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionChatAgentMetadata, dynamicProps: IDynamicChatAgentProps | undefined): void { - const staticAgentRegistration = this._chatAgentService.getAgent(id); - if (!staticAgentRegistration && !dynamicProps) { - if (this._chatAgentService.getAgentsByName(id).length) { - // Likely some extension authors will not adopt the new ID, so give a hint if they register a - // participant by name instead of ID. - throw new Error(`chatParticipant must be declared with an ID in package.json. The "id" property may be missing! "${id}"`); - } - - throw new Error(`chatParticipant must be declared in package.json: ${id}`); - } - - const impl: IChatAgentImplementation = { - invoke: async (request, progress, history, token) => { - const authenticated = await this._csAccountService.ensureAuthenticated(); - if (!authenticated) { - return {}; - } - - this._pendingProgress.set(request.requestId, progress); - try { - return await this._proxy.$invokeAgent(handle, request, { history }, token) ?? {}; - } finally { - this._pendingProgress.delete(request.requestId); - } - }, - provideFollowups: async (request, result, history, token): Promise => { - if (!this._agents.get(handle)?.hasFollowups) { - return []; - } - - return this._proxy.$provideFollowups(request, handle, result, { history }, token); - }, - provideWelcomeMessage: (location: AideChatAgentLocation, token: CancellationToken) => { - return this._proxy.$provideWelcomeMessage(handle, location, token); - }, - provideSampleQuestions: (location: AideChatAgentLocation, token: CancellationToken) => { - return this._proxy.$provideSampleQuestions(handle, location, token); - } - }; - - let disposable: IDisposable; - if (!staticAgentRegistration && dynamicProps) { - const extensionDescription = this._extensionService.extensions.find(e => ExtensionIdentifier.equals(e.identifier, extension)); - disposable = this._chatAgentService.registerDynamicAgent( - { - id, - name: dynamicProps.name ?? '', // This case is for an API change and can be removed tomorrow - description: dynamicProps.description, - extensionId: extension, - extensionDisplayName: extensionDescription?.displayName ?? extension.value, - extensionPublisherId: extensionDescription?.publisher ?? '', - publisherDisplayName: dynamicProps.publisherName, - fullName: dynamicProps.fullName, - metadata: revive(metadata), - slashCommands: [], - locations: [AideChatAgentLocation.Panel] // TODO all dynamic participants are panel only? - }, - impl); - } else { - disposable = this._chatAgentService.registerAgentImplementation(id, impl); - } - - this._agents.set(handle, { - id: id, - extensionId: extension, - dispose: disposable.dispose, - hasFollowups: metadata.hasFollowups - }); - } - - $updateAgent(handle: number, metadataUpdate: IExtensionChatAgentMetadata): void { - const data = this._agents.get(handle); - if (!data) { - throw new Error(`No agent with handle ${handle} registered`); - } - data.hasFollowups = metadataUpdate.hasFollowups; - this._chatAgentService.updateAgent(data.id, revive(metadataUpdate)); - } - - async $handleProgressChunk(requestId: string, progress: IChatProgressDto, responsePartHandle?: number): Promise { - const revivedProgress = revive(progress) as IAideChatProgress; - if (revivedProgress.kind === 'progressTask') { - const handle = ++this._responsePartHandlePool; - const responsePartId = `${requestId}_${handle}`; - const task = new MainThreadChatTask(revivedProgress.content); - this._activeTasks.set(responsePartId, task); - this._pendingProgress.get(requestId)?.(task); - return handle; - } else if (responsePartHandle !== undefined) { - const responsePartId = `${requestId}_${responsePartHandle}`; - const task = this._activeTasks.get(responsePartId); - switch (revivedProgress.kind) { - case 'progressTaskResult': - if (task && revivedProgress.content) { - task.complete(revivedProgress.content.value); - this._activeTasks.delete(responsePartId); - } else { - task?.complete(undefined); - } - return responsePartHandle; - case 'warning': - case 'reference': - task?.add(revivedProgress); - return; - } - } - this._pendingProgress.get(requestId)?.(revivedProgress); - } - - $registerAgentCompletionsProvider(handle: number, id: string, triggerCharacters: string[]): void { - const provide = async (query: string, token: CancellationToken) => { - const completions = await this._proxy.$invokeCompletionProvider(handle, query, token); - return completions.map((c) => ({ ...c, icon: c.icon ? ThemeIcon.fromId(c.icon) : undefined })); - }; - this._agentIdsToCompletionProviders.set(id, this._chatAgentService.registerAgentCompletionProvider(id, provide)); - - this._agentCompletionProviders.set(handle, this._languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatAgentCompletions:' + handle, - triggerCharacters, - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { - const widget = this._chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.viewModel) { - return; - } - - const triggerCharsPart = triggerCharacters.map(c => escapeRegExpCharacters(c)).join(''); - const wordRegex = new RegExp(`[${triggerCharsPart}]\\S*`, 'g'); - const query = getWordAtText(position.column, wordRegex, model.getLineContent(position.lineNumber), 0)?.word ?? ''; - - if (query && !triggerCharacters.some(c => query.startsWith(c))) { - return; - } - - const parsedRequest = this._instantiationService.createInstance(ChatRequestParser).parseChatRequest(widget.viewModel.sessionId, model.getValue()).parts; - const agentPart = parsedRequest.find((part): part is ChatRequestAgentPart => part instanceof ChatRequestAgentPart); - const thisAgentId = this._agents.get(handle)?.id; - if (agentPart?.agent.id !== thisAgentId) { - return; - } - - const range = computeCompletionRanges(model, position, wordRegex); - if (!range) { - return null; - } - - const result = await provide(query, token); - const variableItems = result.map(v => { - const insertText = v.insertText ?? (typeof v.label === 'string' ? v.label : v.label.label); - const rangeAfterInsert = new Range(range.insert.startLineNumber, range.insert.startColumn, range.insert.endLineNumber, range.insert.startColumn + insertText.length); - return { - label: v.label, - range, - insertText: insertText + ' ', - kind: CompletionItemKind.Text, - detail: v.detail, - documentation: v.documentation, - command: { id: AddDynamicVariableAction.ID, title: '', arguments: [{ id: v.id, widget, range: rangeAfterInsert, variableData: revive(v.value) as any, command: v.command } satisfies IAddDynamicVariableContext] } - } satisfies CompletionItem; - }); - - return { - suggestions: variableItems - } satisfies CompletionList; - } - })); - } - - $unregisterAgentCompletionsProvider(handle: number, id: string): void { - this._agentCompletionProviders.deleteAndDispose(handle); - this._agentIdsToCompletionProviders.deleteAndDispose(id); - } -} - - -function computeCompletionRanges(model: ITextModel, position: Position, reg: RegExp): { insert: Range; replace: Range } | undefined { - const varWord = getWordAtText(position.column, reg, model.getLineContent(position.lineNumber), 0); - if (!varWord && model.getWordUntilPosition(position).word) { - // inside a "normal" word - return; - } - - let insert: Range; - let replace: Range; - if (!varWord) { - insert = replace = Range.fromPositions(position); - } else { - insert = new Range(position.lineNumber, varWord.startColumn, position.lineNumber, position.column); - replace = new Range(position.lineNumber, varWord.startColumn, position.lineNumber, varWord.endColumn); - } - - return { insert, replace }; -} diff --git a/src/vs/workbench/api/browser/mainThreadAideChatVariables.ts b/src/vs/workbench/api/browser/mainThreadAideChatVariables.ts deleted file mode 100644 index 51dd122d6cc..00000000000 --- a/src/vs/workbench/api/browser/mainThreadAideChatVariables.ts +++ /dev/null @@ -1,50 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { DisposableMap } from '../../../base/common/lifecycle.js'; -import { revive } from '../../../base/common/marshalling.js'; -import { IAideChatRequestVariableValue, IAideChatVariableData, IAideChatVariableResolverProgress, IAideChatVariablesService } from '../../contrib/aideChat/common/aideChatVariables.js'; -import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; -import { ExtHostAideChatVariablesShape, ExtHostContext, IChatVariableResolverProgressDto, MainContext, MainThreadAideChatVariablesShape } from '../common/extHost.protocol.js'; - -@extHostNamedCustomer(MainContext.MainThreadAideChatVariables) -export class MainThreadAideChatVariables implements MainThreadAideChatVariablesShape { - - private readonly _proxy: ExtHostAideChatVariablesShape; - private readonly _variables = new DisposableMap(); - private readonly _pendingProgress = new Map void>(); - - constructor( - extHostContext: IExtHostContext, - @IAideChatVariablesService private readonly _chatVariablesService: IAideChatVariablesService, - ) { - this._proxy = extHostContext.getProxy(ExtHostContext.ExtHostChatVariables); - } - - dispose(): void { - this._variables.clearAndDisposeAll(); - } - - $registerVariable(handle: number, data: IAideChatVariableData): void { - const registration = this._chatVariablesService.registerVariable(data, async (messageText, _arg, model, progress, token) => { - const varRequestId = `${model.sessionId}-${handle}`; - this._pendingProgress.set(varRequestId, progress); - const result = revive(await this._proxy.$resolveVariable(handle, varRequestId, messageText, token)); - - this._pendingProgress.delete(varRequestId); - return result as any; // 'revive' type signature doesn't like this type for some reason - }); - this._variables.set(handle, registration); - } - - async $handleProgressChunk(requestId: string, progress: IChatVariableResolverProgressDto): Promise { - const revivedProgress = revive(progress); - this._pendingProgress.get(requestId)?.(revivedProgress as IAideChatVariableResolverProgress); - } - - $unregisterVariable(handle: number): void { - this._variables.deleteAndDispose(handle); - } -} diff --git a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts index 8049eca31ae..9d0d3555b66 100644 --- a/src/vs/workbench/api/browser/mainThreadEditorTabs.ts +++ b/src/vs/workbench/api/browser/mainThreadEditorTabs.ts @@ -28,7 +28,6 @@ import { columnToEditorGroup, EditorGroupColumn, editorGroupToColumn } from '../ import { GroupDirection, IEditorGroup, IEditorGroupsService, preferredSideBySideGroupDirection } from '../../services/editor/common/editorGroupsService.js'; import { IEditorsChangeEvent, IEditorService, SIDE_GROUP } from '../../services/editor/common/editorService.js'; import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js'; -import { AideChatEditorInput } from '../../contrib/aideChat/browser/aideChatEditorInput.js'; interface TabInfo { tab: IEditorTabDto; @@ -205,12 +204,6 @@ export class MainThreadEditorTabs implements MainThreadEditorTabsShape { }; } - if (editor instanceof AideChatEditorInput) { - return { - kind: TabInputKind.AideChatEditorInput, - }; - } - if (editor instanceof MultiDiffEditorInput) { const diffEditors: TextDiffInputDto[] = []; for (const resource of (editor?.resources.get() ?? [])) { diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index cf9fa9a1657..78c74fe9bf1 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -109,8 +109,6 @@ import { ProxyIdentifier } from '../../services/extensions/common/proxyIdentifie import { ExcludeSettingOptions, TextSearchCompleteMessageType, TextSearchContextNew, TextSearchMatchNew } from '../../services/search/common/searchExtTypes.js'; import type * as vscode from 'vscode'; import { ExtHostCodeMapper } from './extHostCodeMapper.js'; -import { ExtHostAideChatAgents2 } from './extHostAideChatAgents2.js'; -import { ExtHostAideChatVariables } from './extHostAideChatVariables.js'; import { ExtHostAideProbeProvider } from './extHostAideProbeProvider.js'; import { IExtHostCSAuthentication } from './extHostCSAuthentication.js'; import { ExtHostCSEvents } from './extHostCSEvents.js'; @@ -223,8 +221,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I const extHostChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostChatAgents2, new ExtHostChatAgents2(rpcProtocol, extHostLogService, extHostCommands, extHostDocuments)); const extHostChatVariables = rpcProtocol.set(ExtHostContext.ExtHostChatVariables, new ExtHostChatVariables(rpcProtocol)); const extHostLanguageModelTools = rpcProtocol.set(ExtHostContext.ExtHostLanguageModelTools, new ExtHostLanguageModelTools(rpcProtocol)); - const extHostAideChatAgents2 = rpcProtocol.set(ExtHostContext.ExtHostAideChatAgents2, new ExtHostAideChatAgents2(rpcProtocol, extHostLogService, extHostCommands, initData.quality)); - const extHostAideChatVariables = rpcProtocol.set(ExtHostContext.ExtHostAideChatVariables, new ExtHostAideChatVariables(rpcProtocol)); const extHostAideProbeProvider = rpcProtocol.set(ExtHostContext.ExtHostAideProbeProvider, new ExtHostAideProbeProvider(rpcProtocol)); const extHostAiRelatedInformation = rpcProtocol.set(ExtHostContext.ExtHostAiRelatedInformation, new ExtHostRelatedInformation(rpcProtocol)); const extHostAiEmbeddingVector = rpcProtocol.set(ExtHostContext.ExtHostAiEmbeddingVector, new ExtHostAiEmbeddingVector(rpcProtocol)); @@ -1539,26 +1535,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I }, }; - // namespace: aideChat - const aideChat: typeof vscode.aideChat = { - // IMPORTANT - // this needs to be updated whenever the API proposal changes and breaks backwards compatibility - _version: 1, - - registerChatVariableResolver(id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: vscode.ChatVariableResolver, fullName?: string, icon?: vscode.ThemeIcon) { - checkProposedApiEnabled(extension, 'aideChatVariableResolver'); - return extHostAideChatVariables.registerVariableResolver(extension, id, name, userDescription, modelDescription, isSlow, resolver, fullName, icon?.id); - }, - createChatParticipant(id: string, handler: vscode.AideChatExtendedRequestHandler) { - checkProposedApiEnabled(extension, 'aideChatParticipant'); - return extHostAideChatAgents2.createChatAgent(extension, id, handler); - }, - createDynamicChatParticipant(id: string, dynamicProps: vscode.DynamicChatParticipantProps, handler: vscode.AideChatExtendedRequestHandler): vscode.AideChatParticipant { - checkProposedApiEnabled(extension, 'aideChatParticipant'); - return extHostAideChatAgents2.createDynamicChatAgent(extension, id, dynamicProps, handler); - } - }; - const aideProbe: typeof vscode.aideProbe = { // IMPORTANT // this needs to be updated whenever the API proposal changes and breaks backwards compatibility @@ -1602,7 +1578,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I version: initData.version, // namespaces ai, - aideChat, aideProbe, authentication, commands, @@ -1628,9 +1603,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I // types Breakpoint: extHostTypes.Breakpoint, TerminalOutputAnchor: extHostTypes.TerminalOutputAnchor, - AideChatResultFeedbackKind: extHostTypes.AideChatResultFeedbackKind, - AideChatVariableLevel: extHostTypes.AideChatVariableLevel, - AideChatCompletionItem: extHostTypes.AideChatCompletionItem, ChatResultFeedbackKind: extHostTypes.ChatResultFeedbackKind, ChatVariableLevel: extHostTypes.ChatVariableLevel, ChatCompletionItem: extHostTypes.ChatCompletionItem, @@ -1833,7 +1805,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I LogLevel: LogLevel, EditSessionIdentityMatch: EditSessionIdentityMatch, InteractiveSessionVoteDirection: extHostTypes.InteractiveSessionVoteDirection, - AideChatCopyKind: extHostTypes.AideChatCopyKind, ChatCopyKind: extHostTypes.ChatCopyKind, InteractiveEditorResponseFeedbackKind: extHostTypes.InteractiveEditorResponseFeedbackKind, DebugStackFrame: extHostTypes.DebugStackFrame, @@ -1844,7 +1815,6 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I TextToSpeechStatus: extHostTypes.TextToSpeechStatus, PartialAcceptTriggerKind: extHostTypes.PartialAcceptTriggerKind, KeywordRecognitionStatus: extHostTypes.KeywordRecognitionStatus, - AideChatLocation: extHostTypes.AideChatLocation, ChatResponseMarkdownPart: extHostTypes.ChatResponseMarkdownPart, ChatResponseFileTreePart: extHostTypes.ChatResponseFileTreePart, ChatResponseAnchorPart: extHostTypes.ChatResponseAnchorPart, diff --git a/src/vs/workbench/api/common/extHost.protocol.ts b/src/vs/workbench/api/common/extHost.protocol.ts index c9d19b3f5cc..a36141cc99b 100644 --- a/src/vs/workbench/api/common/extHost.protocol.ts +++ b/src/vs/workbench/api/common/extHost.protocol.ts @@ -3,7 +3,6 @@ * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { CSAuthenticationSession, SymbolNavigationEvent } from 'vscode'; import { VSBuffer } from '../../../base/common/buffer.js'; import { CancellationToken } from '../../../base/common/cancellation.js'; import { IRemoteConsoleLog } from '../../../base/common/console.js'; @@ -26,11 +25,12 @@ import { StandardTokenType } from '../../../editor/common/encodedTokenAttributes import * as languages from '../../../editor/common/languages.js'; import { CharacterPair, CommentRule, EnterAction } from '../../../editor/common/languages/languageConfiguration.js'; import { EndOfLineSequence } from '../../../editor/common/model.js'; -import { AgentCodeEditEvent } from '../../../editor/common/model/csEvents.js'; +import { AgentCodeEditEvent, SymbolNavigationEvent } from '../../../editor/common/model/csEvents.js'; import { IModelChangedEvent } from '../../../editor/common/model/mirrorTextModel.js'; import { IAccessibilityInformation } from '../../../platform/accessibility/common/accessibility.js'; import { ILocalizedString } from '../../../platform/action/common/action.js'; import { IModelSelectionSettings } from '../../../platform/aiModel/common/aiModels.js'; +import { CSAuthenticationSession } from '../../../platform/codestoryAccount/common/csAccount.js'; import { ConfigurationTarget, IConfigurationChange, IConfigurationData, IConfigurationOverrides } from '../../../platform/configuration/common/configuration.js'; import { ConfigurationScope } from '../../../platform/configuration/common/configurationRegistry.js'; import { IExtensionIdWithVersion } from '../../../platform/extensionManagement/common/extensionStorage.js'; @@ -52,16 +52,12 @@ import { EditSessionIdentityMatch } from '../../../platform/workspace/common/edi import { WorkspaceTrustRequestOptions } from '../../../platform/workspace/common/workspaceTrust.js'; import { SaveReason } from '../../common/editor.js'; import { IRevealOptions, ITreeItem, IViewBadge } from '../../common/views.js'; -import { IAideChatAgentMetadata, IAideChatAgentRequest, IAideChatAgentResult, AideChatAgentLocation } from '../../contrib/aideChat/common/aideChatAgents.js'; -import { IAideChatProgressResponseContent } from '../../contrib/aideChat/common/aideChatModel.js'; -import { IAideChatTask, IAideChatTaskDto, IAideChatFollowup, AideChatAgentVoteDirection, IAideChatUserActionEvent, IAideChatResponseErrorDetails, IAideChatProgress, IAideChatMarkdownContent } from '../../contrib/aideChat/common/aideChatService.js'; -import { IAideChatVariableResolverProgress, IAideChatVariableData, IAideChatRequestVariableValue } from '../../contrib/aideChat/common/aideChatVariables.js'; import { IAideProbeTextEdit, IAideProbeIterationFinished, IAideProbeBreakdownContent, IAideProbeGoToDefinition, IAideProbeOpenFile, IAideProbeRepoMapGeneration, IAideProbeLongContextSearch, IAideProbeInitialSymbols, IAideReferenceFound, IAideRelevantReference, IAideFollowups, IAideProbeData, IAideProbeRequestModel, IAideProbeResult, IAideProbeSessionAction, IAideProbeUserAction } from '../../contrib/aideProbe/common/aideProbe.js'; import { CallHierarchyItem } from '../../contrib/callHierarchy/common/callHierarchy.js'; import { ChatAgentLocation, IChatAgentMetadata, IChatAgentRequest, IChatAgentResult } from '../../contrib/chat/common/chatAgents.js'; import { ICodeMapperRequest, ICodeMapperResult } from '../../contrib/chat/common/chatCodeMapperService.js'; import { IChatProgressResponseContent } from '../../contrib/chat/common/chatModel.js'; -import { IChatFollowup, IChatProgress, IChatResponseErrorDetails, IChatTask, IChatTaskDto, IChatUserActionEvent, IChatVoteAction } from '../../contrib/chat/common/chatService.js'; +import { IChatFollowup, IChatMarkdownContent, IChatProgress, IChatResponseErrorDetails, IChatTask, IChatTaskDto, IChatUserActionEvent, IChatVoteAction } from '../../contrib/chat/common/chatService.js'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolverProgress } from '../../contrib/chat/common/chatVariables.js'; import { IChatMessage, IChatResponseFragment, ILanguageModelChatMetadata, ILanguageModelChatSelector, ILanguageModelsChangeEvent } from '../../contrib/chat/common/languageModels.js'; import { IToolData, IToolInvocation, IToolResult } from '../../contrib/chat/common/languageModelToolsService.js'; @@ -747,7 +743,6 @@ export const enum TabInputKind { TerminalEditorInput, InteractiveEditorInput, ChatEditorInput, - AideChatEditorInput, MultiDiffEditorInput } @@ -815,10 +810,6 @@ export interface ChatEditorInputDto { kind: TabInputKind.ChatEditorInput; } -export interface AideChatEditorInputDto { - kind: TabInputKind.AideChatEditorInput; -} - export interface MultiDiffEditorInputDto { kind: TabInputKind.MultiDiffEditorInput; diffEditors: TextDiffInputDto[]; @@ -828,7 +819,7 @@ export interface TabInputDto { kind: TabInputKind.TerminalEditorInput; } -export type AnyInputDto = UnknownInputDto | TextInputDto | TextDiffInputDto | MultiDiffEditorInputDto | TextMergeInputDto | NotebookInputDto | NotebookDiffInputDto | CustomInputDto | WebviewInputDto | InteractiveEditorInputDto | ChatEditorInputDto | AideChatEditorInputDto | TabInputDto; +export type AnyInputDto = UnknownInputDto | TextInputDto | TextDiffInputDto | MultiDiffEditorInputDto | TextMergeInputDto | NotebookInputDto | NotebookDiffInputDto | CustomInputDto | WebviewInputDto | InteractiveEditorInputDto | ChatEditorInputDto | TabInputDto; export interface MainThreadEditorTabsShape extends IDisposable { // manage tabs: move, close, rearrange etc @@ -1431,111 +1422,9 @@ export type IChatProgressDto = ///////////////////////// END CHAT ///////////////////////// ///////////////////////// START AIDE ///////////////////////// -export interface IExtensionAideChatAgentMetadata extends Dto { - hasFollowups?: boolean; -} - -export interface IDynamicAideChatAgentProps { - name: string; - publisherName: string; - description?: string; - fullName?: string; -} - -export interface MainThreadAideChatAgentsShape2 extends IDisposable { - $registerAgent(handle: number, extension: ExtensionIdentifier, id: string, metadata: IExtensionAideChatAgentMetadata, dynamicProps: IDynamicAideChatAgentProps | undefined): void; - $registerAgentCompletionsProvider(handle: number, id: string, triggerCharacters: string[]): void; - $unregisterAgentCompletionsProvider(handle: number, id: string): void; - $updateAgent(handle: number, metadataUpdate: IExtensionAideChatAgentMetadata): void; - $unregisterAgent(handle: number): void; - $handleProgressChunk(requestId: string, chunk: IAideChatProgressDto, handle?: number): Promise; - - $transferActiveChatSession(toWorkspace: UriComponents): void; -} - -export interface IAideChatAgentCompletionItem { - id: string; - fullName?: string; - icon?: string; - insertText?: string; - label: string | languages.CompletionItemLabel; - value: IAideChatRequestVariableValueDto; - detail?: string; - documentation?: string | IMarkdownString; - command?: ICommandDto; -} - -export type IAideChatContentProgressDto = - | Dto> - | IAideChatTaskDto; - -export type IAideChatAgentHistoryEntryDto = { - request: IAideChatAgentRequest; - response: ReadonlyArray; - result: IAideChatAgentResult; -}; - -export interface ExtHostAideChatAgentsShape2 { - $invokeAgent(handle: number, request: IAideChatAgentRequest, context: { history: IAideChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; - $provideFollowups(request: IAideChatAgentRequest, handle: number, result: IAideChatAgentResult, context: { history: IAideChatAgentHistoryEntryDto[] }, token: CancellationToken): Promise; - $acceptFeedback(handle: number, result: IAideChatAgentResult, vote: AideChatAgentVoteDirection, reportIssue?: boolean): void; - $acceptAction(handle: number, result: IAideChatAgentResult, action: IAideChatUserActionEvent): void; - $invokeCompletionProvider(handle: number, query: string, token: CancellationToken): Promise; - $provideWelcomeMessage(handle: number, location: AideChatAgentLocation, token: CancellationToken): Promise<(string | IMarkdownString)[] | undefined>; - $provideSampleQuestions(handle: number, location: AideChatAgentLocation, token: CancellationToken): Promise; - $releaseSession(sessionId: string): void; -} - -export type IAideChatVariableResolverProgressDto = - | Dto; - -export interface MainThreadAideChatVariablesShape extends IDisposable { - $registerVariable(handle: number, data: IAideChatVariableData): void; - $handleProgressChunk(requestId: string, progress: IAideChatVariableResolverProgressDto): Promise; - $unregisterVariable(handle: number): void; -} - -export type IAideChatRequestVariableValueDto = Dto; - -export interface ExtHostAideChatVariablesShape { - $resolveVariable(handle: number, requestId: string, messageText: string, token: CancellationToken): Promise; -} - -export interface MainThreadUrlsShape extends IDisposable { - $registerUriHandler(handle: number, extensionId: ExtensionIdentifier, extensionDisplayName: string): Promise; - $unregisterUriHandler(handle: number): Promise; - $createAppUri(uri: UriComponents): Promise; -} - -export interface IAideChatDto { -} - -export interface IAideChatRequestDto { - message: string; - variables?: Record; -} - -export interface IAideChatResponseDto { - errorDetails?: IAideChatResponseErrorDetails; - timings: { - firstProgress: number; - totalElapsed: number; - }; -} - -export interface IAideChatResponseProgressFileTreeData { - label: string; - uri: URI; - children?: IAideChatResponseProgressFileTreeData[]; -} - -export type IAideChatProgressDto = - | Dto> - | IAideChatTaskDto; - export type IAideProbeTextEditDto = Omit, 'edits'> & { edits: IWorkspaceEditDto }; export type IAideProbeIterationFinishedDto = Omit, 'edits'> & { edits: IWorkspaceEditDto }; -export type IAideProbeProgressDto = Dto | IAideProbeTextEditDto | IAideProbeIterationFinishedDto; +export type IAideProbeProgressDto = Dto | IAideProbeTextEditDto | IAideProbeIterationFinishedDto; export interface MainThreadAideProbeProviderShape extends IDisposable { $registerProbingProvider(handle: number, data: IAideProbeData): void; @@ -1548,7 +1437,6 @@ export interface ExtHostAideProbeProviderShape { $onSessionAction(handle: number, action: IAideProbeSessionAction): Promise; $onUserAction(handle: number, action: IAideProbeUserAction): Promise; } - ///////////////////////// END AIDE ///////////////////////// export interface ExtHostUrlsShape { @@ -3076,8 +2964,6 @@ export const MainContext = { MainThreadBulkEdits: createProxyIdentifier('MainThreadBulkEdits'), MainThreadLanguageModels: createProxyIdentifier('MainThreadLanguageModels'), MainThreadEmbeddings: createProxyIdentifier('MainThreadEmbeddings'), - MainThreadAideChatAgents2: createProxyIdentifier('MainThreadAideChatAgents2'), - MainThreadAideChatVariables: createProxyIdentifier('MainThreadAideChatVariables'), MainThreadProbeProvider: createProxyIdentifier('MainThreadProbeProvider'), MainThreadChatAgents2: createProxyIdentifier('MainThreadChatAgents2'), MainThreadCodeMapper: createProxyIdentifier('MainThreadCodeMapper'), @@ -3208,8 +3094,6 @@ export const ExtHostContext = { ExtHostChatVariables: createProxyIdentifier('ExtHostChatVariables'), ExtHostLanguageModelTools: createProxyIdentifier('ExtHostChatSkills'), ExtHostChatProvider: createProxyIdentifier('ExtHostChatProvider'), - ExtHostAideChatAgents2: createProxyIdentifier('ExtHostAideChatAgents'), - ExtHostAideChatVariables: createProxyIdentifier('ExtHostAideChatVariables'), ExtHostAideProbeProvider: createProxyIdentifier('ExtHostAideProbeProvider'), ExtHostSpeech: createProxyIdentifier('ExtHostSpeech'), ExtHostEmbeddings: createProxyIdentifier('ExtHostEmbeddings'), diff --git a/src/vs/workbench/api/common/extHostAideChatAgents2.ts b/src/vs/workbench/api/common/extHostAideChatAgents2.ts deleted file mode 100644 index cf570d05391..00000000000 --- a/src/vs/workbench/api/common/extHostAideChatAgents2.ts +++ /dev/null @@ -1,761 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { raceCancellation } from '../../../base/common/async.js'; -import { toErrorMessage } from '../../../base/common/errorMessage.js'; -import { IMarkdownString } from '../../../base/common/htmlContent.js'; -import { Iterable } from '../../../base/common/iterator.js'; -import { Disposable, DisposableMap, DisposableStore } from '../../../base/common/lifecycle.js'; -import { StopWatch } from '../../../base/common/stopwatch.js'; -import { assertType } from '../../../base/common/types.js'; -import { URI } from '../../../base/common/uri.js'; -import { ExtensionIdentifier, IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; -import { ILogService } from '../../../platform/log/common/log.js'; -import { AideChatAgentLocation, IAideChatAgentRequest, IAideChatAgentResult } from '../../contrib/aideChat/common/aideChatAgents.js'; -import { AideChatAgentVoteDirection, IAideChatContentReference, IAideChatFollowup, IAideChatResponseErrorDetails, IAideChatUserActionEvent } from '../../contrib/aideChat/common/aideChatService.js'; -import { checkProposedApiEnabled, isProposedApiEnabled } from '../../services/extensions/common/extensions.js'; -import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; -import { ExtHostAideChatAgentsShape2, IAideChatAgentCompletionItem, IAideChatAgentHistoryEntryDto, IAideChatProgressDto, IExtensionAideChatAgentMetadata, IMainContext, MainContext, MainThreadAideChatAgentsShape2 } from './extHost.protocol.js'; -import { CommandsConverter, ExtHostCommands } from './extHostCommands.js'; -import * as typeConvert from './extHostTypeConverters.js'; -import * as extHostTypes from './extHostTypes.js'; -import { Emitter } from '../../../base/common/event.js'; -import { Location } from '../../../editor/common/languages.js'; - -class ChatAgentResponseStream { - - private _stopWatch = StopWatch.create(false); - private _isClosed: boolean = false; - private _firstProgress: number | undefined; - private _apiObject: vscode.AideChatResponseStream | undefined; - - constructor( - private readonly _extension: IExtensionDescription, - private readonly _request: IAideChatAgentRequest, - private readonly _proxy: MainThreadAideChatAgentsShape2, - private readonly _commandsConverter: CommandsConverter, - private readonly _sessionDisposables: DisposableStore - ) { } - - close() { - this._isClosed = true; - } - - get timings() { - return { - firstProgress: this._firstProgress, - totalElapsed: this._stopWatch.elapsed() - }; - } - - get apiObject() { - - if (!this._apiObject) { - - const that = this; - this._stopWatch.reset(); - - function throwIfDone(source: Function | undefined) { - if (that._isClosed) { - const err = new Error('Response stream has been closed'); - Error.captureStackTrace(err, source); - throw err; - } - } - - const _report = (progress: IAideChatProgressDto, task?: (progress: vscode.Progress) => Thenable) => { - // Measure the time to the first progress update with real markdown content - if (typeof this._firstProgress === 'undefined' && 'content' in progress) { - this._firstProgress = this._stopWatch.elapsed(); - } - - if (task) { - const progressReporterPromise = this._proxy.$handleProgressChunk(this._request.requestId, progress); - const progressReporter = { - report: (p: vscode.ChatResponseWarningPart | vscode.ChatResponseReferencePart) => { - progressReporterPromise?.then((handle) => { - if (handle) { - if (extHostTypes.MarkdownString.isMarkdownString(p.value)) { - this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.AideChatResponseWarningPart.from(p), handle); - } else { - this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.AideChatResponseReferencePart.from(p), handle); - } - } - }); - } - }; - - Promise.all([progressReporterPromise, task?.(progressReporter)]).then(([handle, res]) => { - if (handle !== undefined && res !== undefined) { - this._proxy.$handleProgressChunk(this._request.requestId, typeConvert.AideChatTaskResult.from(res), handle); - } - }); - } else { - this._proxy.$handleProgressChunk(this._request.requestId, progress); - } - }; - - this._apiObject = { - markdown(value) { - throwIfDone(this.markdown); - const part = new extHostTypes.AideChatResponseMarkdownPart(value); - const dto = typeConvert.AideChatResponseMarkdownPart.from(part); - _report(dto); - return this; - }, - markdownWithVulnerabilities(value, vulnerabilities) { - throwIfDone(this.markdown); - if (vulnerabilities) { - checkProposedApiEnabled(that._extension, 'aideChatParticipant'); - } - - const part = new extHostTypes.AideChatResponseMarkdownWithVulnerabilitiesPart(value, vulnerabilities); - const dto = typeConvert.AideChatResponseMarkdownWithVulnerabilitiesPart.from(part); - _report(dto); - return this; - }, - codeblockUri(value) { - throwIfDone(this.codeblockUri); - return this; - }, - filetree(value, baseUri) { - throwIfDone(this.filetree); - const part = new extHostTypes.AideChatResponseFileTreePart(value, baseUri); - const dto = typeConvert.AideChatResponseFilesPart.from(part); - _report(dto); - return this; - }, - anchor(value, title?: string) { - throwIfDone(this.anchor); - const part = new extHostTypes.AideChatResponseAnchorPart(value, title); - const dto = typeConvert.AideChatResponseAnchorPart.from(part); - _report(dto); - return this; - }, - button(value) { - throwIfDone(this.anchor); - const part = new extHostTypes.AideChatResponseCommandButtonPart(value); - const dto = typeConvert.AideChatResponseCommandButtonPart.from(part, that._commandsConverter, that._sessionDisposables); - _report(dto); - return this; - }, - progress(value, task?: ((progress: vscode.Progress) => Thenable)) { - throwIfDone(this.progress); - const part = new extHostTypes.AideChatResponseProgressPart2(value, task); - const dto = task ? typeConvert.AideChatTask.from(part) : typeConvert.AideChatResponseProgressPart.from(part); - _report(dto, task); - return this; - }, - warning(value) { - throwIfDone(this.progress); - checkProposedApiEnabled(that._extension, 'aideChatParticipant'); - const part = new extHostTypes.ChatResponseWarningPart(value); - const dto = typeConvert.AideChatResponseWarningPart.from(part); - _report(dto); - return this; - }, - reference(value, iconPath) { - throwIfDone(this.reference); - - if ('variableName' in value) { - checkProposedApiEnabled(that._extension, 'aideChatParticipant'); - } - - if ('variableName' in value && !value.value) { - // The participant used this variable. Does that variable have any references to pull in? - const matchingVarData = that._request.variables.variables.find(v => v.name === value.variableName); - if (matchingVarData) { - let references: Dto[] | undefined; - if (matchingVarData.references?.length) { - references = matchingVarData.references.map(r => ({ - kind: 'reference', - reference: { variableName: value.variableName, value: r.reference as URI | Location } - } satisfies IAideChatContentReference)); - } else { - // Participant sent a variableName reference but the variable produced no references. Show variable reference with no value - const part = new extHostTypes.AideChatResponseReferencePart(value, iconPath); - const dto = typeConvert.AideChatResponseReferencePart.from(part); - references = [dto]; - } - - references.forEach(r => _report(r)); - return this; - } else { - // Something went wrong- that variable doesn't actually exist - } - } else { - const part = new extHostTypes.AideChatResponseReferencePart(value, iconPath); - const dto = typeConvert.AideChatResponseReferencePart.from(part); - _report(dto); - } - - return this; - }, - reference2(value, iconPath, options) { - throwIfDone(this.reference); - return this; - }, - codeCitation(value: vscode.Uri, license: string, snippet: string): void { - throwIfDone(this.codeCitation); - }, - textEdit(target, edits) { - throwIfDone(this.textEdit); - checkProposedApiEnabled(that._extension, 'aideChatParticipant'); - - const part = new extHostTypes.AideChatResponseTextEditPart(target, edits); - const dto = typeConvert.AideChatResponseTextEditPart.from(part); - _report(dto); - return this; - }, - detectedParticipant(participant, command) { - throwIfDone(this.detectedParticipant); - checkProposedApiEnabled(that._extension, 'aideChatParticipant'); - - const part = new extHostTypes.AideChatResponseDetectedParticipantPart(participant, command); - const dto = typeConvert.AideChatResponseDetectedParticipantPart.from(part); - _report(dto); - return this; - }, - confirmation(title, message, data) { - throwIfDone(this.confirmation); - checkProposedApiEnabled(that._extension, 'aideChatParticipant'); - - const part = new extHostTypes.AideChatResponseConfirmationPart(title, message, data); - const dto = typeConvert.AideChatResponseConfirmationPart.from(part); - _report(dto); - return this; - }, - push(part) { - throwIfDone(this.push); - - if ( - part instanceof extHostTypes.ChatResponseTextEditPart || - part instanceof extHostTypes.ChatResponseMarkdownWithVulnerabilitiesPart || - part instanceof extHostTypes.ChatResponseDetectedParticipantPart || - part instanceof extHostTypes.ChatResponseWarningPart || - part instanceof extHostTypes.ChatResponseConfirmationPart || - part instanceof extHostTypes.ChatResponseCodeCitationPart || - part instanceof extHostTypes.ChatResponseMovePart - ) { - checkProposedApiEnabled(that._extension, 'aideChatParticipant'); - } - - if (part instanceof extHostTypes.ChatResponseReferencePart) { - // Ensure variable reference values get fixed up - this.reference2(part.value, part.iconPath, part.options); - } else { - const dto = typeConvert.AideChatResponsePart.from(part, that._commandsConverter, that._sessionDisposables); - _report(dto); - } - - return this; - }, - }; - } - - return this._apiObject; - } -} - -export class ExtHostAideChatAgents2 extends Disposable implements ExtHostAideChatAgentsShape2 { - - private static _idPool = 0; - - private readonly _agents = new Map(); - private readonly _proxy: MainThreadAideChatAgentsShape2; - - private readonly _sessionDisposables: DisposableMap = this._register(new DisposableMap()); - private readonly _completionDisposables: DisposableMap = this._register(new DisposableMap()); - - constructor( - mainContext: IMainContext, - private readonly _logService: ILogService, - private readonly commands: ExtHostCommands, - private readonly quality: string | undefined - ) { - super(); - this._proxy = mainContext.getProxy(MainContext.MainThreadAideChatAgents2); - } - - transferActiveChat(newWorkspace: vscode.Uri): void { - this._proxy.$transferActiveChatSession(newWorkspace); - } - - createChatAgent(extension: IExtensionDescription, id: string, handler: vscode.AideChatExtendedRequestHandler): vscode.AideChatParticipant { - const handle = ExtHostAideChatAgents2._idPool++; - const agent = new ExtHostChatAgent(extension, this.quality, id, this._proxy, handle, handler); - this._agents.set(handle, agent); - - if (agent.isAgentEnabled()) { - this._proxy.$registerAgent(handle, extension.identifier, id, {}, undefined); - } - - return agent.apiAgent; - } - - createDynamicChatAgent(extension: IExtensionDescription, id: string, dynamicProps: vscode.DynamicChatParticipantProps, handler: vscode.AideChatExtendedRequestHandler): vscode.AideChatParticipant { - const handle = ExtHostAideChatAgents2._idPool++; - const agent = new ExtHostChatAgent(extension, this.quality, id, this._proxy, handle, handler); - this._agents.set(handle, agent); - - this._proxy.$registerAgent(handle, extension.identifier, id, { isSticky: true } satisfies IExtensionAideChatAgentMetadata, dynamicProps); - return agent.apiAgent; - } - - async $invokeAgent(handle: number, request: IAideChatAgentRequest, context: { history: IAideChatAgentHistoryEntryDto[] }, token: vscode.CancellationToken): Promise { - const agent = this._agents.get(handle); - if (!agent) { - throw new Error(`[CHAT](${handle}) CANNOT invoke agent because the agent is not registered`); - } - - // Init session disposables - let sessionDisposables = this._sessionDisposables.get(request.sessionId); - if (!sessionDisposables) { - sessionDisposables = new DisposableStore(); - this._sessionDisposables.set(request.sessionId, sessionDisposables); - } - - const stream = new ChatAgentResponseStream(agent.extension, request, this._proxy, this.commands.converter, sessionDisposables); - try { - const convertedHistory = await this.prepareHistoryTurns(request.agentId, context); - const task = agent.invoke( - typeConvert.AideChatAgentRequest.to(request), - { history: convertedHistory }, - stream.apiObject, - token - ); - - return await raceCancellation(Promise.resolve(task).then((result) => { - if (result?.metadata) { - try { - JSON.stringify(result.metadata); - } catch (err) { - const msg = `result.metadata MUST be JSON.stringify-able. Got error: ${err.message}`; - this._logService.error(`[${agent.extension.identifier.value}] [@${agent.id}] ${msg}`, agent.extension); - return { errorDetails: { message: msg }, timings: stream.timings }; - } - } - let errorDetails: IAideChatResponseErrorDetails | undefined; - if (result?.errorDetails) { - errorDetails = { - ...result.errorDetails, - responseIsIncomplete: true - }; - } - if (errorDetails?.responseIsRedacted) { - checkProposedApiEnabled(agent.extension, 'aideChatParticipant'); - } - - return { errorDetails, timings: stream.timings, metadata: result?.metadata } satisfies IAideChatAgentResult; - }), token); - } catch (e) { - this._logService.error(e, agent.extension); - - return { errorDetails: { message: toErrorMessage(e), responseIsIncomplete: true } }; - - } finally { - stream.close(); - } - } - - private async prepareHistoryTurns(agentId: string, context: { history: IAideChatAgentHistoryEntryDto[] }): Promise<(vscode.ChatRequestTurn | vscode.ChatResponseTurn)[]> { - - const res: (vscode.ChatRequestTurn | vscode.ChatResponseTurn)[] = []; - - for (const h of context.history) { - const ehResult = typeConvert.AideChatAgentResult.to(h.result); - const result: vscode.ChatResult = agentId === h.request.agentId ? - ehResult : - { ...ehResult, metadata: undefined }; - - // REQUEST turn - res.push(new extHostTypes.AideChatRequestTurn(h.request.message, h.request.command, h.request.variables.variables.map(typeConvert.AideChatAgentValueReference.to), h.request.agentId)); - - // RESPONSE turn - const parts = coalesce(h.response.map(r => typeConvert.AideChatResponsePart.toContent(r, this.commands.converter))); - res.push(new extHostTypes.AideChatResponseTurn(parts, result, h.request.agentId, h.request.command)); - } - - return res; - } - - $releaseSession(sessionId: string): void { - this._sessionDisposables.deleteAndDispose(sessionId); - } - - async $provideFollowups(request: IAideChatAgentRequest, handle: number, result: IAideChatAgentResult, context: { history: IAideChatAgentHistoryEntryDto[] }, token: vscode.CancellationToken): Promise { - const agent = this._agents.get(handle); - if (!agent) { - return Promise.resolve([]); - } - - const convertedHistory = await this.prepareHistoryTurns(agent.id, context); - - const ehResult = typeConvert.AideChatAgentResult.to(result); - return (await agent.provideFollowups(ehResult, { history: convertedHistory }, token)) - .filter(f => { - // The followup must refer to a participant that exists from the same extension - const isValid = !f.participant || Iterable.some( - this._agents.values(), - a => a.id === f.participant && ExtensionIdentifier.equals(a.extension.identifier, agent.extension.identifier)); - if (!isValid) { - this._logService.warn(`[@${agent.id}] ChatFollowup refers to an unknown participant: ${f.participant}`); - } - return isValid; - }) - .map(f => typeConvert.AideChatFollowup.from(f, request)); - } - - $acceptFeedback(handle: number, result: IAideChatAgentResult, vote: AideChatAgentVoteDirection, reportIssue?: boolean): void { - const agent = this._agents.get(handle); - if (!agent) { - return; - } - - const ehResult = typeConvert.AideChatAgentResult.to(result); - let kind: extHostTypes.AideChatResultFeedbackKind; - switch (vote) { - case AideChatAgentVoteDirection.Down: - kind = extHostTypes.AideChatResultFeedbackKind.Unhelpful; - break; - case AideChatAgentVoteDirection.Up: - kind = extHostTypes.AideChatResultFeedbackKind.Helpful; - break; - } - agent.acceptFeedback(reportIssue ? - Object.freeze({ result: ehResult, kind, reportIssue }) : - Object.freeze({ result: ehResult, kind })); - } - - $acceptAction(handle: number, result: IAideChatAgentResult, event: IAideChatUserActionEvent): void { - const agent = this._agents.get(handle); - if (!agent) { - return; - } - if (event.action.kind === 'vote') { - // handled by $acceptFeedback - return; - } - - const ehAction = typeConvert.AideChatAgentUserActionEvent.to(result, event, this.commands.converter); - if (ehAction) { - agent.acceptAction(Object.freeze(ehAction)); - } - } - - async $invokeCompletionProvider(handle: number, query: string, token: vscode.CancellationToken): Promise { - const agent = this._agents.get(handle); - if (!agent) { - return []; - } - - let disposables = this._completionDisposables.get(handle); - if (disposables) { - // Clear any disposables from the last invocation of this completion provider - disposables.clear(); - } else { - disposables = new DisposableStore(); - this._completionDisposables.set(handle, disposables); - } - - const items = await agent.invokeCompletionProvider(query, token); - - return items.map((i) => typeConvert.AideChatAgentCompletionItem.from(i, this.commands.converter, disposables)); - } - - async $provideWelcomeMessage(handle: number, location: AideChatAgentLocation, token: vscode.CancellationToken): Promise<(string | IMarkdownString)[] | undefined> { - const agent = this._agents.get(handle); - if (!agent) { - return; - } - - return await agent.provideWelcomeMessage(typeConvert.AideChatLocation.to(location), token); - } - - async $provideSampleQuestions(handle: number, location: AideChatAgentLocation, token: vscode.CancellationToken): Promise { - const agent = this._agents.get(handle); - if (!agent) { - return; - } - - return (await agent.provideSampleQuestions(typeConvert.AideChatLocation.to(location), token)) - .map(f => typeConvert.AideChatFollowup.from(f, undefined)); - } -} - -class ExtHostChatAgent { - - private _followupProvider: vscode.ChatFollowupProvider | undefined; - private _iconPath: vscode.Uri | { light: vscode.Uri; dark: vscode.Uri } | vscode.ThemeIcon | undefined; - private _isDefault: boolean | undefined; - private _helpTextPrefix: string | vscode.MarkdownString | undefined; - private _helpTextVariablesPrefix: string | vscode.MarkdownString | undefined; - private _helpTextPostfix: string | vscode.MarkdownString | undefined; - private _isSecondary: boolean | undefined; - private _onDidReceiveFeedback = new Emitter(); - private _onDidPerformAction = new Emitter(); - private _supportIssueReporting: boolean | undefined; - private _agentVariableProvider?: { provider: vscode.ChatParticipantCompletionItemProvider; triggerCharacters: string[] }; - private _welcomeMessageProvider?: vscode.AideChatWelcomeMessageProvider | undefined; - private _requester: vscode.ChatRequesterInformation | undefined; - private _supportsSlowReferences: boolean | undefined; - - constructor( - public readonly extension: IExtensionDescription, - private readonly quality: string | undefined, - public readonly id: string, - private readonly _proxy: MainThreadAideChatAgentsShape2, - private readonly _handle: number, - private _requestHandler: vscode.AideChatExtendedRequestHandler, - ) { } - - acceptFeedback(feedback: vscode.AideChatResultFeedback) { - this._onDidReceiveFeedback.fire(feedback); - } - - acceptAction(event: vscode.ChatUserActionEvent) { - this._onDidPerformAction.fire(event); - } - - async invokeCompletionProvider(query: string, token: vscode.CancellationToken): Promise { - if (!this._agentVariableProvider) { - return []; - } - - return await this._agentVariableProvider.provider.provideCompletionItems(query, token) ?? []; - } - - public isAgentEnabled() { - // If in stable and this extension doesn't have the right proposed API, then don't register the agent - return !(this.quality === 'stable' && !isProposedApiEnabled(this.extension, 'aideChatParticipant')); - } - - async provideFollowups(result: vscode.ChatResult, context: vscode.ChatContext, token: vscode.CancellationToken): Promise { - if (!this._followupProvider) { - return []; - } - - const followups = await this._followupProvider.provideFollowups(result, context, token); - if (!followups) { - return []; - } - return followups - // Filter out "command followups" from older providers - .filter(f => !(f && 'commandId' in f)) - // Filter out followups from older providers before 'message' changed to 'prompt' - .filter(f => !(f && 'message' in f)); - } - - async provideWelcomeMessage(location: vscode.AideChatLocation, token: vscode.CancellationToken): Promise<(string | IMarkdownString)[] | undefined> { - if (!this._welcomeMessageProvider) { - return []; - } - const content = await this._welcomeMessageProvider.provideWelcomeMessage(location, token); - if (!content) { - return []; - } - return content.map(item => { - if (typeof item === 'string') { - return item; - } else { - return typeConvert.MarkdownString.from(item); - } - }); - } - - async provideSampleQuestions(location: vscode.AideChatLocation, token: vscode.CancellationToken): Promise { - if (!this._welcomeMessageProvider || !this._welcomeMessageProvider.provideSampleQuestions) { - return []; - } - const content = await this._welcomeMessageProvider.provideSampleQuestions(location, token); - if (!content) { - return []; - } - - return content; - } - - get apiAgent(): vscode.AideChatParticipant { - let disposed = false; - let updateScheduled = false; - const updateMetadataSoon = () => { - if (disposed) { - return; - } - if (updateScheduled) { - return; - } - updateScheduled = true; - queueMicrotask(() => { - if (!that.isAgentEnabled()) { - return; - } - - this._proxy.$updateAgent(this._handle, { - icon: !this._iconPath ? undefined : - this._iconPath instanceof URI ? this._iconPath : - 'light' in this._iconPath ? this._iconPath.light : - undefined, - iconDark: !this._iconPath ? undefined : - 'dark' in this._iconPath ? this._iconPath.dark : - undefined, - themeIcon: this._iconPath instanceof extHostTypes.ThemeIcon || this._iconPath instanceof URI ? this._iconPath : undefined, - hasFollowups: this._followupProvider !== undefined, - isSecondary: this._isSecondary, - helpTextPrefix: (!this._helpTextPrefix || typeof this._helpTextPrefix === 'string') ? this._helpTextPrefix : typeConvert.MarkdownString.from(this._helpTextPrefix), - helpTextVariablesPrefix: (!this._helpTextVariablesPrefix || typeof this._helpTextVariablesPrefix === 'string') ? this._helpTextVariablesPrefix : typeConvert.MarkdownString.from(this._helpTextVariablesPrefix), - helpTextPostfix: (!this._helpTextPostfix || typeof this._helpTextPostfix === 'string') ? this._helpTextPostfix : typeConvert.MarkdownString.from(this._helpTextPostfix), - supportIssueReporting: this._supportIssueReporting, - requester: this._requester, - supportsSlowVariables: this._supportsSlowReferences, - }); - updateScheduled = false; - }); - }; - - const that = this; - return { - get id() { - return that.id; - }, - get iconPath() { - return that._iconPath; - }, - set iconPath(v) { - that._iconPath = v; - updateMetadataSoon(); - }, - get requestHandler() { - return that._requestHandler; - }, - set requestHandler(v) { - assertType(typeof v === 'function', 'Invalid request handler'); - that._requestHandler = v; - }, - get followupProvider() { - return that._followupProvider; - }, - set followupProvider(v) { - that._followupProvider = v; - updateMetadataSoon(); - }, - get isDefault() { - checkProposedApiEnabled(that.extension, 'aideChatParticipant'); - return that._isDefault; - }, - set isDefault(v) { - checkProposedApiEnabled(that.extension, 'aideChatParticipant'); - that._isDefault = v; - updateMetadataSoon(); - }, - get helpTextPrefix() { - checkProposedApiEnabled(that.extension, 'aideChatParticipant'); - return that._helpTextPrefix; - }, - set helpTextPrefix(v) { - checkProposedApiEnabled(that.extension, 'aideChatParticipant'); - that._helpTextPrefix = v; - updateMetadataSoon(); - }, - get helpTextVariablesPrefix() { - checkProposedApiEnabled(that.extension, 'aideChatParticipant'); - return that._helpTextVariablesPrefix; - }, - set helpTextVariablesPrefix(v) { - checkProposedApiEnabled(that.extension, 'aideChatParticipant'); - that._helpTextVariablesPrefix = v; - updateMetadataSoon(); - }, - get helpTextPostfix() { - checkProposedApiEnabled(that.extension, 'aideChatParticipant'); - return that._helpTextPostfix; - }, - set helpTextPostfix(v) { - checkProposedApiEnabled(that.extension, 'aideChatParticipant'); - that._helpTextPostfix = v; - updateMetadataSoon(); - }, - get isSecondary() { - checkProposedApiEnabled(that.extension, 'aideChatParticipant'); - return that._isSecondary; - }, - set isSecondary(v) { - checkProposedApiEnabled(that.extension, 'aideChatParticipant'); - that._isSecondary = v; - updateMetadataSoon(); - }, - get supportIssueReporting() { - checkProposedApiEnabled(that.extension, 'aideChatParticipant'); - return that._supportIssueReporting; - }, - set supportIssueReporting(v) { - checkProposedApiEnabled(that.extension, 'aideChatParticipant'); - that._supportIssueReporting = v; - updateMetadataSoon(); - }, - get onDidReceiveFeedback() { - return that._onDidReceiveFeedback.event; - }, - set participantVariableProvider(v) { - checkProposedApiEnabled(that.extension, 'aideChatParticipant'); - that._agentVariableProvider = v; - if (v) { - if (!v.triggerCharacters.length) { - throw new Error('triggerCharacters are required'); - } - - that._proxy.$registerAgentCompletionsProvider(that._handle, that.id, v.triggerCharacters); - } else { - that._proxy.$unregisterAgentCompletionsProvider(that._handle, that.id); - } - }, - get participantVariableProvider() { - checkProposedApiEnabled(that.extension, 'aideChatParticipant'); - return that._agentVariableProvider; - }, - set welcomeMessageProvider(v) { - checkProposedApiEnabled(that.extension, 'aideChatParticipant'); - that._welcomeMessageProvider = v; - updateMetadataSoon(); - }, - get welcomeMessageProvider() { - checkProposedApiEnabled(that.extension, 'aideChatParticipant'); - return that._welcomeMessageProvider; - }, - onDidPerformAction: !isProposedApiEnabled(this.extension, 'aideChatParticipant') - ? undefined! - : this._onDidPerformAction.event - , - set requester(v) { - that._requester = v; - updateMetadataSoon(); - }, - get requester() { - return that._requester; - }, - set supportsSlowReferences(v) { - checkProposedApiEnabled(that.extension, 'aideChatParticipant'); - that._supportsSlowReferences = v; - updateMetadataSoon(); - }, - get supportsSlowReferences() { - checkProposedApiEnabled(that.extension, 'aideChatParticipant'); - return that._supportsSlowReferences; - }, - dispose() { - disposed = true; - that._followupProvider = undefined; - that._onDidReceiveFeedback.dispose(); - that._proxy.$unregisterAgent(that._handle); - }, - } satisfies vscode.AideChatParticipant; - } - - invoke(request: vscode.AideChatRequest, context: vscode.ChatContext, response: vscode.AideChatResponseStream, token: vscode.CancellationToken): vscode.ProviderResult { - return this._requestHandler(request, context, response, token); - } -} diff --git a/src/vs/workbench/api/common/extHostAideChatVariables.ts b/src/vs/workbench/api/common/extHostAideChatVariables.ts deleted file mode 100644 index dc5ea5fcf28..00000000000 --- a/src/vs/workbench/api/common/extHostAideChatVariables.ts +++ /dev/null @@ -1,129 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { onUnexpectedExternalError } from '../../../base/common/errors.js'; -import { IDisposable, toDisposable } from '../../../base/common/lifecycle.js'; -import { ThemeIcon } from '../../../base/common/themables.js'; -import { IExtensionDescription } from '../../../platform/extensions/common/extensions.js'; -import { IAideChatRequestVariableValue, IAideChatVariableData } from '../../contrib/aideChat/common/aideChatVariables.js'; -import { checkProposedApiEnabled } from '../../services/extensions/common/extensions.js'; -import { ExtHostAideChatVariablesShape, IChatVariableResolverProgressDto, IMainContext, MainContext, MainThreadAideChatVariablesShape } from './extHost.protocol.js'; -import * as typeConvert from './extHostTypeConverters.js'; -import * as extHostTypes from './extHostTypes.js'; - -export class ExtHostAideChatVariables implements ExtHostAideChatVariablesShape { - - private static _idPool = 0; - - private readonly _resolver = new Map(); - private readonly _proxy: MainThreadAideChatVariablesShape; - - constructor(mainContext: IMainContext) { - this._proxy = mainContext.getProxy(MainContext.MainThreadAideChatVariables); - } - - async $resolveVariable(handle: number, requestId: string, messageText: string, token: vscode.CancellationToken): Promise { - const item = this._resolver.get(handle); - if (!item) { - return undefined; - } - try { - if (item.resolver.resolve2) { - checkProposedApiEnabled(item.extension, 'aideChatParticipant'); - const stream = new ChatVariableResolverResponseStream(requestId, this._proxy); - const value = await item.resolver.resolve2(item.data.name, { prompt: messageText }, stream.apiObject, token); - - // Temp, ignoring other returned values to convert the array into a single value - if (value && value[0]) { - return value[0].value; - } - } else { - const value = await item.resolver.resolve(item.data.name, { prompt: messageText }, token); - if (value && value[0]) { - return value[0].value; - } - } - } catch (err) { - onUnexpectedExternalError(err); - } - return undefined; - } - - registerVariableResolver(extension: IExtensionDescription, id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: vscode.ChatVariableResolver, fullName?: string, themeIconId?: string): IDisposable { - const handle = ExtHostAideChatVariables._idPool++; - const icon = themeIconId ? ThemeIcon.fromId(themeIconId) : undefined; - this._resolver.set(handle, { extension, data: { id, name, description: userDescription, modelDescription, icon }, resolver: resolver }); - this._proxy.$registerVariable(handle, { id, name, description: userDescription, modelDescription, isSlow, fullName, icon }); - - return toDisposable(() => { - this._resolver.delete(handle); - this._proxy.$unregisterVariable(handle); - }); - } -} - -class ChatVariableResolverResponseStream { - - private _isClosed: boolean = false; - private _apiObject: vscode.ChatVariableResolverResponseStream | undefined; - - constructor( - private readonly _requestId: string, - private readonly _proxy: MainThreadAideChatVariablesShape, - ) { } - - close() { - this._isClosed = true; - } - - get apiObject() { - if (!this._apiObject) { - const that = this; - - function throwIfDone(source: Function | undefined) { - if (that._isClosed) { - const err = new Error('Response stream has been closed'); - Error.captureStackTrace(err, source); - throw err; - } - } - - const _report = (progress: IChatVariableResolverProgressDto) => { - this._proxy.$handleProgressChunk(this._requestId, progress); - }; - - this._apiObject = { - progress(value) { - throwIfDone(this.progress); - const part = new extHostTypes.ChatResponseProgressPart(value); - const dto = typeConvert.ChatResponseProgressPart.from(part); - _report(dto); - return this; - }, - reference(value) { - throwIfDone(this.reference); - const part = new extHostTypes.ChatResponseReferencePart(value); - const dto = typeConvert.AideChatResponseReferencePart.from(part); - _report(dto); - return this; - }, - push(part) { - throwIfDone(this.push); - - if (part instanceof extHostTypes.ChatResponseReferencePart) { - _report(typeConvert.AideChatResponseReferencePart.from(part)); - } else if (part instanceof extHostTypes.ChatResponseProgressPart) { - _report(typeConvert.AideChatResponseProgressPart.from(part)); - } - - return this; - } - }; - } - - return this._apiObject; - } -} diff --git a/src/vs/workbench/api/common/extHostAideProbeProvider.ts b/src/vs/workbench/api/common/extHostAideProbeProvider.ts index e011f587979..91cb21b36a5 100644 --- a/src/vs/workbench/api/common/extHostAideProbeProvider.ts +++ b/src/vs/workbench/api/common/extHostAideProbeProvider.ts @@ -56,9 +56,9 @@ export class ExtHostAideProbeProvider extends Disposable implements ExtHostAideP that._proxy.$handleProbingProgressChunk(request, dto); }, breakdown(value) { - const part = new extHostTypes.AideChatResponseBreakdownPart(value.reference.uri, value.reference.name, value.query, value.reason, value.response); - const dto = typeConvert.AideChatResponseBreakdownPart.from(part); - that._proxy.$handleProbingProgressChunk(request, dto); + // const part = new extHostTypes.ChatResponseMarkdownPart(value); + // const dto = typeConvert.AideChatResponseBreakdownPart.from(part); + // that._proxy.$handleProbingProgressChunk(request, dto); }, openFile(value) { const part = new extHostTypes.AideProbeOpenFilePart(value.uri); @@ -81,8 +81,8 @@ export class ExtHostAideProbeProvider extends Disposable implements ExtHostAideP that._proxy.$handleProbingProgressChunk(request, dto); }, markdown(value) { - const part = new extHostTypes.AideChatResponseMarkdownPart(value); - const dto = typeConvert.AideChatResponseMarkdownPart.from(part); + const part = new extHostTypes.ChatResponseMarkdownPart(value); + const dto = typeConvert.ChatResponseMarkdownPart.from(part); that._proxy.$handleProbingProgressChunk(request, dto); }, location(value) { diff --git a/src/vs/workbench/api/common/extHostEditorTabs.ts b/src/vs/workbench/api/common/extHostEditorTabs.ts index ca8a4e813d6..592c75c4bae 100644 --- a/src/vs/workbench/api/common/extHostEditorTabs.ts +++ b/src/vs/workbench/api/common/extHostEditorTabs.ts @@ -11,7 +11,7 @@ import { createDecorator } from '../../../platform/instantiation/common/instanti import { IEditorTabDto, IEditorTabGroupDto, IExtHostEditorTabsShape, MainContext, MainThreadEditorTabsShape, TabInputKind, TabModelOperationKind, TabOperation } from './extHost.protocol.js'; import { IExtHostRpcService } from './extHostRpcService.js'; import * as typeConverters from './extHostTypeConverters.js'; -import { ChatEditorTabInput, CustomEditorTabInput, InteractiveWindowInput, NotebookDiffEditorTabInput, NotebookEditorTabInput, TerminalEditorTabInput, TextDiffTabInput, TextMergeTabInput, TextTabInput, WebviewEditorTabInput, TextMultiDiffTabInput, AideChatEditorTabInput } from './extHostTypes.js'; +import { ChatEditorTabInput, CustomEditorTabInput, InteractiveWindowInput, NotebookDiffEditorTabInput, NotebookEditorTabInput, TerminalEditorTabInput, TextDiffTabInput, TextMergeTabInput, TextTabInput, WebviewEditorTabInput, TextMultiDiffTabInput } from './extHostTypes.js'; import type * as vscode from 'vscode'; export interface IExtHostEditorTabs extends IExtHostEditorTabsShape { @@ -21,7 +21,7 @@ export interface IExtHostEditorTabs extends IExtHostEditorTabsShape { export const IExtHostEditorTabs = createDecorator('IExtHostEditorTabs'); -type AnyTabInput = TextTabInput | TextDiffTabInput | TextMultiDiffTabInput | CustomEditorTabInput | NotebookEditorTabInput | NotebookDiffEditorTabInput | WebviewEditorTabInput | TerminalEditorTabInput | InteractiveWindowInput | ChatEditorTabInput | AideChatEditorTabInput; +type AnyTabInput = TextTabInput | TextDiffTabInput | TextMultiDiffTabInput | CustomEditorTabInput | NotebookEditorTabInput | NotebookDiffEditorTabInput | WebviewEditorTabInput | TerminalEditorTabInput | InteractiveWindowInput | ChatEditorTabInput; class ExtHostEditorTab { private _apiObject: vscode.Tab | undefined; @@ -100,8 +100,6 @@ class ExtHostEditorTab { return new InteractiveWindowInput(URI.revive(this._dto.input.uri), URI.revive(this._dto.input.inputBoxUri)); case TabInputKind.ChatEditorInput: return new ChatEditorTabInput(); - case TabInputKind.AideChatEditorInput: - return new AideChatEditorTabInput(); case TabInputKind.MultiDiffEditorInput: return new TextMultiDiffTabInput(this._dto.input.diffEditors.map(diff => new TextDiffTabInput(URI.revive(diff.original), URI.revive(diff.modified)))); default: diff --git a/src/vs/workbench/api/common/extHostTypeConverters.ts b/src/vs/workbench/api/common/extHostTypeConverters.ts index 31262d85ab2..8fda434fe01 100644 --- a/src/vs/workbench/api/common/extHostTypeConverters.ts +++ b/src/vs/workbench/api/common/extHostTypeConverters.ts @@ -54,9 +54,6 @@ import { ACTIVE_GROUP, SIDE_GROUP } from '../../services/editor/common/editorSer import { Dto } from '../../services/extensions/common/proxyIdentifier.js'; import type * as vscode from 'vscode'; import * as types from './extHostTypes.js'; -import { IAideChatAgentDetection, IAideChatAgentMarkdownContentWithVulnerability, IAideChatCommandButton, IAideChatConfirmation, IAideChatContentInlineReference, IAideChatContentReference, IAideChatFollowup, IAideChatMarkdownContent, IAideChatProgressMessage, IAideChatTaskDto, IAideChatTaskResult, IAideChatTextEdit, IAideChatUserActionEvent, IAideChatWarningMessage } from '../../contrib/aideChat/common/aideChatService.js'; -import { IAideChatAgentRequest, AideChatAgentLocation, IAideChatAgentResult } from '../../contrib/aideChat/common/aideChatAgents.js'; -import { IAideChatRequestVariableEntry } from '../../contrib/aideChat/common/aideChatModel.js'; import { IAideProbeGoToDefinition, IAideProbeInitialSymbols, IAideProbeBreakdownContent, IAideProbeTextEdit, IAideProbeIterationFinished, IAideProbeOpenFile, IAideReferenceFound, IAideRelevantReference, IAideFollowups, IAideProbeRepoMapGeneration, IAideProbeLongContextSearch, IAideProbeRequestModel, IAideProbeSessionAction, IAideProbeUserAction } from '../../contrib/aideProbe/common/aideProbe.js'; export namespace Command { @@ -2500,7 +2497,7 @@ export namespace ChatResponseFilesPart { } export namespace ChatResponseAnchorPart { - export function from(part: vscode.ChatResponseAnchorPart): Dto { + export function from(part: vscode.ChatResponseAnchorPart): Dto { // Work around type-narrowing confusion between vscode.Uri and URI const isUri = (thing: unknown): thing is vscode.Uri => URI.isUri(thing); const isSymbolInformation = (x: any): x is vscode.SymbolInformation => x instanceof types.SymbolInformation; @@ -2516,8 +2513,8 @@ export namespace ChatResponseAnchorPart { }; } - export function to(part: Dto): vscode.ChatResponseAnchorPart { - const value = revive(part); + export function to(part: Dto): vscode.ChatResponseAnchorPart { + const value = revive(part); return new types.ChatResponseAnchorPart( URI.isUri(value.inlineReference) ? value.inlineReference @@ -2884,39 +2881,6 @@ export namespace LanguageModelToolResult { ///////////////////////////// END CHAT ///////////////////////////// ///////////////////////////// START AIDE ///////////////////////////// -export namespace AideChatFollowup { - export function from(followup: vscode.ChatFollowup, request: IAideChatAgentRequest | undefined): IAideChatFollowup { - return { - kind: 'reply', - agentId: followup.participant ?? request?.agentId ?? '', - subCommand: followup.command ?? request?.command, - message: followup.prompt, - title: followup.label - }; - } - - export function to(followup: IAideChatFollowup): vscode.ChatFollowup { - return { - prompt: followup.message, - label: followup.title, - participant: followup.agentId, - command: followup.subCommand, - }; - } -} - -export namespace AideChatResponseMarkdownPart { - export function from(part: vscode.ChatResponseMarkdownPart): Dto { - return { - kind: 'markdownContent', - content: MarkdownString.from(part.value) - }; - } - export function to(part: Dto): vscode.ChatResponseMarkdownPart { - return new types.AideChatResponseMarkdownPart(MarkdownString.to(part.content)); - } -} - export namespace AideProbeGoToDefinitionPart { export function from(part: vscode.AideProbeGoToDefinition): Dto { return { @@ -2939,217 +2903,6 @@ export namespace AideProbeInitialSymbolsPart { } } -export namespace AideChatResponseMarkdownWithVulnerabilitiesPart { - export function from(part: vscode.ChatResponseMarkdownWithVulnerabilitiesPart): Dto { - return { - kind: 'markdownVuln', - content: MarkdownString.from(part.value), - vulnerabilities: part.vulnerabilities, - }; - } - export function to(part: Dto): vscode.ChatResponseMarkdownWithVulnerabilitiesPart { - return new types.AideChatResponseMarkdownWithVulnerabilitiesPart(MarkdownString.to(part.content), part.vulnerabilities); - } -} - -export namespace AideChatResponseDetectedParticipantPart { - export function from(part: vscode.ChatResponseDetectedParticipantPart): Dto { - return { - kind: 'agentDetection', - agentId: part.participant, - command: part.command, - }; - } - export function to(part: Dto): vscode.ChatResponseDetectedParticipantPart { - return new types.AideChatResponseDetectedParticipantPart(part.agentId, part.command); - } -} - -export namespace AideChatResponseConfirmationPart { - export function from(part: vscode.ChatResponseConfirmationPart): Dto { - return { - kind: 'confirmation', - title: part.title, - message: part.message, - data: part.data - }; - } -} - -export namespace AideChatResponseFilesPart { - export function from(part: vscode.ChatResponseFileTreePart): IChatTreeData { - const { value, baseUri } = part; - function convert(items: vscode.ChatResponseFileTree[], baseUri: URI): extHostProtocol.IChatResponseProgressFileTreeData[] { - return items.map(item => { - const myUri = URI.joinPath(baseUri, item.name); - return { - label: item.name, - uri: myUri, - children: item.children && convert(item.children, myUri) - }; - }); - } - return { - kind: 'treeData', - treeData: { - label: basename(baseUri), - uri: baseUri, - children: convert(value, baseUri) - } - }; - } - export function to(part: Dto): vscode.ChatResponseFileTreePart { - const treeData = revive(part.treeData); - function convert(items: extHostProtocol.IChatResponseProgressFileTreeData[]): vscode.ChatResponseFileTree[] { - return items.map(item => { - return { - name: item.label, - children: item.children && convert(item.children) - }; - }); - } - - const baseUri = treeData.uri; - const items = treeData.children ? convert(treeData.children) : []; - return new types.AideChatResponseFileTreePart(items, baseUri); - } -} - -export namespace AideChatResponseAnchorPart { - export function from(part: vscode.ChatResponseAnchorPart): Dto { - // Work around type-narrowing confusion between vscode.Uri and URI - const isUri = (thing: unknown): thing is vscode.Uri => URI.isUri(thing); - - return { - kind: 'inlineReference', - name: part.title, - inlineReference: isUri(part.value) ? part.value : Location.from(part.value) - }; - } - - export function to(part: Dto): vscode.ChatResponseAnchorPart { - const value = revive(part); - return new types.ChatResponseAnchorPart( - URI.isUri(value.inlineReference) ? value.inlineReference : Location.to(value.inlineReference), - part.name - ); - } -} - -export namespace AideChatResponseProgressPart { - export function from(part: vscode.ChatResponseProgressPart): Dto { - return { - kind: 'progressMessage', - content: MarkdownString.from(part.value) - }; - } - export function to(part: Dto): vscode.ChatResponseProgressPart { - return new types.AideChatResponseProgressPart(part.content.value); - } -} - -export namespace AideChatResponseWarningPart { - export function from(part: vscode.ChatResponseWarningPart): Dto { - return { - kind: 'warning', - content: MarkdownString.from(part.value) - }; - } - export function to(part: Dto): vscode.ChatResponseWarningPart { - return new types.ChatResponseWarningPart(part.content.value); - } -} - -export namespace AideChatTask { - export function from(part: vscode.ChatResponseProgressPart2): IAideChatTaskDto { - return { - kind: 'progressTask', - content: MarkdownString.from(part.value), - }; - } -} - -export namespace AideChatTaskResult { - export function from(part: string | void): Dto { - return { - kind: 'progressTaskResult', - content: typeof part === 'string' ? MarkdownString.from(part) : undefined - }; - } -} - -export namespace AideChatResponseCommandButtonPart { - export function from(part: vscode.ChatResponseCommandButtonPart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): Dto { - // If the command isn't in the converter, then this session may have been restored, and the command args don't exist anymore - const command = commandsConverter.toInternal(part.value, commandDisposables) ?? { command: part.value.command, title: part.value.title }; - return { - kind: 'command', - command - }; - } - export function to(part: Dto, commandsConverter: CommandsConverter): vscode.ChatResponseCommandButtonPart { - // If the command isn't in the converter, then this session may have been restored, and the command args don't exist anymore - return new types.AideChatResponseCommandButtonPart(commandsConverter.fromInternal(part.command) ?? { command: part.command.id, title: part.command.title }); - } -} - -export namespace AideChatResponseTextEditPart { - export function from(part: vscode.AideChatResponseTextEdit): Dto { - return { - kind: 'textEdit', - uri: part.uri, - edits: part.edits.map(e => TextEdit.from(e)) - }; - } - export function to(part: Dto): vscode.AideChatResponseTextEdit { - return new types.AideChatResponseTextEditPart(URI.revive(part.uri), part.edits.map(e => TextEdit.to(e))); - } -} - -export namespace AideChatResponseReferencePart { - export function from(part: types.AideChatResponseReferencePart): Dto { - const iconPath = ThemeIcon.isThemeIcon(part.iconPath) ? part.iconPath - : URI.isUri(part.iconPath) ? { light: URI.revive(part.iconPath) } - : (part.iconPath && 'light' in part.iconPath && 'dark' in part.iconPath && URI.isUri(part.iconPath.light) && URI.isUri(part.iconPath.dark) ? { light: URI.revive(part.iconPath.light), dark: URI.revive(part.iconPath.dark) } - : undefined); - if ('variableName' in part.value) { - return { - kind: 'reference', - reference: { - variableName: part.value.variableName, - value: URI.isUri(part.value.value) || !part.value.value ? - part.value.value : - Location.from(part.value.value as vscode.Location) - }, - iconPath - }; - } - - return { - kind: 'reference', - reference: URI.isUri(part.value) ? - part.value : - Location.from(part.value), - iconPath - }; - } - export function to(part: Dto): vscode.ChatResponseReferencePart { - const value = revive(part); - - const mapValue = (value: URI | languages.Location): vscode.Uri | vscode.Location => URI.isUri(value) ? - value : - Location.to(value); - - return new types.AideChatResponseReferencePart( - 'variableName' in value.reference ? { - variableName: value.reference.variableName, - value: value.reference.value && mapValue(value.reference.value) - } : - mapValue(value.reference) - ) as vscode.ChatResponseReferencePart; // 'value' is extended with variableName - } -} - export namespace AideChatResponseBreakdownPart { export function from(part: vscode.AideChatResponseBreakdown): Dto { return { @@ -3250,157 +3003,12 @@ export namespace AideProbeLongContextSearchPart { } } -export namespace AideChatResponsePart { - - export function from(part: vscode.ChatResponsePart | vscode.ChatResponseTextEditPart | vscode.ChatResponseMarkdownWithVulnerabilitiesPart | vscode.ChatResponseDetectedParticipantPart | vscode.ChatResponseWarningPart | vscode.ChatResponseConfirmationPart | vscode.ChatResponseReferencePart2 | vscode.ChatResponseMovePart, commandsConverter: CommandsConverter, commandDisposables: DisposableStore): extHostProtocol.IChatProgressDto { - if (part instanceof types.AideChatResponseMarkdownPart) { - return AideChatResponseMarkdownPart.from(part); - } else if (part instanceof types.AideChatResponseAnchorPart) { - return AideChatResponseAnchorPart.from(part); - } else if (part instanceof types.AideChatResponseReferencePart) { - return AideChatResponseReferencePart.from(part); - } else if (part instanceof types.AideChatResponseProgressPart) { - return AideChatResponseProgressPart.from(part); - } else if (part instanceof types.AideChatResponseFileTreePart) { - return AideChatResponseFilesPart.from(part); - } else if (part instanceof types.AideChatResponseCommandButtonPart) { - return AideChatResponseCommandButtonPart.from(part, commandsConverter, commandDisposables); - } else if (part instanceof types.AideChatResponseTextEditPart) { - return AideChatResponseTextEditPart.from(part); - } else if (part instanceof types.AideChatResponseMarkdownWithVulnerabilitiesPart) { - return AideChatResponseMarkdownWithVulnerabilitiesPart.from(part); - } else if (part instanceof types.ChatResponseCodeblockUriPart) { - return ChatResponseCodeblockUriPart.from(part); - } else if (part instanceof types.AideChatResponseDetectedParticipantPart) { - return AideChatResponseDetectedParticipantPart.from(part); - } else if (part instanceof types.AideChatResponseWarningPart) { - return AideChatResponseWarningPart.from(part); - } else if (part instanceof types.ChatResponseConfirmationPart) { - return ChatResponseConfirmationPart.from(part); - } else if (part instanceof types.ChatResponseCodeCitationPart) { - return ChatResponseCodeCitationPart.from(part); - } else if (part instanceof types.ChatResponseMovePart) { - return ChatResponseMovePart.from(part); - } - - return { - kind: 'markdownContent', - content: MarkdownString.from('') - }; - } - - export function to(part: extHostProtocol.IChatProgressDto, commandsConverter: CommandsConverter): vscode.ChatResponsePart | undefined { - switch (part.kind) { - case 'reference': return ChatResponseReferencePart.to(part); - case 'markdownContent': - case 'inlineReference': - case 'progressMessage': - case 'treeData': - case 'command': - return toContent(part, commandsConverter); - } - return undefined; - } - - export function toContent(part: extHostProtocol.IChatContentProgressDto, commandsConverter: CommandsConverter): vscode.ChatResponseMarkdownPart | vscode.ChatResponseFileTreePart | vscode.ChatResponseAnchorPart | vscode.ChatResponseCommandButtonPart | undefined { - switch (part.kind) { - case 'markdownContent': return ChatResponseMarkdownPart.to(part); - case 'inlineReference': return ChatResponseAnchorPart.to(part); - case 'progressMessage': return undefined; - case 'treeData': return ChatResponseFilesPart.to(part); - case 'command': return ChatResponseCommandButtonPart.to(part, commandsConverter); - } - - return undefined; - } -} - -export namespace AideChatAgentRequest { - export function to(request: IAideChatAgentRequest): vscode.AideChatRequest { - return { - threadId: request.sessionId, - prompt: request.message, - command: request.command, - attempt: request.attempt ?? 0, - enableCommandDetection: request.enableCommandDetection ?? true, - references: request.variables.variables.map(ChatAgentValueReference.to), - location: AideChatLocation.to(request.location), - acceptedConfirmationData: request.acceptedConfirmationData, - rejectedConfirmationData: request.rejectedConfirmationData - }; - } -} - -export namespace AideChatLocation { - export function to(loc: AideChatAgentLocation): types.AideChatLocation { - switch (loc) { - case AideChatAgentLocation.Notebook: return types.AideChatLocation.Notebook; - case AideChatAgentLocation.Terminal: return types.AideChatLocation.Terminal; - case AideChatAgentLocation.Panel: return types.AideChatLocation.Panel; - case AideChatAgentLocation.Editor: return types.AideChatLocation.Editor; - } - } - - export function from(loc: types.AideChatLocation): AideChatAgentLocation { - switch (loc) { - case types.AideChatLocation.Notebook: return AideChatAgentLocation.Notebook; - case types.AideChatLocation.Terminal: return AideChatAgentLocation.Terminal; - case types.AideChatLocation.Panel: return AideChatAgentLocation.Panel; - case types.AideChatLocation.Editor: return AideChatAgentLocation.Editor; - } - } -} - -export namespace AideChatAgentValueReference { - export function to(variable: IAideChatRequestVariableEntry): vscode.ChatPromptReference { - const value = variable.value; - if (!value) { - throw new Error('Invalid value reference'); - } - - return { - id: variable.id, - name: variable.name, - range: variable.range && [variable.range.start, variable.range.endExclusive], - value: isUriComponents(value) ? URI.revive(value) : - value && typeof value === 'object' && 'uri' in value && 'range' in value && isUriComponents(value.uri) ? - Location.to(revive(value)) : value, - modelDescription: variable.modelDescription - }; - } -} - -export namespace AideChatAgentCompletionItem { - export function from(item: vscode.ChatCompletionItem, commandsConverter: CommandsConverter, disposables: DisposableStore): extHostProtocol.IChatAgentCompletionItem { - return { - id: item.id, - label: item.label, - fullName: item.fullName, - icon: item.icon?.id, - value: item.values[0].value, - insertText: item.insertText, - detail: item.detail, - documentation: item.documentation, - command: commandsConverter.toInternal(item.command, disposables), - }; - } -} - -export namespace AideChatAgentResult { - export function to(result: IAideChatAgentResult): vscode.ChatResult { - return { - errorDetails: result.errorDetails, - metadata: result.metadata, - }; - } -} - export namespace AideProbeRequestModel { export function to(request: IAideProbeRequestModel): vscode.ProbeRequest { return { requestId: request.sessionId, query: request.message, - references: request.variableData.variables.map(AideChatAgentValueReference.to), + references: [], mode: request.mode, codebaseSearch: request.codebaseSearch, }; @@ -3422,32 +3030,6 @@ export namespace AideProbeUserAction { return userAction; } } - -export namespace AideChatAgentUserActionEvent { - export function to(result: IAideChatAgentResult, event: IAideChatUserActionEvent, commandsConverter: CommandsConverter): vscode.ChatUserActionEvent | undefined { - if (event.action.kind === 'vote') { - // Is the "feedback" type - return; - } - - const ehResult = AideChatAgentResult.to(result); - if (event.action.kind === 'command') { - const command = event.action.commandButton.command; - const commandButton = { - command: commandsConverter.fromInternal(command) ?? { command: command.id, title: command.title }, - }; - const commandAction: vscode.ChatCommandAction = { kind: 'command', commandButton }; - return { action: commandAction, result: ehResult }; - } else if (event.action.kind === 'followUp') { - const followupAction: vscode.ChatFollowupAction = { kind: 'followUp', followup: ChatFollowup.to(event.action.followup) }; - return { action: followupAction, result: ehResult }; - } else if (event.action.kind === 'inlineChat') { - return { action: { kind: 'editor', accepted: event.action.action === 'accepted' }, result: ehResult }; - } else { - return { action: event.action, result: ehResult }; - } - } -} ///////////////////////////// END AIDE ///////////////////////////// export namespace TerminalQuickFix { diff --git a/src/vs/workbench/api/common/extHostTypes.ts b/src/vs/workbench/api/common/extHostTypes.ts index b92af7e0cd6..0c63c58cec1 100644 --- a/src/vs/workbench/api/common/extHostTypes.ts +++ b/src/vs/workbench/api/common/extHostTypes.ts @@ -4302,10 +4302,6 @@ export class ChatEditorTabInput { constructor() { } } -export class AideChatEditorTabInput { - constructor() { } -} - export class TextMultiDiffTabInput { constructor(readonly textDiffs: TextDiffTabInput[]) { } } @@ -4693,178 +4689,6 @@ export class LanguageModelError extends Error { //#endregion //#region AideChat - -export enum AideChatCopyKind { - Action = 1, - Toolbar = 2 -} - -export enum AideChatVariableLevel { - Short = 1, - Medium = 2, - Full = 3 -} - -export class AideChatCompletionItem implements vscode.ChatCompletionItem { - id: string; - label: string | CompletionItemLabel; - fullName?: string | undefined; - icon?: vscode.ThemeIcon; - insertText?: string; - values: vscode.ChatVariableValue[]; - detail?: string; - documentation?: string | MarkdownString; - command?: vscode.Command; - - constructor(id: string, label: string | CompletionItemLabel, values: vscode.ChatVariableValue[]) { - this.id = id; - this.label = label; - this.values = values; - } -} - -export enum AideChatResultFeedbackKind { - Unhelpful = 0, - Helpful = 1, -} - -export class AideChatResponseMarkdownPart { - value: vscode.MarkdownString; - constructor(value: string | vscode.MarkdownString) { - if (typeof value !== 'string' && value.isTrusted === true) { - throw new Error('The boolean form of MarkdownString.isTrusted is NOT supported for chat participants.'); - } - - this.value = typeof value === 'string' ? new MarkdownString(value) : value; - } -} - -/** - * TODO if 'vulnerabilities' is finalized, this should be merged with the base ChatResponseMarkdownPart. I just don't see how to do that while keeping - * vulnerabilities in a seperate API proposal in a clean way. - */ -export class AideChatResponseMarkdownWithVulnerabilitiesPart { - value: vscode.MarkdownString; - vulnerabilities: vscode.ChatVulnerability[]; - constructor(value: string | vscode.MarkdownString, vulnerabilities: vscode.ChatVulnerability[]) { - if (typeof value !== 'string' && value.isTrusted === true) { - throw new Error('The boolean form of MarkdownString.isTrusted is NOT supported for chat participants.'); - } - - this.value = typeof value === 'string' ? new MarkdownString(value) : value; - this.vulnerabilities = vulnerabilities; - } -} - -export class AideChatResponseDetectedParticipantPart { - participant: string; - // TODO@API validate this against statically-declared slash commands? - command?: vscode.ChatCommand; - constructor(participant: string, command?: vscode.ChatCommand) { - this.participant = participant; - this.command = command; - } -} - -export class AideChatResponseConfirmationPart { - title: string; - message: string; - data: any; - constructor(title: string, message: string, data: any) { - this.title = title; - this.message = message; - this.data = data; - } -} - -export class AideChatResponseFileTreePart { - value: vscode.ChatResponseFileTree[]; - baseUri: vscode.Uri; - constructor(value: vscode.ChatResponseFileTree[], baseUri: vscode.Uri) { - this.value = value; - this.baseUri = baseUri; - } -} - -export class AideChatResponseAnchorPart { - value: vscode.Uri | vscode.Location; - value2: vscode.Uri | vscode.Location | vscode.SymbolInformation; - title?: string; - constructor(value: vscode.Uri | vscode.Location | vscode.SymbolInformation, title?: string) { - this.value = value as any; - this.value2 = value; - this.title = title; - } -} - -export class AideChatResponseProgressPart { - value: string; - constructor(value: string) { - this.value = value; - } -} - -export class AideChatResponseProgressPart2 { - value: string; - task?: (progress: vscode.Progress) => Thenable; - constructor(value: string, task?: (progress: vscode.Progress) => Thenable) { - this.value = value; - this.task = task; - } -} - -export class AideChatResponseWarningPart { - value: vscode.MarkdownString; - constructor(value: string | vscode.MarkdownString) { - if (typeof value !== 'string' && value.isTrusted === true) { - throw new Error('The boolean form of MarkdownString.isTrusted is NOT supported for chat participants.'); - } - - this.value = typeof value === 'string' ? new MarkdownString(value) : value; - } -} - -export class AideChatResponseCommandButtonPart { - value: vscode.Command; - constructor(value: vscode.Command) { - this.value = value; - } -} - -export class AideChatResponseReferencePart { - value: vscode.Uri | vscode.Location | { variableName: string; value?: vscode.Uri | vscode.Location }; - iconPath?: vscode.Uri | vscode.ThemeIcon | { light: vscode.Uri; dark: vscode.Uri }; - constructor(value: vscode.Uri | vscode.Location | { variableName: string; value?: vscode.Uri | vscode.Location }, iconPath?: vscode.Uri | vscode.ThemeIcon | { light: vscode.Uri; dark: vscode.Uri }) { - this.value = value; - this.iconPath = iconPath; - } -} - -export class AideChatResponseBreakdownPart { - reference: vscode.CodeReferenceByName; - query?: vscode.MarkdownString; - reason?: vscode.MarkdownString; - response?: vscode.MarkdownString; - constructor( - uri: vscode.Uri, - name: string, - query?: vscode.MarkdownString, - reason?: vscode.MarkdownString, - response?: vscode.MarkdownString, - ) { - if ((typeof query !== 'string' && query?.isTrusted === true) - || (typeof reason !== 'string' && reason?.isTrusted === true) - || (typeof response !== 'string' && response?.isTrusted === true)) { - throw new Error('The boolean form of MarkdownString.isTrusted is NOT supported for chat participants.'); - } - - this.reference = { uri, name }; - this.query = typeof query === 'string' ? new MarkdownString(query) : query; - this.reason = typeof reason === 'string' ? new MarkdownString(reason) : reason; - this.response = typeof response === 'string' ? new MarkdownString(response) : response; - } -} - export class AideProbeOpenFilePart { uri: vscode.Uri; constructor(uri: vscode.Uri) { @@ -4933,41 +4757,6 @@ export class AideProbeInitialSymbolsPart { } } -export class AideChatResponseTextEditPart { - uri: vscode.Uri; - edits: vscode.TextEdit[]; - constructor(uri: vscode.Uri, edits: vscode.TextEdit | vscode.TextEdit[]) { - this.uri = uri; - this.edits = Array.isArray(edits) ? edits : [edits]; - } -} - -export class AideChatRequestTurn implements vscode.ChatRequestTurn { - constructor( - readonly prompt: string, - readonly command: string | undefined, - readonly references: vscode.ChatPromptReference[], - readonly participant: string, - ) { } -} - -export class AideChatResponseTurn implements vscode.ChatResponseTurn { - - constructor( - readonly response: ReadonlyArray, - readonly result: vscode.ChatResult, - readonly participant: string, - readonly command?: string - ) { } -} - -export enum AideChatLocation { - Panel = 1, - Terminal = 2, - Notebook = 3, - Editor = 4, -} - //#endregion //#region ai diff --git a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts index fb22aa50fe5..8d90fb7c9c2 100644 --- a/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts +++ b/src/vs/workbench/contrib/accessibility/browser/accessibilityConfiguration.ts @@ -51,7 +51,6 @@ export const enum AccessibilityVerbositySettingId { Chat = 'accessibility.verbosity.panelChat', InlineChat = 'accessibility.verbosity.inlineChat', TerminalChat = 'accessibility.verbosity.terminalChat', - AideChat = 'accessibility.verbosity.aideChat', InlineCompletions = 'accessibility.verbosity.inlineCompletions', KeybindingsEditor = 'accessibility.verbosity.keybindingsEditor', Notebook = 'accessibility.verbosity.notebook', diff --git a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatAccessibilityHelp.ts b/src/vs/workbench/contrib/aideChat/browser/actions/aideChatAccessibilityHelp.ts deleted file mode 100644 index 289d7150b58..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatAccessibilityHelp.ts +++ /dev/null @@ -1,75 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { localize } from '../../../../../nls.js'; -import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; -import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; -import { IAideChatWidgetService } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { AccessibleViewProviderId, AccessibleViewType } from '../../../../../platform/accessibility/browser/accessibleView.js'; -import { AccessibilityVerbositySettingId } from '../../../../../workbench/contrib/accessibility/browser/accessibilityConfiguration.js'; -import { AccessibleDiffViewerNext } from '../../../../../editor/browser/widget/diffEditor/commands.js'; -import { INLINE_CHAT_ID } from '../../../../../workbench/contrib/inlineAideChat/common/inlineChat.js'; - -export function getAccessibilityHelpText(type: 'panelChat' | 'inlineChat'): string { - const content = []; - if (type === 'panelChat') { - content.push(localize('aideChat.overview', 'The chat view is comprised of an input box and a request/response list. The input box is used to make requests and the list is used to display responses.')); - content.push(localize('aideChat.requestHistory', 'In the input box, use up and down arrows to navigate your request history. Edit input and use enter or the submit button to run a new request.')); - content.push(localize('aideChat.inspectResponse', 'In the input box, inspect the last response in the accessible view')); - content.push(localize('aideChat.followUp', 'In the input box, navigate to the suggested follow up question (Shift+Tab) and press Enter to run it.')); - content.push(localize('aideChat.announcement', 'Chat responses will be announced as they come in. A response will indicate the number of code blocks, if any, and then the rest of the response.')); - content.push(localize('workbench.action.aideChat.focus', 'To focus the chat request/response list, which can be navigated with up and down arrows, invoke the Focus Chat command.')); - content.push(localize('workbench.action.aideChat.focusInput', 'To focus the input box for chat requests, invoke the Focus Chat Input command.')); - content.push(localize('workbench.action.aideChat.nextCodeBlock', 'To focus the next code block within a response, invoke the Chat: Next Code Block command.')); - content.push(localize('workbench.action.aideChat.nextFileTree', 'To focus the next file tree within a response, invoke the Chat: Next File Tree command.')); - content.push(localize('workbench.action.aideChat.clear', 'To clear the request/response list, invoke the Chat Clear command.')); - } else { - content.push(localize('inlineChat.overview', "Inline chat occurs within a code editor and takes into account the current selection. It is useful for making changes to the current editor. For example, fixing diagnostics, documenting or refactoring code. Keep in mind that AI generated code may be incorrect.")); - content.push(localize('inlineChat.access', "It can be activated via code actions or directly using the command: Inline Chat: Start Inline Chat.")); - content.push(localize('inlineChat.requestHistory', 'In the input box, use and to navigate your request history. Edit input and use enter or the submit button to run a new request.')); - content.push(localize('inlineChat.inspectResponse', 'In the input box, inspect the response in the accessible viewview')); - content.push(localize('inlineChat.contextActions', "Context menu actions may run a request prefixed with a /. Type / to discover such ready-made commands.")); - content.push(localize('inlineChat.fix', "If a fix action is invoked, a response will indicate the problem with the current code. A diff editor will be rendered and can be reached by tabbing.")); - content.push(localize('inlineChat.diff', "Once in the diff editor, enter review mode with. Use up and down arrows to navigate lines with the proposed changes.", AccessibleDiffViewerNext.id)); - content.push(localize('inlineChat.toolbar', "Use tab to reach conditional parts like commands, status, message responses and more.")); - } - content.push(localize('aideChat.signals', "Accessibility Signals can be changed via settings with a prefix of signals.chat. By default, if a request takes more than 4 seconds, you will hear a sound indicating that progress is still occurring.")); - return content.join('\n\n'); -} - -export function getChatAccessibilityHelpProvider(accessor: ServicesAccessor, editor: ICodeEditor | undefined, type: 'panelChat' | 'inlineChat') { - const widgetService = accessor.get(IAideChatWidgetService); - const inputEditor: ICodeEditor | undefined = type === 'panelChat' ? widgetService.lastFocusedWidget?.inputEditor : editor; - - if (!inputEditor) { - return; - } - const domNode = inputEditor.getDomNode() ?? undefined; - if (!domNode) { - return; - } - - const cachedPosition = inputEditor.getPosition(); - inputEditor.getSupportedActions(); - const helpText = getAccessibilityHelpText(type); - return { - id: type === 'panelChat' ? AccessibleViewProviderId.Chat : AccessibleViewProviderId.InlineChat, - verbositySettingKey: type === 'panelChat' ? AccessibilityVerbositySettingId.Chat : AccessibilityVerbositySettingId.InlineChat, - provideContent: () => helpText, - onClose: () => { - if (type === 'panelChat' && cachedPosition) { - inputEditor.setPosition(cachedPosition); - inputEditor.focus(); - - } else if (type === 'inlineChat') { - // TODO@jrieken find a better way for this - const ctrl = <{ focus(): void } | undefined>editor?.getContribution(INLINE_CHAT_ID); - ctrl?.focus(); - - } - }, - options: { type: AccessibleViewType.Help } - }; -} diff --git a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatActions.ts b/src/vs/workbench/contrib/aideChat/browser/actions/aideChatActions.ts deleted file mode 100644 index 7c1e38f88ad..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatActions.ts +++ /dev/null @@ -1,295 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Codicon } from '../../../../../base/common/codicons.js'; -import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; -import { DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; -import { EditorAction2, ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; -import { localize, localize2 } from '../../../../../nls.js'; -import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; -import { IsLinuxContext, IsWindowsContext } from '../../../../../platform/contextkey/common/contextkeys.js'; -import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { IQuickInputButton, IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js'; -import { CHAT_VIEW_ID, IAideChatWidgetService, showChatView } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { IChatEditorOptions } from '../../../../../workbench/contrib/aideChat/browser/aideChatEditor.js'; -import { AideChatEditorInput } from '../../../../../workbench/contrib/aideChat/browser/aideChatEditorInput.js'; -import { ChatViewPane } from '../../../../../workbench/contrib/aideChat/browser/aideChatViewPane.js'; -import { AideChatAgentLocation } from '../../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { CONTEXT_CHAT_ENABLED, CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION } from '../../../../../workbench/contrib/aideChat/common/aideChatContextKeys.js'; -import { IChatDetail, IAideChatService } from '../../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM } from '../../../../../workbench/contrib/aideChat/common/aideChatViewModel.js'; -import { IAideChatWidgetHistoryService } from '../../../../../workbench/contrib/aideChat/common/aideChatWidgetHistoryService.js'; -import { ACTIVE_GROUP, IEditorService } from '../../../../../workbench/services/editor/common/editorService.js'; -import { IViewsService } from '../../../../../workbench/services/views/common/viewsService.js'; - -export interface IChatViewTitleActionContext { - chatView: ChatViewPane; -} - -export function isChatViewTitleActionContext(obj: unknown): obj is IChatViewTitleActionContext { - return obj instanceof Object && 'chatView' in obj; -} - -export const CHAT_CATEGORY = localize2('aideChat.category', 'Aide'); -export const CHAT_OPEN_ACTION_ID = 'workbench.action.aideChat.open'; - -export interface IChatViewOpenOptions { - /** - * The query for quick chat. - */ - query: string; - /** - * Whether the query is partial and will await more input from the user. - */ - isPartialQuery?: boolean; - /** - * Any previous chat requests and responses that should be shown in the chat view. - */ - previousRequests?: IChatViewOpenRequestEntry[]; -} - -export interface IChatViewOpenRequestEntry { - request: string; - response: string; -} - -class OpenChatGlobalAction extends Action2 { - constructor() { - super({ - id: CHAT_OPEN_ACTION_ID, - title: localize2('openChat', "Open Chat"), - icon: Codicon.commentDiscussion, - f1: false, - category: CHAT_CATEGORY, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyI, - mac: { - primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyI - } - } - }); - } - - override async run(accessor: ServicesAccessor, opts?: string | IChatViewOpenOptions): Promise { - opts = typeof opts === 'string' ? { query: opts } : opts; - - const chatService = accessor.get(IAideChatService); - const chatWidget = await showChatView(accessor.get(IViewsService)); - if (!chatWidget) { - return; - } - if (opts?.previousRequests?.length && chatWidget.viewModel) { - for (const { request, response } of opts.previousRequests) { - chatService.addCompleteRequest(chatWidget.viewModel.sessionId, request, undefined, 0, { message: response }); - } - } - if (opts?.query) { - if (opts.isPartialQuery) { - chatWidget.setInput(opts.query); - } else { - chatWidget.acceptInput(opts.query); - } - } - - chatWidget.focusInput(); - } -} - -class ChatHistoryAction extends Action2 { - constructor() { - super({ - id: `workbench.action.aideChat.history`, - title: localize2('aideChat.history.label', "Show Chats..."), - menu: { - id: MenuId.ViewTitle, - when: ContextKeyExpr.equals('view', CHAT_VIEW_ID), - group: 'navigation', - order: -1 - }, - category: CHAT_CATEGORY, - icon: Codicon.history, - f1: true, - precondition: CONTEXT_CHAT_ENABLED - }); - } - - async run(accessor: ServicesAccessor) { - const chatService = accessor.get(IAideChatService); - const quickInputService = accessor.get(IQuickInputService); - const viewsService = accessor.get(IViewsService); - const editorService = accessor.get(IEditorService); - - const openInEditorButton: IQuickInputButton = { - iconClass: ThemeIcon.asClassName(Codicon.file), - tooltip: localize('aideChat.history.editor', "Open in Editor"), - }; - const deleteButton: IQuickInputButton = { - iconClass: ThemeIcon.asClassName(Codicon.x), - tooltip: localize('aideChat.history.delete', "Delete"), - }; - - interface IChatPickerItem extends IQuickPickItem { - chat: IChatDetail; - } - - const getPicks = () => { - const items = chatService.getHistory(); - return items.map((i): IChatPickerItem => ({ - label: i.title, - chat: i, - buttons: [ - openInEditorButton, - deleteButton - ] - })); - }; - - const store = new DisposableStore(); - const picker = store.add(quickInputService.createQuickPick()); - picker.placeholder = localize('aideChat.history.pick', "Switch to chat"); - picker.items = getPicks(); - store.add(picker.onDidTriggerItemButton(context => { - if (context.button === openInEditorButton) { - editorService.openEditor({ resource: AideChatEditorInput.getNewEditorUri(), options: { target: { sessionId: context.item.chat.sessionId }, pinned: true } }, ACTIVE_GROUP); - picker.hide(); - } else if (context.button === deleteButton) { - chatService.removeHistoryEntry(context.item.chat.sessionId); - picker.items = getPicks(); - } - })); - store.add(picker.onDidAccept(async () => { - try { - const item = picker.selectedItems[0]; - const sessionId = item.chat.sessionId; - const view = await viewsService.openView(CHAT_VIEW_ID) as ChatViewPane; - view.loadSession(sessionId); - } finally { - picker.hide(); - } - })); - store.add(picker.onDidHide(() => store.dispose())); - - picker.show(); - } -} - -class OpenChatEditorAction extends Action2 { - constructor() { - super({ - id: `workbench.action.openAideChat`, - title: localize2('aideChat.open', "Open Editor"), - f1: true, - category: CHAT_CATEGORY, - precondition: CONTEXT_CHAT_ENABLED - }); - } - - async run(accessor: ServicesAccessor) { - const editorService = accessor.get(IEditorService); - await editorService.openEditor({ resource: AideChatEditorInput.getNewEditorUri(), options: { pinned: true } satisfies IChatEditorOptions }); - } -} - -export function registerChatActions() { - registerAction2(OpenChatGlobalAction); - registerAction2(ChatHistoryAction); - registerAction2(OpenChatEditorAction); - - registerAction2(class ClearChatInputHistoryAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.aideChat.clearInputHistory', - title: localize2('aideChat.clearHistory.label', "Clear Input History"), - precondition: CONTEXT_CHAT_ENABLED, - category: CHAT_CATEGORY, - f1: true, - }); - } - async run(accessor: ServicesAccessor, ...args: any[]) { - const historyService = accessor.get(IAideChatWidgetHistoryService); - historyService.clearHistory(); - } - }); - - registerAction2(class ClearChatHistoryAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.aideChat.clearHistory', - title: localize2('aideChat.clear.label', "Clear All Workspace Chats"), - precondition: CONTEXT_CHAT_ENABLED, - category: CHAT_CATEGORY, - f1: true, - }); - } - async run(accessor: ServicesAccessor, ...args: any[]) { - const chatService = accessor.get(IAideChatService); - chatService.clearAllHistoryEntries(); - } - }); - - registerAction2(class FocusChatAction extends EditorAction2 { - constructor() { - super({ - id: 'aideChat.action.focus', - title: localize2('actions.aideChat.focus', 'Focus Chat List'), - precondition: ContextKeyExpr.and(CONTEXT_IN_CHAT_INPUT, CONTEXT_CHAT_LOCATION.isEqualTo(AideChatAgentLocation.Panel)), - category: CHAT_CATEGORY, - keybinding: [ - // On mac, require that the cursor is at the top of the input, to avoid stealing cmd+up to move the cursor to the top - { - when: CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, - primary: KeyMod.CtrlCmd | KeyCode.UpArrow, - weight: KeybindingWeight.EditorContrib, - }, - // On win/linux, ctrl+up can always focus the chat list - { - when: ContextKeyExpr.or(IsWindowsContext, IsLinuxContext), - primary: KeyMod.CtrlCmd | KeyCode.UpArrow, - weight: KeybindingWeight.EditorContrib, - } - ] - }); - } - - runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): void | Promise { - const editorUri = editor.getModel()?.uri; - if (editorUri) { - const widgetService = accessor.get(IAideChatWidgetService); - widgetService.getWidgetByInputUri(editorUri)?.focusLastMessage(); - } - } - }); - - registerAction2(class FocusChatInputAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.aideChat.focusInput', - title: localize2('aideChat.focusInput.label', "Focus Chat Input"), - f1: false, - keybinding: { - primary: KeyMod.CtrlCmd | KeyCode.DownArrow, - weight: KeybindingWeight.WorkbenchContrib, - when: ContextKeyExpr.and(CONTEXT_IN_CHAT_SESSION, CONTEXT_IN_CHAT_INPUT.negate()) - } - }); - } - run(accessor: ServicesAccessor, ...args: any[]) { - const widgetService = accessor.get(IAideChatWidgetService); - widgetService.lastFocusedWidget?.focusInput(); - } - }); -} - -export function stringifyItem(item: IChatRequestViewModel | IChatResponseViewModel, includeName = true): string { - if (isRequestVM(item)) { - return (includeName ? `${item.username}: ` : '') + item.messageText; - } else { - return (includeName ? `${item.username}: ` : '') + item.response.asString(); - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatClear.ts b/src/vs/workbench/contrib/aideChat/browser/actions/aideChatClear.ts deleted file mode 100644 index 71a0c47cf82..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatClear.ts +++ /dev/null @@ -1,23 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IChatEditorOptions } from '../../../../../workbench/contrib/aideChat/browser/aideChatEditor.js'; -import { AideChatEditorInput } from '../../../../../workbench/contrib/aideChat/browser/aideChatEditorInput.js'; -import { IEditorGroupsService } from '../../../../../workbench/services/editor/common/editorGroupsService.js'; -import { IEditorService } from '../../../../../workbench/services/editor/common/editorService.js'; - -export async function clearChatEditor(accessor: ServicesAccessor): Promise { - const editorService = accessor.get(IEditorService); - const editorGroupsService = accessor.get(IEditorGroupsService); - - const chatEditorInput = editorService.activeEditor; - if (chatEditorInput instanceof AideChatEditorInput) { - await editorService.replaceEditors([{ - editor: chatEditorInput, - replacement: { resource: AideChatEditorInput.getNewEditorUri(), options: { pinned: true } satisfies IChatEditorOptions } - }], editorGroupsService.activeGroup); - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatClearActions.ts b/src/vs/workbench/contrib/aideChat/browser/actions/aideChatClearActions.ts deleted file mode 100644 index e0f7f983354..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatClearActions.ts +++ /dev/null @@ -1,147 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Codicon } from '../../../../../base/common/codicons.js'; -import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; -import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; -import { localize2 } from '../../../../../nls.js'; -import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; -import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; -import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { ActiveEditorContext } from '../../../../../workbench/common/contextkeys.js'; -import { CHAT_CATEGORY, isChatViewTitleActionContext } from '../../../../../workbench/contrib/aideChat/browser/actions/aideChatActions.js'; -import { clearChatEditor } from '../../../../../workbench/contrib/aideChat/browser/actions/aideChatClear.js'; -import { CHAT_VIEW_ID, IAideChatWidgetService } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { AideChatEditorInput } from '../../../../../workbench/contrib/aideChat/browser/aideChatEditorInput.js'; -import { ChatViewPane } from '../../../../../workbench/contrib/aideChat/browser/aideChatViewPane.js'; -import { CONTEXT_IN_CHAT_SESSION, CONTEXT_CHAT_ENABLED, CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_CHAT_HAS_REQUESTS } from '../../../../../workbench/contrib/aideChat/common/aideChatContextKeys.js'; -import { IViewsService } from '../../../../../workbench/services/views/common/viewsService.js'; - -export const ACTION_ID_NEW_CHAT = `workbench.action.aideChat.newChat`; - -export class ClearChatEditorAction extends Action2 { - static readonly ID = 'workbench.action.aideChatEditor.clearChat'; - - constructor() { - super({ - id: ClearChatEditorAction.ID, - title: localize2('aideChat.clearChat.label', "Clear"), - f1: false, - precondition: CONTEXT_CHAT_ENABLED, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyCode.KeyL, - mac: { - primary: KeyMod.WinCtrl | KeyCode.KeyL - }, - when: CONTEXT_IN_CHAT_SESSION - }, - menu: [{ - id: MenuId.AideChatExecute, - when: ContextKeyExpr.and(CONTEXT_IN_CHAT_SESSION, CONTEXT_CHAT_HAS_REQUESTS, CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate()), - group: 'navigation', - order: -1 - }] - }); - } - async run(accessor: ServicesAccessor, ...args: any[]) { - const widgetService = accessor.get(IAideChatWidgetService); - - const widget = widgetService.lastFocusedWidget; - if (!widget) { - return; - } - announceChatCleared(accessor.get(IAccessibilitySignalService)); - widget.clear(); - widget.focusInput(); - } -} - -export function registerNewChatActions() { - registerAction2(ClearChatEditorAction); - - registerAction2(class NewChatEditorAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.aideChatEditor.newChat', - title: localize2('aideChat.newChat.label', "New Chat"), - icon: Codicon.plus, - f1: false, - precondition: CONTEXT_CHAT_ENABLED, - menu: [{ - id: MenuId.EditorTitle, - group: 'navigation', - order: 0, - when: ActiveEditorContext.isEqualTo(AideChatEditorInput.EditorID), - }] - }); - } - async run(accessor: ServicesAccessor, ...args: any[]) { - announceChatCleared(accessor.get(IAccessibilitySignalService)); - await clearChatEditor(accessor); - } - }); - - registerAction2(class GlobalClearChatAction extends Action2 { - constructor() { - super({ - id: ACTION_ID_NEW_CHAT, - title: localize2('aideChat.newChat.label', "New Chat"), - category: CHAT_CATEGORY, - icon: Codicon.plus, - precondition: CONTEXT_CHAT_ENABLED, - f1: true, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyCode.KeyL, - mac: { - primary: KeyMod.WinCtrl | KeyCode.KeyL - }, - when: CONTEXT_IN_CHAT_SESSION - }, - menu: [{ - id: MenuId.AideChatContext, - group: 'z_clear' - }, - { - id: MenuId.ViewTitle, - when: ContextKeyExpr.equals('view', CHAT_VIEW_ID), - group: 'navigation', - order: -1 - }] - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - const context = args[0]; - const accessibilitySignalService = accessor.get(IAccessibilitySignalService); - if (isChatViewTitleActionContext(context)) { - // Is running in the Chat view title - announceChatCleared(accessibilitySignalService); - context.chatView.clear(); - context.chatView.widget.focusInput(); - } else { - // Is running from f1 or keybinding - const widgetService = accessor.get(IAideChatWidgetService); - const viewsService = accessor.get(IViewsService); - - let widget = widgetService.lastFocusedWidget; - if (!widget) { - const chatView = await viewsService.openView(CHAT_VIEW_ID) as ChatViewPane; - widget = chatView.widget; - } - - announceChatCleared(accessibilitySignalService); - widget.clear(); - widget.focusInput(); - } - } - }); -} - -function announceChatCleared(accessibilitySignalService: IAccessibilitySignalService): void { - accessibilitySignalService.playSignal(AccessibilitySignal.clear); -} diff --git a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatCodeblockActions.ts b/src/vs/workbench/contrib/aideChat/browser/actions/aideChatCodeblockActions.ts deleted file mode 100644 index d934f1937ef..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatCodeblockActions.ts +++ /dev/null @@ -1,683 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; -import { Codicon } from '../../../../../base/common/codicons.js'; -import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; -import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../../editor/browser/editorBrowser.js'; -import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; -import { IBulkEditService, ResourceTextEdit } from '../../../../../editor/browser/services/bulkEditService.js'; -import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js'; -import { Range } from '../../../../../editor/common/core/range.js'; -import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js'; -import { DocumentContextItem, WorkspaceEdit } from '../../../../../editor/common/languages.js'; -import { ILanguageService } from '../../../../../editor/common/languages/language.js'; -import { ITextModel } from '../../../../../editor/common/model.js'; -import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; -import { CopyAction } from '../../../../../editor/contrib/clipboard/browser/clipboard.js'; -import { localize, localize2 } from '../../../../../nls.js'; -import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; -import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { INotificationService, Severity } from '../../../../../platform/notification/common/notification.js'; -import { IProgressService, ProgressLocation } from '../../../../../platform/progress/common/progress.js'; -import { TerminalLocation } from '../../../../../platform/terminal/common/terminal.js'; -import { IUntitledTextResourceEditorInput } from '../../../../../workbench/common/editor.js'; -import { accessibleViewInCodeBlock } from '../../../../../workbench/contrib/accessibility/browser/accessibilityConfiguration.js'; -import { CHAT_CATEGORY } from '../../../../../workbench/contrib/aideChat/browser/actions/aideChatActions.js'; -import { IAideChatWidgetService, IAideChatCodeBlockContextProviderService } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { DefaultChatTextEditor, ICodeBlockActionContext, ICodeCompareBlockActionContext } from '../../../../../workbench/contrib/aideChat/browser/codeBlockPart.js'; -import { CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_CHAT_ENABLED, CONTEXT_CHAT_EDIT_APPLIED } from '../../../../../workbench/contrib/aideChat/common/aideChatContextKeys.js'; -import { ChatCopyKind, IAideChatService, IDocumentContext } from '../../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { IChatResponseViewModel, isResponseVM } from '../../../../../workbench/contrib/aideChat/common/aideChatViewModel.js'; -import { insertCell } from '../../../../../workbench/contrib/notebook/browser/controller/cellOperations.js'; -import { INotebookEditor } from '../../../../../workbench/contrib/notebook/browser/notebookBrowser.js'; -import { CellKind, NOTEBOOK_EDITOR_ID } from '../../../../../workbench/contrib/notebook/common/notebookCommon.js'; -import { ITerminalEditorService, ITerminalGroupService, ITerminalService } from '../../../../../workbench/contrib/terminal/browser/terminal.js'; -import { IEditorService } from '../../../../../workbench/services/editor/common/editorService.js'; -import { ITextFileService } from '../../../../../workbench/services/textfile/common/textfiles.js'; - -export interface IChatCodeBlockActionContext extends ICodeBlockActionContext { - element: IChatResponseViewModel; -} - -export function isCodeBlockActionContext(thing: unknown): thing is ICodeBlockActionContext { - return typeof thing === 'object' && thing !== null && 'code' in thing && 'element' in thing; -} - -export function isCodeCompareBlockActionContext(thing: unknown): thing is ICodeCompareBlockActionContext { - return typeof thing === 'object' && thing !== null && 'element' in thing; -} - -function isResponseFiltered(context: ICodeBlockActionContext) { - return isResponseVM(context.element) && context.element.errorDetails?.responseIsFiltered; -} - -function getUsedDocuments(context: ICodeBlockActionContext): IDocumentContext[] | undefined { - return isResponseVM(context.element) ? context.element.usedContext?.documents : undefined; -} - -abstract class ChatCodeBlockAction extends Action2 { - run(accessor: ServicesAccessor, ...args: any[]) { - let context = args[0]; - if (!isCodeBlockActionContext(context)) { - const codeEditorService = accessor.get(ICodeEditorService); - const editor = codeEditorService.getFocusedCodeEditor() || codeEditorService.getActiveCodeEditor(); - if (!editor) { - return; - } - - context = getContextFromEditor(editor, accessor); - if (!isCodeBlockActionContext(context)) { - return; - } - } - - return this.runWithContext(accessor, context); - } - - abstract runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext): any; -} - -export function registerChatCodeBlockActions() { - registerAction2(class CopyCodeBlockAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.aideChat.copyCodeBlock', - title: localize2('aideChat.copyCodeBlock.label', "Copy"), - f1: false, - category: CHAT_CATEGORY, - icon: Codicon.copy, - menu: { - id: MenuId.AideChatCodeBlock, - group: 'navigation' - } - }); - } - - run(accessor: ServicesAccessor, ...args: any[]) { - const context = args[0]; - if (!isCodeBlockActionContext(context) || isResponseFiltered(context)) { - return; - } - - const clipboardService = accessor.get(IClipboardService); - clipboardService.writeText(context.code); - - if (isResponseVM(context.element)) { - const chatService = accessor.get(IAideChatService); - chatService.notifyUserAction({ - agentId: context.element.agent?.id, - sessionId: context.element.sessionId, - requestId: context.element.requestId, - result: context.element.result, - action: { - kind: 'copy', - codeBlockIndex: context.codeBlockIndex, - copyKind: ChatCopyKind.Toolbar, - copiedCharacters: context.code.length, - totalCharacters: context.code.length, - copiedText: context.code, - } - }); - } - } - }); - - CopyAction?.addImplementation(50000, 'chat-codeblock', (accessor) => { - // get active code editor - const editor = accessor.get(ICodeEditorService).getFocusedCodeEditor(); - if (!editor) { - return false; - } - - const editorModel = editor.getModel(); - if (!editorModel) { - return false; - } - - const context = getContextFromEditor(editor, accessor); - if (!context) { - return false; - } - - const noSelection = editor.getSelections()?.length === 1 && editor.getSelection()?.isEmpty(); - const copiedText = noSelection ? - editorModel.getValue() : - editor.getSelections()?.reduce((acc, selection) => acc + editorModel.getValueInRange(selection), '') ?? ''; - const totalCharacters = editorModel.getValueLength(); - - // Report copy to extensions - const chatService = accessor.get(IAideChatService); - const element = context.element as IChatResponseViewModel | undefined; - if (element) { - chatService.notifyUserAction({ - agentId: element.agent?.id, - sessionId: element.sessionId, - requestId: element.requestId, - result: element.result, - action: { - kind: 'copy', - codeBlockIndex: context.codeBlockIndex, - copyKind: ChatCopyKind.Action, - copiedText, - copiedCharacters: copiedText.length, - totalCharacters, - } - }); - } - - // Copy full cell if no selection, otherwise fall back on normal editor implementation - if (noSelection) { - accessor.get(IClipboardService).writeText(context.code); - return true; - } - - return false; - }); - - registerAction2(class InsertCodeBlockAction extends ChatCodeBlockAction { - constructor() { - super({ - id: 'workbench.action.aideChat.insertCodeBlock', - title: localize2('aideChat.insertCodeBlock.label', "Insert at Cursor"), - precondition: CONTEXT_CHAT_ENABLED, - f1: true, - category: CHAT_CATEGORY, - icon: Codicon.insert, - menu: { - id: MenuId.AideChatCodeBlock, - group: 'navigation', - when: CONTEXT_IN_CHAT_SESSION - }, - keybinding: { - when: ContextKeyExpr.or(ContextKeyExpr.and(CONTEXT_IN_CHAT_SESSION, CONTEXT_IN_CHAT_INPUT.negate()), accessibleViewInCodeBlock), - primary: KeyMod.CtrlCmd | KeyCode.Enter, - mac: { primary: KeyMod.WinCtrl | KeyCode.Enter }, - weight: KeybindingWeight.ExternalExtension + 1 - }, - }); - } - - override async runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext) { - const editorService = accessor.get(IEditorService); - const textFileService = accessor.get(ITextFileService); - - if (isResponseFiltered(context)) { - // When run from command palette - return; - } - - if (editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) { - return this.handleNotebookEditor(accessor, editorService.activeEditorPane.getControl() as INotebookEditor, context); - } - - let activeEditorControl = editorService.activeTextEditorControl; - if (isDiffEditor(activeEditorControl)) { - activeEditorControl = activeEditorControl.getOriginalEditor().hasTextFocus() ? activeEditorControl.getOriginalEditor() : activeEditorControl.getModifiedEditor(); - } - - if (!isCodeEditor(activeEditorControl)) { - return; - } - - const activeModel = activeEditorControl.getModel(); - if (!activeModel) { - return; - } - - // Check if model is editable, currently only support untitled and text file - const activeTextModel = textFileService.files.get(activeModel.uri) ?? textFileService.untitled.get(activeModel.uri); - if (!activeTextModel || activeTextModel.isReadonly()) { - return; - } - - await this.handleTextEditor(accessor, activeEditorControl, activeModel, context); - } - - private async handleNotebookEditor(accessor: ServicesAccessor, notebookEditor: INotebookEditor, context: ICodeBlockActionContext) { - if (!notebookEditor.hasModel()) { - return; - } - - if (notebookEditor.isReadOnly) { - return; - } - - if (notebookEditor.activeCodeEditor?.hasTextFocus()) { - const codeEditor = notebookEditor.activeCodeEditor; - const textModel = codeEditor.getModel(); - - if (textModel) { - return this.handleTextEditor(accessor, codeEditor, textModel, context); - } - } - - const languageService = accessor.get(ILanguageService); - const focusRange = notebookEditor.getFocus(); - const next = Math.max(focusRange.end - 1, 0); - insertCell(languageService, notebookEditor, next, CellKind.Code, 'below', context.code, true); - this.notifyUserAction(accessor, context); - } - - private async handleTextEditor(accessor: ServicesAccessor, codeEditor: ICodeEditor, activeModel: ITextModel, codeBlockActionContext: ICodeBlockActionContext) { - this.notifyUserAction(accessor, codeBlockActionContext); - - const bulkEditService = accessor.get(IBulkEditService); - const codeEditorService = accessor.get(ICodeEditorService); - const progressService = accessor.get(IProgressService); - const notificationService = accessor.get(INotificationService); - - const mappedEditsProviders = accessor.get(ILanguageFeaturesService).mappedEditsProvider.ordered(activeModel); - - // try applying workspace edit that was returned by a MappedEditsProvider, else simply insert at selection - - let mappedEdits: WorkspaceEdit | null = null; - - if (mappedEditsProviders.length > 0) { - - // 0th sub-array - editor selections array if there are any selections - // 1st sub-array - array with documents used to get the chat reply - const docRefs: DocumentContextItem[][] = []; - - if (codeEditor.hasModel()) { - const model = codeEditor.getModel(); - const currentDocUri = model.uri; - const currentDocVersion = model.getVersionId(); - const selections = codeEditor.getSelections(); - if (selections.length > 0) { - docRefs.push([ - { - uri: currentDocUri, - version: currentDocVersion, - ranges: selections, - } - ]); - } - } - - const usedDocuments = getUsedDocuments(codeBlockActionContext); - if (usedDocuments) { - docRefs.push(usedDocuments); - } - - const cancellationTokenSource = new CancellationTokenSource(); - - try { - mappedEdits = await progressService.withProgress( - { location: ProgressLocation.Notification, delay: 500, sticky: true, cancellable: true }, - async progress => { - progress.report({ message: localize('applyCodeBlock.progress', "Applying code block...") }); - - for (const provider of mappedEditsProviders) { - const mappedEdits = await provider.provideMappedEdits( - activeModel, - [codeBlockActionContext.code], - { documents: docRefs }, - cancellationTokenSource.token - ); - if (mappedEdits) { - return mappedEdits; - } - } - return null; - }, - () => cancellationTokenSource.cancel() - ); - } catch (e) { - notificationService.notify({ severity: Severity.Error, message: localize('applyCodeBlock.error', "Failed to apply code block: {0}", e.message) }); - } finally { - cancellationTokenSource.dispose(); - } - - } - - if (mappedEdits) { - console.log('Mapped edits:', mappedEdits); - await bulkEditService.apply(mappedEdits); - } else { - const activeSelection = codeEditor.getSelection() ?? new Range(activeModel.getLineCount(), 1, activeModel.getLineCount(), 1); - await bulkEditService.apply([ - new ResourceTextEdit(activeModel.uri, { - range: activeSelection, - text: codeBlockActionContext.code, - }), - ]); - } - codeEditorService.listCodeEditors().find(editor => editor.getModel()?.uri.toString() === activeModel.uri.toString())?.focus(); - } - - private notifyUserAction(accessor: ServicesAccessor, context: ICodeBlockActionContext) { - if (isResponseVM(context.element)) { - const chatService = accessor.get(IAideChatService); - chatService.notifyUserAction({ - agentId: context.element.agent?.id, - sessionId: context.element.sessionId, - requestId: context.element.requestId, - result: context.element.result, - action: { - kind: 'insert', - codeBlockIndex: context.codeBlockIndex, - totalCharacters: context.code.length, - } - }); - } - } - - }); - - registerAction2(class InsertIntoNewFileAction extends ChatCodeBlockAction { - constructor() { - super({ - id: 'workbench.action.aideChat.insertIntoNewFile', - title: localize2('aideChat.insertIntoNewFile.label', "Insert into New File"), - precondition: CONTEXT_CHAT_ENABLED, - f1: true, - category: CHAT_CATEGORY, - icon: Codicon.newFile, - menu: { - id: MenuId.AideChatCodeBlock, - group: 'navigation', - isHiddenByDefault: true - } - }); - } - - override async runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext) { - if (isResponseFiltered(context)) { - // When run from command palette - return; - } - - const editorService = accessor.get(IEditorService); - const chatService = accessor.get(IAideChatService); - - editorService.openEditor({ contents: context.code, languageId: context.languageId, resource: undefined } satisfies IUntitledTextResourceEditorInput); - - if (isResponseVM(context.element)) { - chatService.notifyUserAction({ - agentId: context.element.agent?.id, - sessionId: context.element.sessionId, - requestId: context.element.requestId, - result: context.element.result, - action: { - kind: 'insert', - codeBlockIndex: context.codeBlockIndex, - totalCharacters: context.code.length, - newFile: true - } - }); - } - } - }); - - const shellLangIds = [ - 'fish', - 'ps1', - 'pwsh', - 'powershell', - 'sh', - 'shellscript', - 'zsh' - ]; - registerAction2(class RunInTerminalAction extends ChatCodeBlockAction { - constructor() { - super({ - id: 'workbench.action.aideChat.runInTerminal', - title: localize2('aideChat.runInTerminal.label', "Insert into Terminal"), - precondition: CONTEXT_CHAT_ENABLED, - f1: true, - category: CHAT_CATEGORY, - icon: Codicon.terminal, - menu: [{ - id: MenuId.AideChatCodeBlock, - group: 'navigation', - when: ContextKeyExpr.and( - CONTEXT_IN_CHAT_SESSION, - ContextKeyExpr.or(...shellLangIds.map(e => ContextKeyExpr.equals(EditorContextKeys.languageId.key, e))) - ), - }, - { - id: MenuId.AideChatCodeBlock, - group: 'navigation', - isHiddenByDefault: true, - when: ContextKeyExpr.and( - CONTEXT_IN_CHAT_SESSION, - ...shellLangIds.map(e => ContextKeyExpr.notEquals(EditorContextKeys.languageId.key, e)) - ) - }], - keybinding: [{ - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Enter, - mac: { - primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Enter - }, - weight: KeybindingWeight.EditorContrib, - when: ContextKeyExpr.or(CONTEXT_IN_CHAT_SESSION, accessibleViewInCodeBlock), - }] - }); - } - - override async runWithContext(accessor: ServicesAccessor, context: ICodeBlockActionContext) { - if (isResponseFiltered(context)) { - // When run from command palette - return; - } - - const chatService = accessor.get(IAideChatService); - const terminalService = accessor.get(ITerminalService); - const editorService = accessor.get(IEditorService); - const terminalEditorService = accessor.get(ITerminalEditorService); - const terminalGroupService = accessor.get(ITerminalGroupService); - - let terminal = await terminalService.getActiveOrCreateInstance(); - - // isFeatureTerminal = debug terminal or task terminal - const unusableTerminal = terminal.xterm?.isStdinDisabled || terminal.shellLaunchConfig.isFeatureTerminal; - terminal = unusableTerminal ? await terminalService.createTerminal() : terminal; - - terminalService.setActiveInstance(terminal); - await terminal.focusWhenReady(true); - if (terminal.target === TerminalLocation.Editor) { - const existingEditors = editorService.findEditors(terminal.resource); - terminalEditorService.openEditor(terminal, { viewColumn: existingEditors?.[0].groupId }); - } else { - terminalGroupService.showPanel(true); - } - - terminal.runCommand(context.code, false); - - if (isResponseVM(context.element)) { - chatService.notifyUserAction({ - agentId: context.element.agent?.id, - sessionId: context.element.sessionId, - requestId: context.element.requestId, - result: context.element.result, - action: { - kind: 'runInTerminal', - codeBlockIndex: context.codeBlockIndex, - languageId: context.languageId, - } - }); - } - } - }); - - function navigateCodeBlocks(accessor: ServicesAccessor, reverse?: boolean): void { - const codeEditorService = accessor.get(ICodeEditorService); - const chatWidgetService = accessor.get(IAideChatWidgetService); - const widget = chatWidgetService.lastFocusedWidget; - if (!widget) { - return; - } - - const editor = codeEditorService.getFocusedCodeEditor(); - const editorUri = editor?.getModel()?.uri; - const curCodeBlockInfo = editorUri ? widget.getCodeBlockInfoForEditor(editorUri) : undefined; - const focused = !widget.inputEditor.hasWidgetFocus() && widget.getFocus(); - const focusedResponse = isResponseVM(focused) ? focused : undefined; - - const currentResponse = curCodeBlockInfo ? - curCodeBlockInfo.element : - (focusedResponse ?? widget.viewModel?.getItems().reverse().find((item): item is IChatResponseViewModel => isResponseVM(item))); - if (!currentResponse || !isResponseVM(currentResponse)) { - return; - } - - widget.reveal(currentResponse); - const responseCodeblocks = widget.getCodeBlockInfosForResponse(currentResponse); - const focusIdx = curCodeBlockInfo ? - (curCodeBlockInfo.codeBlockIndex + (reverse ? -1 : 1) + responseCodeblocks.length) % responseCodeblocks.length : - reverse ? responseCodeblocks.length - 1 : 0; - - responseCodeblocks[focusIdx]?.focus(); - } - - registerAction2(class NextCodeBlockAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.aideChat.nextCodeBlock', - title: localize2('aideChat.nextCodeBlock.label', "Next Code Block"), - keybinding: { - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageDown, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageDown, }, - weight: KeybindingWeight.WorkbenchContrib, - when: CONTEXT_IN_CHAT_SESSION, - }, - precondition: CONTEXT_CHAT_ENABLED, - f1: true, - category: CHAT_CATEGORY, - }); - } - - run(accessor: ServicesAccessor, ...args: any[]) { - navigateCodeBlocks(accessor); - } - }); - - registerAction2(class PreviousCodeBlockAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.aideChat.previousCodeBlock', - title: localize2('aideChat.previousCodeBlock.label', "Previous Code Block"), - keybinding: { - primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageUp, - mac: { primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.PageUp, }, - weight: KeybindingWeight.WorkbenchContrib, - when: CONTEXT_IN_CHAT_SESSION, - }, - precondition: CONTEXT_CHAT_ENABLED, - f1: true, - category: CHAT_CATEGORY, - }); - } - - run(accessor: ServicesAccessor, ...args: any[]) { - navigateCodeBlocks(accessor, true); - } - }); -} - -function getContextFromEditor(editor: ICodeEditor, accessor: ServicesAccessor): ICodeBlockActionContext | undefined { - const chatWidgetService = accessor.get(IAideChatWidgetService); - const chatCodeBlockContextProviderService = accessor.get(IAideChatCodeBlockContextProviderService); - const model = editor.getModel(); - if (!model) { - return; - } - - const widget = chatWidgetService.lastFocusedWidget; - const codeBlockInfo = widget?.getCodeBlockInfoForEditor(model.uri); - if (!codeBlockInfo) { - for (const provider of chatCodeBlockContextProviderService.providers) { - const context = provider.getCodeBlockContext(editor); - if (context) { - return context; - } - } - return; - } - - return { - element: codeBlockInfo.element, - codeBlockIndex: codeBlockInfo.codeBlockIndex, - code: editor.getValue(), - languageId: editor.getModel()!.getLanguageId(), - }; -} - -export function registerChatCodeCompareBlockActions() { - - abstract class ChatCompareCodeBlockAction extends Action2 { - run(accessor: ServicesAccessor, ...args: any[]) { - const context = args[0]; - if (!isCodeCompareBlockActionContext(context)) { - return; - // TODO@jrieken derive context - } - - return this.runWithContext(accessor, context); - } - - abstract runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): any; - } - - registerAction2(class ApplyEditsCompareBlockAction extends ChatCompareCodeBlockAction { - constructor() { - super({ - id: 'workbench.action.aideChat.applyCompareEdits', - title: localize2('aideChat.compare.apply', "Apply Edits"), - f1: false, - category: CHAT_CATEGORY, - icon: Codicon.check, - precondition: ContextKeyExpr.and(EditorContextKeys.hasChanges, CONTEXT_CHAT_EDIT_APPLIED.negate()), - menu: { - id: MenuId.AideChatCompareBlock, - group: 'navigation', - order: 1, - } - }); - } - - async runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): Promise { - - const editorService = accessor.get(IEditorService); - const instaService = accessor.get(IInstantiationService); - - const editor = instaService.createInstance(DefaultChatTextEditor); - await editor.apply(context.element, context.edit, context.diffEditor); - - await editorService.openEditor({ - resource: context.edit.uri, - options: { revealIfVisible: true }, - }); - } - }); - - registerAction2(class DiscardEditsCompareBlockAction extends ChatCompareCodeBlockAction { - constructor() { - super({ - id: 'workbench.action.aideChat.discardCompareEdits', - title: localize2('aideChat.compare.discard', "Discard Edits"), - f1: false, - category: CHAT_CATEGORY, - icon: Codicon.trash, - precondition: ContextKeyExpr.and(EditorContextKeys.hasChanges, CONTEXT_CHAT_EDIT_APPLIED.negate()), - menu: { - id: MenuId.AideChatCompareBlock, - group: 'navigation', - order: 2, - } - }); - } - - async runWithContext(accessor: ServicesAccessor, context: ICodeCompareBlockActionContext): Promise { - const instaService = accessor.get(IInstantiationService); - const editor = instaService.createInstance(DefaultChatTextEditor); - editor.discard(context.element, context.edit); - } - }); -} diff --git a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatContextActions.ts b/src/vs/workbench/contrib/aideChat/browser/actions/aideChatContextActions.ts deleted file mode 100644 index 5f69a6ed28f..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatContextActions.ts +++ /dev/null @@ -1,356 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { Codicon } from '../../../../../base/common/codicons.js'; -import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; -import { Schemas } from '../../../../../base/common/network.js'; -import { IRange } from '../../../../../editor/common/core/range.js'; -import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; -import { Command } from '../../../../../editor/common/languages.js'; -import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from '../../../../../editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.js'; -import { localize, localize2 } from '../../../../../nls.js'; -import { Action2, MenuId } from '../../../../../platform/actions/common/actions.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { AnythingQuickAccessProviderRunOptions } from '../../../../../platform/quickinput/common/quickAccess.js'; -import { IQuickInputService, IQuickPickItem, QuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js'; -import { CHAT_CATEGORY } from '../../../../../workbench/contrib/aideChat/browser/actions/aideChatActions.js'; -import { IChatWidget, IAideChatWidgetService } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { ChatContextAttachments } from '../../../../../workbench/contrib/aideChat/browser/contrib/aideChatContextAttachments.js'; -import { AideChatAgentLocation, IAideChatAgentService } from '../../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { CONTEXT_CHAT_LOCATION, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_QUICK_CHAT } from '../../../../../workbench/contrib/aideChat/common/aideChatContextKeys.js'; -import { IAideChatRequestVariableEntry } from '../../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { ChatRequestAgentPart } from '../../../../../workbench/contrib/aideChat/common/aideChatParserTypes.js'; -import { IAideChatVariablesService } from '../../../../../workbench/contrib/aideChat/common/aideChatVariables.js'; -import { AnythingQuickAccessProvider } from '../../../../../workbench/contrib/search/browser/anythingQuickAccess.js'; -import { ISymbolQuickPickItem, SymbolsQuickAccessProvider } from '../../../../../workbench/contrib/search/browser/symbolsQuickAccess.js'; -import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; -import { IEditorService } from '../../../../../workbench/services/editor/common/editorService.js'; -import { EditorType } from '../../../../../editor/common/editorCommon.js'; -import { compare } from '../../../../../base/common/strings.js'; - -export function registerChatContextActions() { - console.log(AttachContextAction.ID); - console.log(AttachFileAction.ID); - console.log(AttachSelectionAction.ID); - // registerAction2(AttachContextAction); - // registerAction2(AttachFileAction); - // registerAction2(AttachSelectionAction); -} - -export type IChatContextQuickPickItem = IFileQuickPickItem | IDynamicVariableQuickPickItem | IStaticVariableQuickPickItem | IGotoSymbolQuickPickItem | ISymbolQuickPickItem | IQuickAccessQuickPickItem; - -export interface IFileQuickPickItem extends IQuickPickItem { - kind: 'file'; - id: string; - name: string; - value: URI; - isDynamic: true; - - resource: URI; -} - -export interface IDynamicVariableQuickPickItem extends IQuickPickItem { - kind: 'dynamic'; - id: string; - name?: string; - value: unknown; - isDynamic: true; - - icon?: ThemeIcon; - command?: Command; -} - -export interface IStaticVariableQuickPickItem extends IQuickPickItem { - kind: 'static'; - id: string; - name: string; - value: unknown; - isDynamic?: false; - - icon?: ThemeIcon; -} - -export interface IQuickAccessQuickPickItem extends IQuickPickItem { - kind: 'quickaccess'; - id: string; - name: string; - value: string; - - prefix: string; -} - -class AttachFileAction extends Action2 { - - static readonly ID = 'workbench.action.aideChat.attachFile'; - - constructor() { - super({ - id: AttachFileAction.ID, - title: localize2('workbench.action.aideChat.attachFile.label', "Attach File"), - category: CHAT_CATEGORY, - f1: false - }); - } - - override async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const variablesService = accessor.get(IAideChatVariablesService); - const textEditorService = accessor.get(IEditorService); - - const activeUri = textEditorService.activeEditor?.resource; - if (textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote].includes(activeUri.scheme)) { - variablesService.attachContext('file', activeUri, AideChatAgentLocation.Panel); - } - } -} - -class AttachSelectionAction extends Action2 { - - static readonly ID = 'workbench.action.aideChat.attachSelection'; - - constructor() { - super({ - id: AttachSelectionAction.ID, - title: localize2('workbench.action.aideChat.attachSelection.label', "Add Selection to Chat"), - category: CHAT_CATEGORY, - f1: false - }); - } - - override async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const variablesService = accessor.get(IAideChatVariablesService); - const textEditorService = accessor.get(IEditorService); - - const activeEditor = textEditorService.activeTextEditorControl; - const activeUri = textEditorService.activeEditor?.resource; - if (textEditorService.activeTextEditorControl?.getEditorType() === EditorType.ICodeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote].includes(activeUri.scheme)) { - const selection = activeEditor?.getSelection(); - if (selection) { - variablesService.attachContext('file', { uri: activeUri, range: selection }, AideChatAgentLocation.Panel); - } - } - } -} - -class AttachContextAction extends Action2 { - - static readonly ID = 'workbench.action.aideChat.attachContext'; - - constructor() { - super({ - id: AttachContextAction.ID, - title: localize2('workbench.action.aideChat.attachContext.label', "Attach Context"), - icon: Codicon.attach, - category: CHAT_CATEGORY, - keybinding: { - when: CONTEXT_IN_CHAT_INPUT, - primary: KeyMod.CtrlCmd | KeyCode.Slash, - weight: KeybindingWeight.EditorContrib - }, - menu: [ - { - when: ContextKeyExpr.and(CONTEXT_CHAT_LOCATION.isEqualTo(AideChatAgentLocation.Panel), CONTEXT_IN_QUICK_CHAT.isEqualTo(false)), - id: MenuId.AideChatExecute, - group: 'navigation', - }, - ] - }); - } - - private _getFileContextId(item: { resource: URI } | { uri: URI; range: IRange }) { - if ('resource' in item) { - return item.resource.toString(); - } - - return item.uri.toString() + (item.range.startLineNumber !== item.range.endLineNumber ? - `:${item.range.startLineNumber}-${item.range.endLineNumber}` : - `:${item.range.startLineNumber}`); - } - - private async _attachContext(widget: IChatWidget, commandService: ICommandService, ...picks: IChatContextQuickPickItem[]) { - const toAttach: IAideChatRequestVariableEntry[] = []; - for (const pick of picks) { - if (pick && typeof pick === 'object' && 'command' in pick && pick.command) { - // Dynamic variable with a followup command - const selection = await commandService.executeCommand(pick.command.id, ...(pick.command.arguments ?? [])); - if (!selection) { - // User made no selection, skip this variable - continue; - } - toAttach.push({ - ...pick, - isDynamic: pick.isDynamic, - value: pick.value, - name: `${typeof pick.value === 'string' && pick.value.startsWith('#') ? pick.value.slice(1) : ''}${selection}`, - // Apply the original icon with the new name - fullName: `${pick.icon ? `$(${pick.icon.id}) ` : ''}${selection}` - }); - } else if ('symbol' in pick && pick.symbol) { - // Symbol - toAttach.push({ - ...pick, - id: this._getFileContextId(pick.symbol.location), - value: pick.symbol.location, - fullName: pick.label, - name: pick.symbol.name, - isDynamic: true - }); - } else if (pick && typeof pick === 'object' && 'resource' in pick && pick.resource) { - // #file variable - toAttach.push({ - ...pick, - id: this._getFileContextId({ resource: pick.resource }), - value: pick.resource, - name: pick.label, - isFile: true, - isDynamic: true - }); - } else if ('symbolName' in pick && pick.uri && pick.range) { - // Symbol - toAttach.push({ - ...pick, - range: undefined, - id: this._getFileContextId({ uri: pick.uri, range: pick.range.decoration }), - value: { uri: pick.uri, range: pick.range.decoration }, - fullName: pick.label, - name: pick.symbolName!, - isDynamic: true - }); - } else { - // All other dynamic variables and static variables - toAttach.push({ - ...pick, - range: undefined, - id: pick.id ?? '', - value: 'value' in pick ? pick.value : undefined, - fullName: pick.label, - name: 'name' in pick && typeof pick.name === 'string' ? pick.name : pick.label, - icon: 'icon' in pick && ThemeIcon.isThemeIcon(pick.icon) ? pick.icon : undefined - }); - } - } - - widget.getContrib(ChatContextAttachments.ID)?.setContext(false, ...toAttach); - } - - override async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const quickInputService = accessor.get(IQuickInputService); - const chatAgentService = accessor.get(IAideChatAgentService); - const chatVariablesService = accessor.get(IAideChatVariablesService); - const commandService = accessor.get(ICommandService); - const widgetService = accessor.get(IAideChatWidgetService); - const context: { widget?: IChatWidget } | undefined = args[0]; - const widget = context?.widget ?? widgetService.lastFocusedWidget; - if (!widget) { - return; - } - - const usedAgent = widget.parsedInput.parts.find(p => p instanceof ChatRequestAgentPart); - const slowSupported = usedAgent ? usedAgent.agent.metadata.supportsSlowVariables : true; - const quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[] = []; - for (const variable of chatVariablesService.getVariables()) { - if (variable.fullName && (!variable.isSlow || slowSupported)) { - quickPickItems.push({ - label: `${variable.icon ? `$(${variable.icon.id}) ` : ''}${variable.fullName}`, - name: variable.name, - id: variable.id, - icon: variable.icon - }); - } - } - - if (widget.viewModel?.sessionId) { - const agentPart = widget.parsedInput.parts.find((part): part is ChatRequestAgentPart => part instanceof ChatRequestAgentPart); - if (agentPart) { - const completions = await chatAgentService.getAgentCompletionItems(agentPart.agent.id, '', CancellationToken.None); - for (const variable of completions) { - if (variable.fullName) { - quickPickItems.push({ - label: `${variable.icon ? `$(${variable.icon.id}) ` : ''}${variable.fullName}`, - id: variable.id, - command: variable.command, - icon: variable.icon, - value: variable.value, - isDynamic: true, - name: variable.name - }); - } - } - } - - } - - quickPickItems.push({ - label: localize('aideChatContext.symbol', '{0} Symbol...', `$(${Codicon.symbolField.id})`), - icon: ThemeIcon.fromId(Codicon.symbolField.id), - prefix: SymbolsQuickAccessProvider.PREFIX - }); - - function extractTextFromIconLabel(label: string | undefined): string { - if (!label) { - return ''; - } - const match = label.match(/\$\([^\)]+\)\s*(.+)/); - return match ? match[1] : label; - } - - this._show(quickInputService, commandService, widget, quickPickItems.sort(function (a, b) { - - const first = extractTextFromIconLabel(a.label).toUpperCase(); - const second = extractTextFromIconLabel(b.label).toUpperCase(); - - return compare(first, second); - })); - } - - private _show(quickInputService: IQuickInputService, commandService: ICommandService, widget: IChatWidget, quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[], query: string = '') { - quickInputService.quickAccess.show(query, { - enabledProviderPrefixes: [ - AnythingQuickAccessProvider.PREFIX, - SymbolsQuickAccessProvider.PREFIX, - AbstractGotoSymbolQuickAccessProvider.PREFIX - ], - placeholder: localize('aideChatContext.attach.placeholder', 'Search attachments'), - providerOptions: { - handleAccept: (item: IChatContextQuickPickItem) => { - if ('prefix' in item) { - this._show(quickInputService, commandService, widget, quickPickItems, item.prefix); - } else { - this._attachContext(widget, commandService, item); - } - }, - additionPicks: quickPickItems, - filter: (item: IChatContextQuickPickItem) => { - // Avoid attaching the same context twice - const attachedContext = widget.getContrib(ChatContextAttachments.ID)?.getContext() ?? new Set(); - - if ('symbol' in item && item.symbol) { - return !attachedContext.has(this._getFileContextId(item.symbol.location)); - } - - if (item && typeof item === 'object' && 'resource' in item && URI.isUri(item.resource)) { - return [Schemas.file, Schemas.vscodeRemote].includes(item.resource.scheme) - && !attachedContext.has(this._getFileContextId({ resource: item.resource })); // Hack because Typescript doesn't narrow this type correctly - } - - if (item && typeof item === 'object' && 'uri' in item && item.uri && item.range) { - return !attachedContext.has(this._getFileContextId({ uri: item.uri, range: item.range.decoration })); - } - - if (!('command' in item) && item.id) { - return !attachedContext.has(item.id); - } - - // Don't filter out dynamic variables which show secondary data (temporary) - return true; - } - } - }); - - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatCopyActions.ts b/src/vs/workbench/contrib/aideChat/browser/actions/aideChatCopyActions.ts deleted file mode 100644 index c3040e372b7..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatCopyActions.ts +++ /dev/null @@ -1,74 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; -import { localize2 } from '../../../../../nls.js'; -import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { IClipboardService } from '../../../../../platform/clipboard/common/clipboardService.js'; -import { CHAT_CATEGORY, stringifyItem } from '../../../../../workbench/contrib/aideChat/browser/actions/aideChatActions.js'; -import { IAideChatWidgetService } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { CONTEXT_RESPONSE_FILTERED } from '../../../../../workbench/contrib/aideChat/common/aideChatContextKeys.js'; -import { IChatRequestViewModel, IChatResponseViewModel, isRequestVM, isResponseVM } from '../../../../../workbench/contrib/aideChat/common/aideChatViewModel.js'; - -export function registerChatCopyActions() { - registerAction2(class CopyAllAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.aideChat.copyAll', - title: localize2('aideChat.copyAll.label', "Copy All"), - f1: false, - category: CHAT_CATEGORY, - menu: { - id: MenuId.AideChatContext, - when: CONTEXT_RESPONSE_FILTERED.toNegated(), - group: 'copy', - } - }); - } - - run(accessor: ServicesAccessor, ...args: any[]) { - const clipboardService = accessor.get(IClipboardService); - const chatWidgetService = accessor.get(IAideChatWidgetService); - const widget = chatWidgetService.lastFocusedWidget; - if (widget) { - const viewModel = widget.viewModel; - const sessionAsText = viewModel?.getItems() - .filter((item): item is (IChatRequestViewModel | IChatResponseViewModel) => isRequestVM(item) || (isResponseVM(item) && !item.errorDetails?.responseIsFiltered)) - .map(item => stringifyItem(item)) - .join('\n\n'); - if (sessionAsText) { - clipboardService.writeText(sessionAsText); - } - } - } - }); - - registerAction2(class CopyItemAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.aideChat.copyItem', - title: localize2('aideChat.copyItem.label', "Copy"), - f1: false, - category: CHAT_CATEGORY, - menu: { - id: MenuId.AideChatContext, - when: CONTEXT_RESPONSE_FILTERED.toNegated(), - group: 'copy', - } - }); - } - - run(accessor: ServicesAccessor, ...args: any[]) { - const item = args[0]; - if (!isRequestVM(item) && !isResponseVM(item)) { - return; - } - - const clipboardService = accessor.get(IClipboardService); - const text = stringifyItem(item, false); - clipboardService.writeText(text); - } - }); -} diff --git a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatDeveloperActions.ts b/src/vs/workbench/contrib/aideChat/browser/actions/aideChatDeveloperActions.ts deleted file mode 100644 index 448ac964ba8..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatDeveloperActions.ts +++ /dev/null @@ -1,35 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Codicon } from '../../../../../base/common/codicons.js'; -import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; -import { localize2 } from '../../../../../nls.js'; -import { Categories } from '../../../../../platform/action/common/actionCommonCategories.js'; -import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { IAideChatWidgetService } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; - -export function registerChatDeveloperActions() { - registerAction2(LogChatInputHistoryAction); -} - -class LogChatInputHistoryAction extends Action2 { - - static readonly ID = 'workbench.action.aideChat.logInputHistory'; - - constructor() { - super({ - id: LogChatInputHistoryAction.ID, - title: localize2('workbench.action.aideChat.logInputHistory.label', "Log Chat Input History"), - icon: Codicon.attach, - category: Categories.Developer, - f1: true - }); - } - - override async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const chatWidgetService = accessor.get(IAideChatWidgetService); - chatWidgetService.lastFocusedWidget?.logInputHistory(); - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatExecuteActions.ts b/src/vs/workbench/contrib/aideChat/browser/actions/aideChatExecuteActions.ts deleted file mode 100644 index e012727ce04..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatExecuteActions.ts +++ /dev/null @@ -1,189 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Codicon } from '../../../../../base/common/codicons.js'; -import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; -import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; -import { localize2 } from '../../../../../nls.js'; -import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; -import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { CHAT_CATEGORY } from '../../../../../workbench/contrib/aideChat/browser/actions/aideChatActions.js'; -import { IChatWidget, IAideChatWidgetService } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { IAideChatAgentService } from '../../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { CONTEXT_CHAT_INPUT_HAS_AGENT, CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_INPUT } from '../../../../../workbench/contrib/aideChat/common/aideChatContextKeys.js'; -import { chatAgentLeader, extractAgentAndCommand } from '../../../../../workbench/contrib/aideChat/common/aideChatParserTypes.js'; -import { IAideChatService } from '../../../../../workbench/contrib/aideChat/common/aideChatService.js'; - -export interface IVoiceChatExecuteActionContext { - readonly disableTimeout?: boolean; -} - -export interface IChatExecuteActionContext { - widget?: IChatWidget; - inputValue?: string; - voice?: IVoiceChatExecuteActionContext; -} - -export class SubmitAction extends Action2 { - static readonly ID = 'workbench.action.aideChat.submit'; - - constructor() { - super({ - id: SubmitAction.ID, - title: localize2('aideChat.submit.label', "Send"), - f1: false, - category: CHAT_CATEGORY, - icon: Codicon.send, - precondition: ContextKeyExpr.and(CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate()), - keybinding: { - when: CONTEXT_IN_CHAT_INPUT, - primary: KeyCode.Enter, - weight: KeybindingWeight.EditorContrib - }, - menu: [ - { - id: MenuId.AideChatExecuteSecondary, - group: 'group_1', - }, - { - id: MenuId.AideChatExecute, - when: CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), - group: 'navigation', - }, - ] - }); - } - - run(accessor: ServicesAccessor, ...args: any[]) { - const context: IChatExecuteActionContext | undefined = args[0]; - - const widgetService = accessor.get(IAideChatWidgetService); - const widget = context?.widget ?? widgetService.lastFocusedWidget; - widget?.acceptInput(context?.inputValue); - } -} - - -export class ChatSubmitSecondaryAgentAction extends Action2 { - static readonly ID = 'workbench.action.aideChat.submitSecondaryAgent'; - - constructor() { - super({ - id: ChatSubmitSecondaryAgentAction.ID, - title: localize2({ key: 'actions.chat.submitSecondaryAgent', comment: ['Send input from the chat input box to the secondary agent'] }, "Submit to Secondary Agent"), - precondition: ContextKeyExpr.and(CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_CHAT_INPUT_HAS_AGENT.negate(), CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate()), - keybinding: { - when: CONTEXT_IN_CHAT_INPUT, - primary: KeyMod.CtrlCmd | KeyCode.Enter, - weight: KeybindingWeight.EditorContrib - }, - menu: { - id: MenuId.AideChatExecuteSecondary, - group: 'group_1' - } - }); - } - - run(accessor: ServicesAccessor, ...args: any[]) { - const context: IChatExecuteActionContext | undefined = args[0]; - const agentService = accessor.get(IAideChatAgentService); - const secondaryAgent = agentService.getSecondaryAgent(); - if (!secondaryAgent) { - return; - } - - const widgetService = accessor.get(IAideChatWidgetService); - const widget = context?.widget ?? widgetService.lastFocusedWidget; - if (!widget) { - return; - } - - if (extractAgentAndCommand(widget.parsedInput).agentPart) { - widget.acceptInput(); - } else { - widget.lastSelectedAgent = secondaryAgent; - widget.acceptInputWithPrefix(`${chatAgentLeader}${secondaryAgent.name}`); - } - } -} - -class SendToNewChatAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.aideChat.sendToNewChat', - title: localize2('aideChat.newChat.label', "Send to New Chat"), - precondition: ContextKeyExpr.and(CONTEXT_CHAT_REQUEST_IN_PROGRESS.negate(), CONTEXT_CHAT_INPUT_HAS_TEXT), - category: CHAT_CATEGORY, - f1: false, - menu: { - id: MenuId.AideChatExecuteSecondary, - group: 'group_2' - }, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter, - when: CONTEXT_IN_CHAT_INPUT, - } - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - const context: IChatExecuteActionContext | undefined = args[0]; - - const widgetService = accessor.get(IAideChatWidgetService); - const widget = context?.widget ?? widgetService.lastFocusedWidget; - if (!widget) { - return; - } - - widget.clear(); - widget.acceptInput(context?.inputValue); - } -} - -export class CancelAction extends Action2 { - static readonly ID = 'workbench.action.aideChat.cancel'; - constructor() { - super({ - id: CancelAction.ID, - title: localize2('aideChat.cancel.label', "Cancel"), - f1: false, - category: CHAT_CATEGORY, - icon: Codicon.debugStop, - menu: { - id: MenuId.AideChatExecute, - when: CONTEXT_CHAT_REQUEST_IN_PROGRESS, - group: 'navigation', - }, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyCode.Escape, - } - }); - } - - run(accessor: ServicesAccessor, ...args: any[]) { - const context: IChatExecuteActionContext | undefined = args[0]; - - const widgetService = accessor.get(IAideChatWidgetService); - const widget = context?.widget ?? widgetService.lastFocusedWidget; - if (!widget) { - return; - } - - const chatService = accessor.get(IAideChatService); - if (widget.viewModel) { - chatService.cancelCurrentRequestForSession(widget.viewModel.sessionId); - } - } -} - -export function registerChatExecuteActions() { - registerAction2(SubmitAction); - registerAction2(CancelAction); - registerAction2(SendToNewChatAction); - registerAction2(ChatSubmitSecondaryAgentAction); -} diff --git a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatFileTreeActions.ts b/src/vs/workbench/contrib/aideChat/browser/actions/aideChatFileTreeActions.ts deleted file mode 100644 index 91eb5fb8310..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatFileTreeActions.ts +++ /dev/null @@ -1,83 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; -import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; -import { localize2 } from '../../../../../nls.js'; -import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { CHAT_CATEGORY } from '../../../../../workbench/contrib/aideChat/browser/actions/aideChatActions.js'; -import { IAideChatWidgetService } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { CONTEXT_IN_CHAT_SESSION, CONTEXT_CHAT_ENABLED } from '../../../../../workbench/contrib/aideChat/common/aideChatContextKeys.js'; -import { IChatResponseViewModel, isResponseVM } from '../../../../../workbench/contrib/aideChat/common/aideChatViewModel.js'; - -export function registerChatFileTreeActions() { - registerAction2(class NextFileTreeAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.aideChat.nextFileTree', - title: localize2('aideChat.nextFileTree.label', "Next File Tree"), - keybinding: { - primary: KeyMod.CtrlCmd | KeyCode.F9, - weight: KeybindingWeight.WorkbenchContrib, - when: CONTEXT_IN_CHAT_SESSION, - }, - precondition: CONTEXT_CHAT_ENABLED, - f1: true, - category: CHAT_CATEGORY, - }); - } - - run(accessor: ServicesAccessor, ...args: any[]) { - navigateTrees(accessor, false); - } - }); - - registerAction2(class PreviousFileTreeAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.aideChat.previousFileTree', - title: localize2('aideChat.previousFileTree.label', "Previous File Tree"), - keybinding: { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.F9, - weight: KeybindingWeight.WorkbenchContrib, - when: CONTEXT_IN_CHAT_SESSION, - }, - precondition: CONTEXT_CHAT_ENABLED, - f1: true, - category: CHAT_CATEGORY, - }); - } - - run(accessor: ServicesAccessor, ...args: any[]) { - navigateTrees(accessor, true); - } - }); -} - -function navigateTrees(accessor: ServicesAccessor, reverse: boolean) { - const chatWidgetService = accessor.get(IAideChatWidgetService); - const widget = chatWidgetService.lastFocusedWidget; - if (!widget) { - return; - } - - const focused = !widget.inputEditor.hasWidgetFocus() && widget.getFocus(); - const focusedResponse = isResponseVM(focused) ? focused : undefined; - - const currentResponse = focusedResponse ?? widget.viewModel?.getItems().reverse().find((item): item is IChatResponseViewModel => isResponseVM(item)); - if (!currentResponse) { - return; - } - - widget.reveal(currentResponse); - const responseFileTrees = widget.getFileTreeInfosForResponse(currentResponse); - const lastFocusedFileTree = widget.getLastFocusedFileTreeForResponse(currentResponse); - const focusIdx = lastFocusedFileTree ? - (lastFocusedFileTree.treeIndex + (reverse ? -1 : 1) + responseFileTrees.length) % responseFileTrees.length : - reverse ? responseFileTrees.length - 1 : 0; - - responseFileTrees[focusIdx]?.focus(); -} diff --git a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatImportExport.ts b/src/vs/workbench/contrib/aideChat/browser/actions/aideChatImportExport.ts deleted file mode 100644 index 39910261af3..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatImportExport.ts +++ /dev/null @@ -1,105 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { VSBuffer } from '../../../../../base/common/buffer.js'; -import { joinPath } from '../../../../../base/common/resources.js'; -import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; -import { localize, localize2 } from '../../../../../nls.js'; -import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { IFileDialogService } from '../../../../../platform/dialogs/common/dialogs.js'; -import { IFileService } from '../../../../../platform/files/common/files.js'; -import { CHAT_CATEGORY } from '../../../../../workbench/contrib/aideChat/browser/actions/aideChatActions.js'; -import { IAideChatWidgetService } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { IChatEditorOptions } from '../../../../../workbench/contrib/aideChat/browser/aideChatEditor.js'; -import { AideChatEditorInput } from '../../../../../workbench/contrib/aideChat/browser/aideChatEditorInput.js'; -import { CONTEXT_CHAT_ENABLED } from '../../../../../workbench/contrib/aideChat/common/aideChatContextKeys.js'; -import { isExportableSessionData } from '../../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { IAideChatService } from '../../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { IEditorService } from '../../../../../workbench/services/editor/common/editorService.js'; - -const defaultFileName = 'aideChat.json'; -const filters = [{ name: localize('aideChat.file.label', "Chat Session"), extensions: ['json'] }]; - -export function registerChatExportActions() { - registerAction2(class ExportChatAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.aideChat.export', - category: CHAT_CATEGORY, - title: localize2('aideChat.export.label', "Export Chat..."), - precondition: CONTEXT_CHAT_ENABLED, - f1: true, - }); - } - async run(accessor: ServicesAccessor, ...args: any[]) { - const widgetService = accessor.get(IAideChatWidgetService); - const fileDialogService = accessor.get(IFileDialogService); - const fileService = accessor.get(IFileService); - const chatService = accessor.get(IAideChatService); - - const widget = widgetService.lastFocusedWidget; - if (!widget || !widget.viewModel) { - return; - } - - const defaultUri = joinPath(await fileDialogService.defaultFilePath(), defaultFileName); - const result = await fileDialogService.showSaveDialog({ - defaultUri, - filters - }); - if (!result) { - return; - } - - const model = chatService.getSession(widget.viewModel.sessionId); - if (!model) { - return; - } - - // Using toJSON on the model - const content = VSBuffer.fromString(JSON.stringify(model.toExport(), undefined, 2)); - await fileService.writeFile(result, content); - } - }); - - registerAction2(class ImportChatAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.aideChat.import', - title: localize2('aideChat.import.label', "Import Chat..."), - category: CHAT_CATEGORY, - precondition: CONTEXT_CHAT_ENABLED, - f1: true, - }); - } - async run(accessor: ServicesAccessor, ...args: any[]) { - const fileDialogService = accessor.get(IFileDialogService); - const fileService = accessor.get(IFileService); - const editorService = accessor.get(IEditorService); - - const defaultUri = joinPath(await fileDialogService.defaultFilePath(), defaultFileName); - const result = await fileDialogService.showOpenDialog({ - defaultUri, - canSelectFiles: true, - filters - }); - if (!result) { - return; - } - - const content = await fileService.readFile(result[0]); - try { - const data = JSON.parse(content.value.toString()); - if (!isExportableSessionData(data)) { - throw new Error('Invalid chat session data'); - } - - await editorService.openEditor({ resource: AideChatEditorInput.getNewEditorUri(), options: { target: { data }, pinned: true } as IChatEditorOptions }); - } catch (err) { - throw err; - } - } - }); -} diff --git a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatMoveActions.ts b/src/vs/workbench/contrib/aideChat/browser/actions/aideChatMoveActions.ts deleted file mode 100644 index 6f24680a54d..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatMoveActions.ts +++ /dev/null @@ -1,130 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { localize2 } from '../../../../../nls.js'; -import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; -import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; -import { ActiveEditorContext } from '../../../../../workbench/common/contextkeys.js'; -import { CHAT_CATEGORY, isChatViewTitleActionContext } from '../../../../../workbench/contrib/aideChat/browser/actions/aideChatActions.js'; -import { CHAT_VIEW_ID, IAideChatWidgetService } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { IChatEditorOptions } from '../../../../../workbench/contrib/aideChat/browser/aideChatEditor.js'; -import { AideChatEditorInput } from '../../../../../workbench/contrib/aideChat/browser/aideChatEditorInput.js'; -import { ChatViewPane } from '../../../../../workbench/contrib/aideChat/browser/aideChatViewPane.js'; -import { CONTEXT_CHAT_ENABLED } from '../../../../../workbench/contrib/aideChat/common/aideChatContextKeys.js'; -import { IEditorGroupsService } from '../../../../../workbench/services/editor/common/editorGroupsService.js'; -import { ACTIVE_GROUP, AUX_WINDOW_GROUP, IEditorService } from '../../../../../workbench/services/editor/common/editorService.js'; -import { IViewsService } from '../../../../../workbench/services/views/common/viewsService.js'; - -enum MoveToNewLocation { - Editor = 'Editor', - Window = 'Window' -} - -export function registerMoveActions() { - registerAction2(class GlobalMoveToEditorAction extends Action2 { - constructor() { - super({ - id: `workbench.action.aideChat.openInEditor`, - title: localize2('aideChat.openInEditor.label', "Open Chat in Editor"), - category: CHAT_CATEGORY, - precondition: CONTEXT_CHAT_ENABLED, - f1: true, - menu: { - id: MenuId.ViewTitle, - when: ContextKeyExpr.equals('view', CHAT_VIEW_ID), - order: 0 - }, - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - const context = args[0]; - executeMoveToAction(accessor, MoveToNewLocation.Editor, isChatViewTitleActionContext(context) ? context.chatView : undefined); - } - }); - - registerAction2(class GlobalMoveToNewWindowAction extends Action2 { - constructor() { - super({ - id: `workbench.action.aideChat.openInNewWindow`, - title: localize2('aideChat.openInNewWindow.label', "Open Chat in New Window"), - category: CHAT_CATEGORY, - precondition: CONTEXT_CHAT_ENABLED, - f1: true, - menu: { - id: MenuId.ViewTitle, - when: ContextKeyExpr.equals('view', CHAT_VIEW_ID), - order: 0 - }, - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - const context = args[0]; - executeMoveToAction(accessor, MoveToNewLocation.Window, isChatViewTitleActionContext(context) ? context.chatView : undefined); - } - }); - - registerAction2(class GlobalMoveToSidebarAction extends Action2 { - constructor() { - super({ - id: `workbench.action.aideChat.openInSidebar`, - title: localize2('aideChat.openInSidebar.label', "Open Chat in Side Bar"), - category: CHAT_CATEGORY, - precondition: CONTEXT_CHAT_ENABLED, - f1: true, - menu: [{ - id: MenuId.EditorTitle, - order: 0, - when: ActiveEditorContext.isEqualTo(AideChatEditorInput.EditorID), - }] - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - return moveToSidebar(accessor); - } - }); -} - -async function executeMoveToAction(accessor: ServicesAccessor, moveTo: MoveToNewLocation, chatView?: ChatViewPane) { - const widgetService = accessor.get(IAideChatWidgetService); - const viewService = accessor.get(IViewsService); - const editorService = accessor.get(IEditorService); - - const widget = chatView?.widget ?? widgetService.lastFocusedWidget; - if (!widget || !('viewId' in widget.viewContext)) { - await editorService.openEditor({ resource: AideChatEditorInput.getNewEditorUri(), options: { pinned: true } }, moveTo === MoveToNewLocation.Window ? AUX_WINDOW_GROUP : ACTIVE_GROUP); - return; - } - - const viewModel = widget.viewModel; - if (!viewModel) { - return; - } - - const sessionId = viewModel.sessionId; - const view = await viewService.openView(widget.viewContext.viewId) as ChatViewPane; - const viewState = view.widget.getViewState(); - view.clear(); - - await editorService.openEditor({ resource: AideChatEditorInput.getNewEditorUri(), options: { target: { sessionId }, pinned: true, viewState: viewState } }, moveTo === MoveToNewLocation.Window ? AUX_WINDOW_GROUP : ACTIVE_GROUP); -} - -async function moveToSidebar(accessor: ServicesAccessor): Promise { - const viewsService = accessor.get(IViewsService); - const editorService = accessor.get(IEditorService); - const editorGroupService = accessor.get(IEditorGroupsService); - - const chatEditorInput = editorService.activeEditor; - if (chatEditorInput instanceof AideChatEditorInput && chatEditorInput.sessionId) { - await editorService.closeEditor({ editor: chatEditorInput, groupId: editorGroupService.activeGroup.id }); - const view = await viewsService.openView(CHAT_VIEW_ID) as ChatViewPane; - view.loadSession(chatEditorInput.sessionId); - } else { - await viewsService.openView(CHAT_VIEW_ID); - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatQuickInputActions.ts b/src/vs/workbench/contrib/aideChat/browser/actions/aideChatQuickInputActions.ts deleted file mode 100644 index 4ada77f096d..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatQuickInputActions.ts +++ /dev/null @@ -1,179 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Codicon } from '../../../../../base/common/codicons.js'; -import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; -import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js'; -import { Selection } from '../../../../../editor/common/core/selection.js'; -import { localize, localize2 } from '../../../../../nls.js'; -import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; -import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { CHAT_CATEGORY } from '../../../../../workbench/contrib/aideChat/browser/actions/aideChatActions.js'; -import { IQuickChatOpenOptions, IQuickChatService } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { CONTEXT_CHAT_ENABLED } from '../../../../../workbench/contrib/aideChat/common/aideChatContextKeys.js'; -import { InlineChatController } from '../../../../../workbench/contrib/inlineAideChat/browser/inlineChatController.js'; - -export const ASK_QUICK_QUESTION_ACTION_ID = 'workbench.action.aidequickchat.toggle'; -export function registerQuickChatActions() { - registerAction2(QuickChatGlobalAction); - registerAction2(AskQuickChatAction); - - registerAction2(class OpenInChatViewAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.aidequickchat.openInChatView', - title: localize2('aideChat.openInChatView.label', "Open in Chat View"), - f1: false, - category: CHAT_CATEGORY, - icon: Codicon.commentDiscussion, - menu: { - id: MenuId.AideChatInputSide, - group: 'navigation', - order: 10 - } - }); - } - - run(accessor: ServicesAccessor) { - const quickChatService = accessor.get(IQuickChatService); - quickChatService.openInChatView(); - } - }); - - registerAction2(class CloseQuickChatAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.aidequickchat.close', - title: localize2('aideChat.closeQuickChat.label', "Close Quick Chat"), - f1: false, - category: CHAT_CATEGORY, - icon: Codicon.close, - menu: { - id: MenuId.AideChatInputSide, - group: 'navigation', - order: 20 - } - }); - } - - run(accessor: ServicesAccessor) { - const quickChatService = accessor.get(IQuickChatService); - quickChatService.close(); - } - }); - - registerAction2(class LaunchInlineChatFromQuickChatAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.quickchat.launchInlineAideChat', - title: localize2('aideChat.launchInlineChat.label', "Launch Inline Chat"), - f1: false, - category: CHAT_CATEGORY - }); - } - - async run(accessor: ServicesAccessor) { - const quickChatService = accessor.get(IQuickChatService); - const codeEditorService = accessor.get(ICodeEditorService); - if (quickChatService.focused) { - quickChatService.close(); - } - const codeEditor = codeEditorService.getActiveCodeEditor(); - if (!codeEditor) { - return; - } - - const controller = InlineChatController.get(codeEditor); - if (!controller) { - return; - } - - await controller.run(); - controller.focus(); - } - }); - -} - -class QuickChatGlobalAction extends Action2 { - constructor() { - super({ - id: ASK_QUICK_QUESTION_ACTION_ID, - title: localize2('quickChat', 'Quick Chat'), - precondition: CONTEXT_CHAT_ENABLED, - icon: Codicon.commentDiscussion, - f1: false, - category: CHAT_CATEGORY, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI, - linux: { - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyMod.Alt | KeyCode.KeyI - } - }, - metadata: { - description: localize('toggle.desc', 'Toggle the quick chat'), - args: [{ - name: 'args', - schema: { - anyOf: [ - { - type: 'object', - required: ['query'], - properties: { - query: { - description: localize('toggle.query', "The query to open the quick chat with"), - type: 'string' - }, - isPartialQuery: { - description: localize('toggle.isPartialQuery', "Whether the query is partial; it will wait for more user input"), - type: 'boolean' - } - }, - }, - { - type: 'string', - description: localize('toggle.query', "The query to open the quick chat with") - } - ] - } - }] - }, - }); - } - - override run(accessor: ServicesAccessor, query?: string | Omit): void { - const quickChatService = accessor.get(IQuickChatService); - let options: IQuickChatOpenOptions | undefined; - switch (typeof query) { - case 'string': options = { query }; break; - case 'object': options = query; break; - } - if (options?.query) { - options.selection = new Selection(1, options.query.length + 1, 1, options.query.length + 1); - } - quickChatService.toggle(options); - } -} - -class AskQuickChatAction extends Action2 { - constructor() { - super({ - id: `workbench.action.openAideQuickChat`, - category: CHAT_CATEGORY, - title: localize2('aideChat.open', "Open Quick Chat"), - f1: true - }); - } - - override run(accessor: ServicesAccessor, query?: string): void { - const quickChatService = accessor.get(IQuickChatService); - quickChatService.toggle(query ? { - query, - selection: new Selection(1, query.length + 1, 1, query.length + 1) - } : undefined); - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatTitleActions.ts b/src/vs/workbench/contrib/aideChat/browser/actions/aideChatTitleActions.ts deleted file mode 100644 index 404b34b9500..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/actions/aideChatTitleActions.ts +++ /dev/null @@ -1,299 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Codicon } from '../../../../../base/common/codicons.js'; -import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; -import { marked } from '../../../../../base/common/marked/marked.js'; -import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js'; -import { IBulkEditService } from '../../../../../editor/browser/services/bulkEditService.js'; -import { localize2 } from '../../../../../nls.js'; -import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js'; -import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { ResourceNotebookCellEdit } from '../../../../../workbench/contrib/bulkEdit/browser/bulkCellEdits.js'; -import { CHAT_CATEGORY } from '../../../../../workbench/contrib/aideChat/browser/actions/aideChatActions.js'; -import { IAideChatWidgetService } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_IN_CHAT_INPUT, CONTEXT_IN_CHAT_SESSION, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from '../../../../../workbench/contrib/aideChat/common/aideChatContextKeys.js'; -import { IAideChatService, AideChatAgentVoteDirection } from '../../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { isRequestVM, isResponseVM } from '../../../../../workbench/contrib/aideChat/common/aideChatViewModel.js'; -import { INotebookEditor } from '../../../../../workbench/contrib/notebook/browser/notebookBrowser.js'; -import { CellEditType, CellKind, NOTEBOOK_EDITOR_ID } from '../../../../../workbench/contrib/notebook/common/notebookCommon.js'; -import { NOTEBOOK_IS_ACTIVE_EDITOR } from '../../../../../workbench/contrib/notebook/common/notebookContextKeys.js'; -import { IEditorService } from '../../../../../workbench/services/editor/common/editorService.js'; - -export function registerChatTitleActions() { - registerAction2(class MarkHelpfulAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.aideChat.markHelpful', - title: localize2('aideChat.helpful.label', "Helpful"), - f1: false, - category: CHAT_CATEGORY, - icon: Codicon.thumbsup, - toggled: CONTEXT_RESPONSE_VOTE.isEqualTo('up'), - menu: { - id: MenuId.AideChatMessageTitle, - group: 'navigation', - order: 1, - when: CONTEXT_RESPONSE - } - }); - } - - run(accessor: ServicesAccessor, ...args: any[]) { - const item = args[0]; - if (!isResponseVM(item)) { - return; - } - - const chatService = accessor.get(IAideChatService); - chatService.notifyUserAction({ - agentId: item.agent?.id, - sessionId: item.sessionId, - requestId: item.requestId, - result: item.result, - action: { - kind: 'vote', - direction: AideChatAgentVoteDirection.Up, - } - }); - item.setVote(AideChatAgentVoteDirection.Up); - } - }); - - registerAction2(class MarkUnhelpfulAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.aideChat.markUnhelpful', - title: localize2('aideChat.unhelpful.label', "Unhelpful"), - f1: false, - category: CHAT_CATEGORY, - icon: Codicon.thumbsdown, - toggled: CONTEXT_RESPONSE_VOTE.isEqualTo('down'), - menu: { - id: MenuId.AideChatMessageTitle, - group: 'navigation', - order: 2, - when: CONTEXT_RESPONSE - } - }); - } - - run(accessor: ServicesAccessor, ...args: any[]) { - const item = args[0]; - if (!isResponseVM(item)) { - return; - } - - const chatService = accessor.get(IAideChatService); - chatService.notifyUserAction({ - agentId: item.agent?.id, - sessionId: item.sessionId, - requestId: item.requestId, - result: item.result, - action: { - kind: 'vote', - direction: AideChatAgentVoteDirection.Down, - } - }); - item.setVote(AideChatAgentVoteDirection.Down); - } - }); - - registerAction2(class ReportIssueForBugAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.aideChat.reportIssueForBug', - title: localize2('aideChat.reportIssueForBug.label', "Report Issue"), - f1: false, - category: CHAT_CATEGORY, - icon: Codicon.report, - menu: { - id: MenuId.AideChatMessageTitle, - group: 'navigation', - order: 3, - when: ContextKeyExpr.and(CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_RESPONSE) - } - }); - } - - run(accessor: ServicesAccessor, ...args: any[]) { - const item = args[0]; - if (!isResponseVM(item)) { - return; - } - - const chatService = accessor.get(IAideChatService); - chatService.notifyUserAction({ - agentId: item.agent?.id, - sessionId: item.sessionId, - requestId: item.requestId, - result: item.result, - action: { - kind: 'bug' - } - }); - } - }); - - registerAction2(class InsertToNotebookAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.aideChat.insertIntoNotebook', - title: localize2('aideChat.insertIntoNotebook.label', "Insert into Notebook"), - f1: false, - category: CHAT_CATEGORY, - icon: Codicon.insert, - menu: { - id: MenuId.AideChatMessageTitle, - group: 'navigation', - isHiddenByDefault: true, - when: ContextKeyExpr.and(NOTEBOOK_IS_ACTIVE_EDITOR, CONTEXT_RESPONSE, CONTEXT_RESPONSE_FILTERED.negate()) - } - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - const item = args[0]; - if (!isResponseVM(item)) { - return; - } - - const editorService = accessor.get(IEditorService); - - if (editorService.activeEditorPane?.getId() === NOTEBOOK_EDITOR_ID) { - const notebookEditor = editorService.activeEditorPane.getControl() as INotebookEditor; - - if (!notebookEditor.hasModel()) { - return; - } - - if (notebookEditor.isReadOnly) { - return; - } - - const value = item.response.asString(); - const splitContents = splitMarkdownAndCodeBlocks(value); - - const focusRange = notebookEditor.getFocus(); - const index = Math.max(focusRange.end, 0); - const bulkEditService = accessor.get(IBulkEditService); - - await bulkEditService.apply( - [ - new ResourceNotebookCellEdit(notebookEditor.textModel.uri, - { - editType: CellEditType.Replace, - index: index, - count: 0, - cells: splitContents.map(content => { - const kind = content.type === 'markdown' ? CellKind.Markup : CellKind.Code; - const language = content.type === 'markdown' ? 'markdown' : content.language; - const mime = content.type === 'markdown' ? 'text/markdown' : `text/x-${content.language}`; - return { - cellKind: kind, - language, - mime, - source: content.content, - outputs: [], - metadata: {} - }; - }) - } - ) - ], - { quotableLabel: 'Insert into Notebook' } - ); - } - } - }); - - - registerAction2(class RemoveAction extends Action2 { - constructor() { - super({ - id: 'workbench.action.aideChat.remove', - title: localize2('aideChat.remove.label', "Remove Request and Response"), - f1: false, - category: CHAT_CATEGORY, - icon: Codicon.x, - keybinding: { - primary: KeyCode.Delete, - mac: { - primary: KeyMod.CtrlCmd | KeyCode.Backspace, - }, - when: ContextKeyExpr.and(CONTEXT_IN_CHAT_SESSION, CONTEXT_IN_CHAT_INPUT.negate()), - weight: KeybindingWeight.WorkbenchContrib, - }, - menu: { - id: MenuId.AideChatMessageTitle, - group: 'navigation', - order: 2, - when: CONTEXT_REQUEST - } - }); - } - - run(accessor: ServicesAccessor, ...args: any[]) { - let item = args[0]; - if (!isRequestVM(item)) { - const chatWidgetService = accessor.get(IAideChatWidgetService); - const widget = chatWidgetService.lastFocusedWidget; - item = widget?.getFocus(); - } - - const requestId = isRequestVM(item) ? item.id : - isResponseVM(item) ? item.requestId : undefined; - - if (requestId) { - const chatService = accessor.get(IAideChatService); - chatService.removeRequest(item.sessionId, requestId); - } - } - }); -} - -interface MarkdownContent { - type: 'markdown'; - content: string; -} - -interface CodeContent { - type: 'code'; - language: string; - content: string; -} - -type Content = MarkdownContent | CodeContent; - -function splitMarkdownAndCodeBlocks(markdown: string): Content[] { - const lexer = new marked.Lexer(); - const tokens = lexer.lex(markdown); - - const splitContent: Content[] = []; - - let markdownPart = ''; - tokens.forEach((token) => { - if (token.type === 'code') { - if (markdownPart.trim()) { - splitContent.push({ type: 'markdown', content: markdownPart }); - markdownPart = ''; - } - splitContent.push({ - type: 'code', - language: token.lang || '', - content: token.text, - }); - } else { - markdownPart += token.raw; - } - }); - - if (markdownPart.trim()) { - splitContent.push({ type: 'markdown', content: markdownPart }); - } - - return splitContent; -} diff --git a/src/vs/workbench/contrib/aideChat/browser/aideChat.contribution.ts b/src/vs/workbench/contrib/aideChat/browser/aideChat.contribution.ts deleted file mode 100644 index 16b248104a7..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/aideChat.contribution.ts +++ /dev/null @@ -1,285 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { isMacintosh } from '../../../../base/common/platform.js'; -import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; -import * as nls from '../../../../nls.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { Extensions as ConfigurationExtensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; -import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; -import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { Registry } from '../../../../platform/registry/common/platform.js'; -import { EditorPaneDescriptor, IEditorPaneRegistry } from '../../../../workbench/browser/editor.js'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, WorkbenchPhase, registerWorkbenchContribution2 } from '../../../../workbench/common/contributions.js'; -import { EditorExtensions, IEditorFactoryRegistry } from '../../../../workbench/common/editor.js'; -import { registerChatActions } from '../../../../workbench/contrib/aideChat/browser/actions/aideChatActions.js'; -import { ACTION_ID_NEW_CHAT, registerNewChatActions } from '../../../../workbench/contrib/aideChat/browser/actions/aideChatClearActions.js'; -import { registerChatCodeBlockActions, registerChatCodeCompareBlockActions } from '../../../../workbench/contrib/aideChat/browser/actions/aideChatCodeblockActions.js'; -//import { registerChatContextActions } from 'vs/workbench/contrib/aideChat/browser/actions/aideChatContextActions'; -import { registerChatCopyActions } from '../../../../workbench/contrib/aideChat/browser/actions/aideChatCopyActions.js'; -import { registerChatDeveloperActions } from '../../../../workbench/contrib/aideChat/browser/actions/aideChatDeveloperActions.js'; -import { SubmitAction, registerChatExecuteActions } from '../../../../workbench/contrib/aideChat/browser/actions/aideChatExecuteActions.js'; -import { registerChatFileTreeActions } from '../../../../workbench/contrib/aideChat/browser/actions/aideChatFileTreeActions.js'; -import { registerChatExportActions } from '../../../../workbench/contrib/aideChat/browser/actions/aideChatImportExport.js'; -import { registerMoveActions } from '../../../../workbench/contrib/aideChat/browser/actions/aideChatMoveActions.js'; -import { registerQuickChatActions } from '../../../../workbench/contrib/aideChat/browser/actions/aideChatQuickInputActions.js'; -import { registerChatTitleActions } from '../../../../workbench/contrib/aideChat/browser/actions/aideChatTitleActions.js'; -import { IAideChatAccessibilityService, IAideChatCodeBlockContextProviderService, IAideChatWidgetService, IQuickChatService } from '../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { ChatAccessibilityService } from '../../../../workbench/contrib/aideChat/browser/aideChatAccessibilityService.js'; -import { ChatEditor, IChatEditorOptions } from '../../../../workbench/contrib/aideChat/browser/aideChatEditor.js'; -import { AideChatEditorInput, ChatEditorInputSerializer } from '../../../../workbench/contrib/aideChat/browser/aideChatEditorInput.js'; -import { agentSlashCommandToMarkdown, agentToMarkdown } from '../../../../workbench/contrib/aideChat/browser/aideChatMarkdownDecorationsRenderer.js'; -import { ChatExtensionPointHandler } from '../../../../workbench/contrib/aideChat/browser/aideChatParticipantContributions.js'; -import { QuickChatService } from '../../../../workbench/contrib/aideChat/browser/aideChatQuick.js'; -import { ChatVariablesService } from '../../../../workbench/contrib/aideChat/browser/aideChatVariables.js'; -import { ChatWidgetService } from '../../../../workbench/contrib/aideChat/browser/aideChatWidget.js'; -import { KeybindingPillWidget } from '../../../../workbench/contrib/aideChat/browser/aideKeybindingPill.js'; -import { ChatCodeBlockContextProviderService } from '../../../../workbench/contrib/aideChat/browser/codeBlockContextProviderService.js'; -import '../../../../workbench/contrib/aideChat/browser/contrib/aideChatContextAttachments.js'; -import '../../../../workbench/contrib/aideChat/browser/contrib/aideChatInputCompletions.js'; -import '../../../../workbench/contrib/aideChat/browser/contrib/aideChatInputEditorContrib.js'; -import '../../../../workbench/contrib/aideChat/browser/contrib/aideChatInputEditorHover.js'; -import { AideChatAgentLocation, ChatAgentNameService, ChatAgentService, IAideChatAgentNameService, IAideChatAgentService } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { chatVariableLeader } from '../../../../workbench/contrib/aideChat/common/aideChatParserTypes.js'; -import { IAideChatService } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { ChatService } from '../../../../workbench/contrib/aideChat/common/aideChatServiceImpl.js'; -import { ChatSlashCommandService, IAideChatSlashCommandService } from '../../../../workbench/contrib/aideChat/common/aideChatSlashCommands.js'; -import { IAideChatVariablesService } from '../../../../workbench/contrib/aideChat/common/aideChatVariables.js'; -import { ChatWidgetHistoryService, IAideChatWidgetHistoryService } from '../../../../workbench/contrib/aideChat/common/aideChatWidgetHistoryService.js'; -import { ILanguageModelsService, LanguageModelsService } from '../../../../workbench/contrib/aideChat/common/languageModels.js'; -import { ILanguageModelStatsService, LanguageModelStatsService } from '../../../../workbench/contrib/aideChat/common/languageModelStats.js'; -import { ILanguageModelToolsService, LanguageModelToolsService } from '../../../../workbench/contrib/aideChat/common/languageModelToolsService.js'; -import { LanguageModelToolsExtensionPointHandler } from '../../../../workbench/contrib/aideChat/common/tools/languageModelToolsContribution.js'; -import { IVoiceChatService, VoiceChatService } from '../../../../workbench/contrib/aideChat/common/voiceChatService.js'; -import { IEditorResolverService, RegisteredEditorPriority } from '../../../../workbench/services/editor/common/editorResolverService.js'; -import { LifecyclePhase } from '../../../../workbench/services/lifecycle/common/lifecycle.js'; -import '../common/aideChatColors.js'; -import { KeybindingPillContribution } from '../../../../workbench/contrib/aideChat/browser/contrib/aideChatKeybindingPillContrib.js'; - -// Register configuration -const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); -configurationRegistry.registerConfiguration({ - id: 'aideChatSidebar', - title: nls.localize('aideChatConfigurationTitle', "Chat"), - type: 'object', - properties: { - 'aideChat.editor.fontSize': { - type: 'number', - description: nls.localize('aideChat.editor.fontSize', "Controls the font size in pixels in chat codeblocks."), - default: isMacintosh ? 14 : 14, - }, - 'aideChat.editor.fontFamily': { - type: 'string', - description: nls.localize('aideChat.editor.fontFamily', "Controls the font family in chat codeblocks."), - default: 'default' - }, - 'aideChat.editor.fontWeight': { - type: 'string', - description: nls.localize('aideChat.editor.fontWeight', "Controls the font weight in chat codeblocks."), - default: 'default' - }, - 'aideChat.editor.wordWrap': { - type: 'string', - description: nls.localize('aideChat.editor.wordWrap', "Controls whether lines should wrap in chat codeblocks."), - default: 'off', - enum: ['on', 'off'] - }, - 'aideChat.editor.lineHeight': { - type: 'number', - description: nls.localize('aideChat.editor.lineHeight', "Controls the line height in pixels in chat codeblocks. Use 0 to compute the line height from the font size."), - default: 0 - }, - 'aideChat.experimental.implicitContext': { - type: 'boolean', - description: nls.localize('aideChat.experimental.implicitContext', "Controls whether a checkbox is shown to allow the user to determine which implicit context is included with a chat participant's prompt."), - deprecated: true, - default: false - }, - 'aideChat.experimental.variables.editor': { - type: 'boolean', - description: nls.localize('aideChat.experimental.variables.editor', "Enables variables for editor chat."), - default: false - }, - 'aideChat.experimental.variables.notebook': { - type: 'boolean', - description: nls.localize('aideChat.experimental.variables.notebook', "Enables variables for notebook chat."), - default: false - }, - 'aideChat.experimental.variables.terminal': { - type: 'boolean', - description: nls.localize('aideChat.experimental.variables.terminal', "Enables variables for terminal chat."), - default: false - }, - } -}); -Registry.as(EditorExtensions.EditorPane).registerEditorPane( - EditorPaneDescriptor.create( - ChatEditor, - AideChatEditorInput.EditorID, - nls.localize('aideChat', "Chat") - ), - [ - new SyncDescriptor(AideChatEditorInput) - ] -); - -class ChatResolverContribution extends Disposable { - - static readonly ID = 'workbench.contrib.aideChatResolver'; - - constructor( - @IEditorResolverService editorResolverService: IEditorResolverService, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super(); - - this._register(editorResolverService.registerEditor( - `${Schemas.vscodeChatSesssion}:**/**`, - { - id: AideChatEditorInput.EditorID, - label: nls.localize('aideChat', "Chat"), - priority: RegisteredEditorPriority.builtin - }, - { - singlePerResource: true, - canSupportResource: resource => resource.scheme === Schemas.vscodeChatSesssion - }, - { - createEditorInput: ({ resource, options }) => { - return { editor: instantiationService.createInstance(AideChatEditorInput, resource, options as IChatEditorOptions), options }; - } - } - )); - } -} - -class ChatSlashStaticSlashCommandsContribution extends Disposable { - - constructor( - @IAideChatSlashCommandService slashCommandService: IAideChatSlashCommandService, - @ICommandService commandService: ICommandService, - @IAideChatAgentService chatAgentService: IAideChatAgentService, - @IAideChatVariablesService chatVariablesService: IAideChatVariablesService, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super(); - this._store.add(slashCommandService.registerSlashCommand({ - command: 'clear', - detail: nls.localize('clear', "Start a new chat"), - sortText: 'z2_clear', - executeImmediately: true - }, async () => { - commandService.executeCommand(ACTION_ID_NEW_CHAT); - })); - this._store.add(slashCommandService.registerSlashCommand({ - command: 'help', - detail: '', - sortText: 'z1_help', - executeImmediately: true - }, async (prompt, progress) => { - const defaultAgent = chatAgentService.getDefaultAgent(AideChatAgentLocation.Panel); - const agents = chatAgentService.getAgents(); - - // Report prefix - if (defaultAgent?.metadata.helpTextPrefix) { - if (isMarkdownString(defaultAgent.metadata.helpTextPrefix)) { - progress.report({ content: defaultAgent.metadata.helpTextPrefix, kind: 'markdownContent' }); - } else { - progress.report({ content: new MarkdownString(defaultAgent.metadata.helpTextPrefix), kind: 'markdownContent' }); - } - progress.report({ content: new MarkdownString('\n\n'), kind: 'markdownContent' }); - } - - // Report agent list - const agentText = (await Promise.all(agents - .filter(a => a.id !== defaultAgent?.id) - .filter(a => a.locations.includes(AideChatAgentLocation.Panel)) - .map(async a => { - const description = a.description ? `- ${a.description}` : ''; - const agentMarkdown = instantiationService.invokeFunction(accessor => agentToMarkdown(a, true, accessor)); - const agentLine = `- ${agentMarkdown} ${description}`; - const commandText = a.slashCommands.map(c => { - const description = c.description ? `- ${c.description}` : ''; - return `\t* ${agentSlashCommandToMarkdown(a, c)} ${description}`; - }).join('\n'); - - return (agentLine + '\n' + commandText).trim(); - }))).join('\n'); - progress.report({ content: new MarkdownString(agentText, { isTrusted: { enabledCommands: [SubmitAction.ID] } }), kind: 'markdownContent' }); - - // Report variables - if (defaultAgent?.metadata.helpTextVariablesPrefix) { - progress.report({ content: new MarkdownString('\n\n'), kind: 'markdownContent' }); - if (isMarkdownString(defaultAgent.metadata.helpTextVariablesPrefix)) { - progress.report({ content: defaultAgent.metadata.helpTextVariablesPrefix, kind: 'markdownContent' }); - } else { - progress.report({ content: new MarkdownString(defaultAgent.metadata.helpTextVariablesPrefix), kind: 'markdownContent' }); - } - - const variables = [ - ...chatVariablesService.getVariables(), - { name: 'file', description: nls.localize('file', "Choose a file in the workspace") } - ]; - const variableText = variables - .map(v => `* \`${chatVariableLeader}${v.name}\` - ${v.description}`) - .join('\n'); - progress.report({ content: new MarkdownString('\n' + variableText), kind: 'markdownContent' }); - } - - // Report help text ending - if (defaultAgent?.metadata.helpTextPostfix) { - progress.report({ content: new MarkdownString('\n\n'), kind: 'markdownContent' }); - if (isMarkdownString(defaultAgent.metadata.helpTextPostfix)) { - progress.report({ content: defaultAgent.metadata.helpTextPostfix, kind: 'markdownContent' }); - } else { - progress.report({ content: new MarkdownString(defaultAgent.metadata.helpTextPostfix), kind: 'markdownContent' }); - } - } - })); - } -} - -const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); -registerWorkbenchContribution2(ChatResolverContribution.ID, ChatResolverContribution, WorkbenchPhase.BlockStartup); -workbenchContributionsRegistry.registerWorkbenchContribution(ChatSlashStaticSlashCommandsContribution, LifecyclePhase.Eventually); -Registry.as(EditorExtensions.EditorFactory).registerEditorSerializer(AideChatEditorInput.TypeID, ChatEditorInputSerializer); -registerWorkbenchContribution2(ChatExtensionPointHandler.ID, ChatExtensionPointHandler, WorkbenchPhase.BlockStartup); - -registerEditorContribution(KeybindingPillContribution.ID, KeybindingPillContribution, EditorContributionInstantiation.Eventually); -registerEditorContribution(KeybindingPillWidget.ID, KeybindingPillWidget, EditorContributionInstantiation.Lazy); -registerWorkbenchContribution2(LanguageModelToolsExtensionPointHandler.ID, LanguageModelToolsExtensionPointHandler, WorkbenchPhase.Eventually); - -registerChatActions(); -registerChatCopyActions(); -registerChatCodeBlockActions(); -registerChatCodeCompareBlockActions(); -registerChatFileTreeActions(); -registerChatTitleActions(); -registerChatExecuteActions(); -registerQuickChatActions(); -registerChatExportActions(); -registerMoveActions(); -registerNewChatActions(); -//registerChatContextActions(); -registerChatDeveloperActions(); - -registerSingleton(IAideChatService, ChatService, InstantiationType.Delayed); -registerSingleton(IAideChatWidgetService, ChatWidgetService, InstantiationType.Delayed); -registerSingleton(IQuickChatService, QuickChatService, InstantiationType.Delayed); -registerSingleton(IAideChatAccessibilityService, ChatAccessibilityService, InstantiationType.Delayed); -registerSingleton(IAideChatWidgetHistoryService, ChatWidgetHistoryService, InstantiationType.Delayed); -registerSingleton(ILanguageModelsService, LanguageModelsService, InstantiationType.Delayed); -registerSingleton(ILanguageModelStatsService, LanguageModelStatsService, InstantiationType.Delayed); -registerSingleton(IAideChatSlashCommandService, ChatSlashCommandService, InstantiationType.Delayed); -registerSingleton(IAideChatAgentService, ChatAgentService, InstantiationType.Delayed); -registerSingleton(IAideChatAgentNameService, ChatAgentNameService, InstantiationType.Delayed); -registerSingleton(IAideChatVariablesService, ChatVariablesService, InstantiationType.Delayed); -registerSingleton(ILanguageModelToolsService, LanguageModelToolsService, InstantiationType.Delayed); -registerSingleton(IVoiceChatService, VoiceChatService, InstantiationType.Delayed); -registerSingleton(IAideChatCodeBlockContextProviderService, ChatCodeBlockContextProviderService, InstantiationType.Delayed); diff --git a/src/vs/workbench/contrib/aideChat/browser/aideChat.ts b/src/vs/workbench/contrib/aideChat/browser/aideChat.ts deleted file mode 100644 index 2f556d1f693..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/aideChat.ts +++ /dev/null @@ -1,200 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { IDisposable } from '../../../../base/common/lifecycle.js'; -import { URI } from '../../../../base/common/uri.js'; -import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { Selection } from '../../../../editor/common/core/selection.js'; -import { localize } from '../../../../nls.js'; -import { MenuId } from '../../../../platform/actions/common/actions.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { ChatViewPane } from '../../../../workbench/contrib/aideChat/browser/aideChatViewPane.js'; -import { IChatWidgetContrib } from '../../../../workbench/contrib/aideChat/browser/aideChatWidget.js'; -import { ICodeBlockActionContext } from '../../../../workbench/contrib/aideChat/browser/codeBlockPart.js'; -import { AideChatAgentLocation, IChatAgentCommand, IChatAgentData } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { IAideChatRequestVariableEntry, IChatResponseModel } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { IParsedChatRequest } from '../../../../workbench/contrib/aideChat/common/aideChatParserTypes.js'; -import { CHAT_PROVIDER_ID } from '../../../../workbench/contrib/aideChat/common/aideChatParticipantContribTypes.js'; -import { IChatRequestViewModel, IChatResponseViewModel, IChatViewModel, IChatWelcomeMessageViewModel } from '../../../../workbench/contrib/aideChat/common/aideChatViewModel.js'; -import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; - -export const IAideChatWidgetService = createDecorator('aideChatWidgetService'); - -export interface IAideChatWidgetService { - - readonly _serviceBrand: undefined; - - /** - * Returns the most recently focused widget if any. - */ - readonly lastFocusedWidget: IChatWidget | undefined; - - getWidgetByInputUri(uri: URI): IChatWidget | undefined; - - getWidgetBySessionId(sessionId: string): IChatWidget | undefined; -} - -export interface IChatRequester { - username: string; - avatarIconUri?: URI; -} - -export async function showChatView(viewsService: IViewsService): Promise { - return (await viewsService.openView(CHAT_VIEW_ID))?.widget; -} - -export const IQuickChatService = createDecorator('quickChatService'); -export interface IQuickChatService { - readonly _serviceBrand: undefined; - readonly onDidClose: Event; - readonly enabled: boolean; - readonly focused: boolean; - toggle(options?: IQuickChatOpenOptions): void; - focus(): void; - open(options?: IQuickChatOpenOptions): void; - close(): void; - openInChatView(): void; -} - -export interface IQuickChatOpenOptions { - /** - * The query for quick chat. - */ - query: string; - /** - * Whether the query is partial and will await more input from the user. - */ - isPartialQuery?: boolean; - /** - * An optional selection range to apply to the query text box. - */ - selection?: Selection; -} - -export const IAideChatAccessibilityService = createDecorator('aideChatAccessibilityService'); -export interface IAideChatAccessibilityService { - readonly _serviceBrand: undefined; - acceptRequest(): number; - acceptResponse(response: IChatResponseViewModel | string | undefined, requestId: number): void; -} - -export interface IChatCodeBlockInfo { - codeBlockIndex: number; - element: ChatTreeItem; - uri: URI | undefined; - focus(): void; -} - -export interface IChatFileTreeInfo { - treeDataId: string; - treeIndex: number; - focus(): void; -} - -export type ChatTreeItem = IChatRequestViewModel | IChatResponseViewModel | IChatWelcomeMessageViewModel; - -export interface IChatListItemRendererOptions { - readonly renderStyle?: 'default' | 'compact' | 'minimal'; - readonly noHeader?: boolean; - readonly noPadding?: boolean; - readonly editableCodeBlock?: boolean; - readonly renderTextEditsAsSummary?: (uri: URI) => boolean; -} - -export interface IChatWidgetViewOptions { - renderInputOnTop?: boolean; - renderFollowups?: boolean; - renderStyle?: 'default' | 'compact' | 'minimal'; - supportsFileReferences?: boolean; - filter?: (item: ChatTreeItem) => boolean; - rendererOptions?: IChatListItemRendererOptions; - menus?: { - /** - * The menu that is inside the input editor, use for send, dictation - */ - executeToolbar?: MenuId; - /** - * The menu that next to the input editor, use for close, config etc - */ - inputSideToolbar?: MenuId; - /** - * The telemetry source for all commands of this widget - */ - telemetrySource?: string; - }; - defaultElementHeight?: number; - editorOverflowWidgetsDomNode?: HTMLElement; -} - -export interface IChatViewViewContext { - viewId: string; -} - -export interface IChatResourceViewContext { - resource: boolean; -} - -export type IChatWidgetViewContext = IChatViewViewContext | IChatResourceViewContext; - -export interface IChatWidget { - readonly onDidChangeViewModel: Event; - readonly onDidAcceptInput: Event; - readonly onDidHide: Event; - readonly onDidSubmitAgent: Event<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>; - readonly onDidChangeParsedInput: Event; - readonly onDidDeleteContext: Event; - readonly location: AideChatAgentLocation; - readonly viewContext: IChatWidgetViewContext; - readonly viewModel: IChatViewModel | undefined; - readonly inputEditor: ICodeEditor; - readonly supportsFileReferences: boolean; - readonly parsedInput: IParsedChatRequest; - lastSelectedAgent: IChatAgentData | undefined; - readonly scopedContextKeyService: IContextKeyService; - - getContrib(id: string): T | undefined; - reveal(item: ChatTreeItem): void; - focus(item: ChatTreeItem): void; - moveFocus(item: ChatTreeItem, type: 'next' | 'previous'): void; - getFocus(): ChatTreeItem | undefined; - setInput(query?: string): void; - getInput(): string; - logInputHistory(): void; - acceptInput(query?: string): Promise; - acceptInputWithPrefix(prefix: string): void; - setInputPlaceholder(placeholder: string): void; - resetInputPlaceholder(): void; - focusLastMessage(): void; - focusInput(): void; - hasInputFocus(): boolean; - getCodeBlockInfoForEditor(uri: URI): IChatCodeBlockInfo | undefined; - getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[]; - getFileTreeInfosForResponse(response: IChatResponseViewModel): IChatFileTreeInfo[]; - getLastFocusedFileTreeForResponse(response: IChatResponseViewModel): IChatFileTreeInfo | undefined; - setContext(overwrite: boolean, ...context: IAideChatRequestVariableEntry[]): void; - clear(): void; -} - -export interface IChatViewPane { - clear(): void; -} - - -export interface ICodeBlockActionContextProvider { - getCodeBlockContext(editor?: ICodeEditor): ICodeBlockActionContext | undefined; -} - -export const IAideChatCodeBlockContextProviderService = createDecorator('aideChatCodeBlockContextProviderService'); -export interface IAideChatCodeBlockContextProviderService { - readonly _serviceBrand: undefined; - readonly providers: ICodeBlockActionContextProvider[]; - registerProvider(provider: ICodeBlockActionContextProvider, id: string): IDisposable; -} - -export const GeneratingPhrase = localize('generating', "Generating"); - -export const CHAT_VIEW_ID = `workbench.panel.aideChat.view.${CHAT_PROVIDER_ID}`; diff --git a/src/vs/workbench/contrib/aideChat/browser/aideChatAccessibilityProvider.ts b/src/vs/workbench/contrib/aideChat/browser/aideChatAccessibilityProvider.ts deleted file mode 100644 index 223bffe225f..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/aideChatAccessibilityProvider.ts +++ /dev/null @@ -1,79 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { AriaRole } from '../../../../base/browser/ui/aria/aria.js'; -import { IListAccessibilityProvider } from '../../../../base/browser/ui/list/listWidget.js'; -import { marked } from '../../../../base/common/marked/marked.js'; -import { localize } from '../../../../nls.js'; -import { AccessibilityVerbositySettingId } from '../../../../workbench/contrib/accessibility/browser/accessibilityConfiguration.js'; -import { IAccessibleViewService } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { ChatTreeItem } from '../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { isRequestVM, isResponseVM, isWelcomeVM, IChatResponseViewModel } from '../../../../workbench/contrib/aideChat/common/aideChatViewModel.js'; - -export class ChatAccessibilityProvider implements IListAccessibilityProvider { - - constructor( - @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService - ) { - - } - getWidgetRole(): AriaRole { - return 'list'; - } - - getRole(element: ChatTreeItem): AriaRole | undefined { - return 'listitem'; - } - - getWidgetAriaLabel(): string { - return localize('aideChat', "Chat"); - } - - getAriaLabel(element: ChatTreeItem): string { - if (isRequestVM(element)) { - return element.messageText; - } - - if (isResponseVM(element)) { - return this._getLabelWithCodeBlockCount(element); - } - - if (isWelcomeVM(element)) { - return element.content.map(c => 'value' in c ? c.value : c.map(followup => followup.message).join('\n')).join('\n'); - } - - return ''; - } - - private _getLabelWithCodeBlockCount(element: IChatResponseViewModel): string { - const accessibleViewHint = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.Chat); - let label: string = ''; - const fileTreeCount = element.response.value.filter((v) => !('value' in v))?.length ?? 0; - let fileTreeCountHint = ''; - switch (fileTreeCount) { - case 0: - break; - case 1: - fileTreeCountHint = localize('singleFileTreeHint', "1 file tree"); - break; - default: - fileTreeCountHint = localize('multiFileTreeHint', "{0} file trees", fileTreeCount); - break; - } - const codeBlockCount = marked.lexer(element.response.asString()).filter(token => token.type === 'code')?.length ?? 0; - switch (codeBlockCount) { - case 0: - label = accessibleViewHint ? localize('noCodeBlocksHint', "{0} {1} {2}", fileTreeCountHint, element.response.asString(), accessibleViewHint) : localize('noCodeBlocks', "{0} {1}", fileTreeCountHint, element.response.asString()); - break; - case 1: - label = accessibleViewHint ? localize('singleCodeBlockHint', "{0} 1 code block: {1} {2}", fileTreeCountHint, element.response.asString(), accessibleViewHint) : localize('singleCodeBlock', "{0} 1 code block: {1}", fileTreeCountHint, element.response.asString()); - break; - default: - label = accessibleViewHint ? localize('multiCodeBlockHint', "{0} {1} code blocks: {2}", fileTreeCountHint, codeBlockCount, element.response.asString(), accessibleViewHint) : localize('multiCodeBlock', "{0} {1} code blocks", fileTreeCountHint, codeBlockCount, element.response.asString()); - break; - } - return label; - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/aideChatAccessibilityService.ts b/src/vs/workbench/contrib/aideChat/browser/aideChatAccessibilityService.ts deleted file mode 100644 index 721f1685b72..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/aideChatAccessibilityService.ts +++ /dev/null @@ -1,47 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { status } from '../../../../base/browser/ui/aria/aria.js'; -import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js'; -import { AccessibilitySignal, IAccessibilitySignalService } from '../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { AccessibilityProgressSignalScheduler } from '../../../../platform/accessibilitySignal/browser/progressAccessibilitySignalScheduler.js'; -import { IAideChatAccessibilityService } from '../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { IChatResponseViewModel } from '../../../../workbench/contrib/aideChat/common/aideChatViewModel.js'; -import { renderStringAsPlaintext } from '../../../../base/browser/markdownRenderer.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; - -const CHAT_RESPONSE_PENDING_ALLOWANCE_MS = 4000; -export class ChatAccessibilityService extends Disposable implements IAideChatAccessibilityService { - - declare readonly _serviceBrand: undefined; - - private _pendingSignalMap: DisposableMap = this._register(new DisposableMap()); - - private _requestId: number = 0; - - constructor(@IAccessibilitySignalService private readonly _accessibilitySignalService: IAccessibilitySignalService, @IInstantiationService private readonly _instantiationService: IInstantiationService) { - super(); - } - acceptRequest(): number { - this._requestId++; - this._accessibilitySignalService.playSignal(AccessibilitySignal.chatRequestSent, { allowManyInParallel: true }); - this._pendingSignalMap.set(this._requestId, this._instantiationService.createInstance(AccessibilityProgressSignalScheduler, CHAT_RESPONSE_PENDING_ALLOWANCE_MS, undefined)); - return this._requestId; - } - acceptResponse(response: IChatResponseViewModel | string | undefined, requestId: number): void { - this._pendingSignalMap.deleteAndDispose(requestId); - const isPanelChat = typeof response !== 'string'; - const responseContent = typeof response === 'string' ? response : response?.response.asString(); - this._accessibilitySignalService.playSignal(AccessibilitySignal.chatResponseReceived, { allowManyInParallel: true }); - if (!response || !responseContent) { - return; - } - const errorDetails = isPanelChat && response.errorDetails ? ` ${response.errorDetails.message}` : ''; - const plainTextResponse = renderStringAsPlaintext(new MarkdownString(responseContent)); - status(plainTextResponse + errorDetails); - } -} - diff --git a/src/vs/workbench/contrib/aideChat/browser/aideChatAgentHover.ts b/src/vs/workbench/contrib/aideChat/browser/aideChatAgentHover.ts deleted file mode 100644 index 260f24fe514..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/aideChatAgentHover.ts +++ /dev/null @@ -1,144 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from '../../../../base/browser/dom.js'; -import { IManagedHoverOptions } from '../../../../base/browser/ui/hover/hover.js'; -import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { FileAccess } from '../../../../base/common/network.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { URI } from '../../../../base/common/uri.js'; -import { localize } from '../../../../nls.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { getFullyQualifiedId, IChatAgentData, IAideChatAgentNameService, IAideChatAgentService } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { showExtensionsWithIdsCommandId } from '../../../../workbench/contrib/extensions/browser/extensionsActions.js'; -import { verifiedPublisherIcon } from '../../../../workbench/contrib/extensions/browser/extensionsIcons.js'; -import { IExtensionsWorkbenchService } from '../../../../workbench/contrib/extensions/common/extensions.js'; - -const h = dom.h; - -export class ChatAgentHover extends Disposable { - public readonly domNode: HTMLElement; - - private readonly icon: HTMLElement; - private readonly name: HTMLElement; - private readonly extensionName: HTMLElement; - private readonly publisherName: HTMLElement; - private readonly description: HTMLElement; - - private readonly _onDidChangeContents = this._register(new Emitter()); - public readonly onDidChangeContents: Event = this._onDidChangeContents.event; - - constructor( - @IAideChatAgentService private readonly chatAgentService: IAideChatAgentService, - @IExtensionsWorkbenchService private readonly extensionService: IExtensionsWorkbenchService, - @IAideChatAgentNameService private readonly chatAgentNameService: IAideChatAgentNameService, - ) { - super(); - - const hoverElement = h( - '.chat-agent-hover@root', - [ - h('.chat-agent-hover-header', [ - h('.chat-agent-hover-icon@icon'), - h('.chat-agent-hover-details', [ - h('.chat-agent-hover-name@name'), - h('.chat-agent-hover-extension', [ - h('.chat-agent-hover-extension-name@extensionName'), - h('.chat-agent-hover-separator@separator'), - h('.chat-agent-hover-publisher@publisher'), - ]), - ]), - ]), - h('.chat-agent-hover-warning@warning'), - h('span.chat-agent-hover-description@description'), - ]); - this.domNode = hoverElement.root; - - this.icon = hoverElement.icon; - this.name = hoverElement.name; - this.extensionName = hoverElement.extensionName; - this.description = hoverElement.description; - - hoverElement.separator.textContent = '|'; - - const verifiedBadge = dom.$('span.extension-verified-publisher', undefined, renderIcon(verifiedPublisherIcon)); - - this.publisherName = dom.$('span.chat-agent-hover-publisher-name'); - dom.append( - hoverElement.publisher, - verifiedBadge, - this.publisherName); - - hoverElement.warning.appendChild(renderIcon(Codicon.warning)); - hoverElement.warning.appendChild(dom.$('span', undefined, localize('reservedName', "This chat extension is using a reserved name."))); - } - - setAgent(id: string): void { - const agent = this.chatAgentService.getAgent(id)!; - if (agent.metadata.icon instanceof URI) { - const avatarIcon = dom.$('img.icon'); - avatarIcon.src = FileAccess.uriToBrowserUri(agent.metadata.icon).toString(true); - this.icon.replaceChildren(dom.$('.avatar', undefined, avatarIcon)); - } else if (agent.metadata.themeIcon instanceof URI) { - const avatarIcon = dom.$('img.icon'); - avatarIcon.src = FileAccess.uriToBrowserUri(agent.metadata.themeIcon).toString(true); - this.icon.replaceChildren(dom.$('.avatar.codicon-avatar', undefined, avatarIcon)); - } else if (agent.metadata.themeIcon) { - const avatarIcon = dom.$(ThemeIcon.asCSSSelector(agent.metadata.themeIcon)); - this.icon.replaceChildren(dom.$('.avatar.codicon-avatar', undefined, avatarIcon)); - } - - this.domNode.classList.toggle('noExtensionName', !!agent.isDynamic); - - const isAllowed = this.chatAgentNameService.getAgentNameRestriction(agent); - this.name.textContent = isAllowed ? `@${agent.name}` : getFullyQualifiedId(agent); - this.extensionName.textContent = agent.extensionDisplayName; - this.publisherName.textContent = agent.publisherDisplayName ?? agent.extensionPublisherId; - - let description = agent.description ?? ''; - if (description) { - if (!description.match(/[\.\?\!] *$/)) { - description += '.'; - } - } - - this.description.textContent = description; - this.domNode.classList.toggle('allowedName', isAllowed); - - this.domNode.classList.toggle('verifiedPublisher', false); - if (!agent.isDynamic) { - const cancel = this._register(new CancellationTokenSource()); - this.extensionService.getExtensions([{ id: agent.extensionId.value }], cancel.token).then(extensions => { - cancel.dispose(); - const extension = extensions[0]; - if (extension?.publisherDomain?.verified) { - this.domNode.classList.toggle('verifiedPublisher', true); - this._onDidChangeContents.fire(); - } - }); - } - } -} - -export function getChatAgentHoverOptions(getAgent: () => IChatAgentData | undefined, commandService: ICommandService): IManagedHoverOptions { - return { - actions: [ - { - commandId: showExtensionsWithIdsCommandId, - label: localize('viewExtensionLabel', "View Extension"), - run: () => { - const agent = getAgent(); - if (agent) { - commandService.executeCommand(showExtensionsWithIdsCommandId, [agent.extensionId.value]); - } - }, - } - ] - }; -} diff --git a/src/vs/workbench/contrib/aideChat/browser/aideChatEditor.ts b/src/vs/workbench/contrib/aideChat/browser/aideChatEditor.ts deleted file mode 100644 index 4ce5a70cb46..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/aideChatEditor.ts +++ /dev/null @@ -1,131 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from '../../../../base/browser/dom.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { IContextKeyService, IScopedContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IEditorOptions } from '../../../../platform/editor/common/editor.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { editorBackground, editorForeground, inputBackground } from '../../../../platform/theme/common/colorRegistry.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { EditorPane } from '../../../../workbench/browser/parts/editor/editorPane.js'; -import { IEditorOpenContext } from '../../../../workbench/common/editor.js'; -import { Memento } from '../../../../workbench/common/memento.js'; -import { AideChatEditorInput } from '../../../../workbench/contrib/aideChat/browser/aideChatEditorInput.js'; -import { IChatViewState, ChatWidget } from '../../../../workbench/contrib/aideChat/browser/aideChatWidget.js'; -import { IChatModel, IExportableChatData, ISerializableChatData } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { clearChatEditor } from '../../../../workbench/contrib/aideChat/browser/actions/aideChatClear.js'; -import { IEditorGroup } from '../../../../workbench/services/editor/common/editorGroupsService.js'; -import { AideChatAgentLocation } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { CHAT_PROVIDER_ID } from '../../../../workbench/contrib/aideChat/common/aideChatParticipantContribTypes.js'; - -export interface IChatEditorOptions extends IEditorOptions { - target?: { sessionId: string } | { data: IExportableChatData | ISerializableChatData }; -} - -export class ChatEditor extends EditorPane { - private widget!: ChatWidget; - - private _scopedContextKeyService!: IScopedContextKeyService; - override get scopedContextKeyService() { - return this._scopedContextKeyService; - } - - private _memento: Memento | undefined; - private _viewState: IChatViewState | undefined; - - constructor( - group: IEditorGroup, - @ITelemetryService telemetryService: ITelemetryService, - @IThemeService themeService: IThemeService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IStorageService private readonly storageService: IStorageService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - ) { - super(AideChatEditorInput.EditorID, group, telemetryService, themeService, storageService); - } - - public async clear() { - return this.instantiationService.invokeFunction(clearChatEditor); - } - - protected override createEditor(parent: HTMLElement): void { - this._scopedContextKeyService = this._register(this.contextKeyService.createScoped(parent)); - const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); - - this.widget = this._register( - scopedInstantiationService.createInstance( - ChatWidget, - AideChatAgentLocation.Panel, - { resource: true }, - { supportsFileReferences: true }, - { - listForeground: editorForeground, - listBackground: editorBackground, - inputEditorBackground: inputBackground, - resultEditorBackground: editorBackground - })); - this._register(this.widget.onDidClear(() => this.clear())); - this.widget.render(parent); - this.widget.setVisible(true); - } - - protected override setEditorVisible(visible: boolean): void { - super.setEditorVisible(visible); - - this.widget?.setVisible(visible); - } - - public override focus(): void { - super.focus(); - - this.widget?.focusInput(); - } - - override clearInput(): void { - this.saveState(); - super.clearInput(); - } - - override async setInput(input: AideChatEditorInput, options: IChatEditorOptions | undefined, context: IEditorOpenContext, token: CancellationToken): Promise { - super.setInput(input, options, context, token); - - const editorModel = await input.resolve(); - if (!editorModel) { - throw new Error(`Failed to get model for chat editor. id: ${input.sessionId}`); - } - - if (!this.widget) { - throw new Error('ChatEditor lifecycle issue: no editor widget'); - } - - this.updateModel(editorModel.model, options?.viewState ?? input.options.viewState); - } - - private updateModel(model: IChatModel, viewState?: IChatViewState): void { - this._memento = new Memento('aide-chat-editor-' + CHAT_PROVIDER_ID, this.storageService); - this._viewState = viewState ?? this._memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE) as IChatViewState; - this.widget.setModel(model, { ...this._viewState }); - } - - protected override saveState(): void { - this.widget?.saveState(); - - if (this._memento && this._viewState) { - const widgetViewState = this.widget.getViewState(); - this._viewState.inputValue = widgetViewState.inputValue; - this._memento.saveMemento(); - } - } - - override layout(dimension: dom.Dimension, position?: dom.IDomPosition | undefined): void { - if (this.widget) { - this.widget.layout(dimension.height, dimension.width); - } - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/aideChatEditorInput.ts b/src/vs/workbench/contrib/aideChat/browser/aideChatEditorInput.ts deleted file mode 100644 index bcab33675b8..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/aideChatEditorInput.ts +++ /dev/null @@ -1,212 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { Codicon } from '../../../../base/common/codicons.js'; -import { Emitter } from '../../../../base/common/event.js'; -import { Disposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { URI } from '../../../../base/common/uri.js'; -import * as nls from '../../../../nls.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; -import { EditorInputCapabilities, IEditorSerializer, IUntypedEditorInput } from '../../../../workbench/common/editor.js'; -import { EditorInput } from '../../../../workbench/common/editor/editorInput.js'; -import type { IChatEditorOptions } from '../../../../workbench/contrib/aideChat/browser/aideChatEditor.js'; -import { AideChatAgentLocation } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { IChatModel } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { IAideChatService } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; - -const ChatEditorIcon = registerIcon('chat-editor-label-icon', Codicon.commentDiscussion, nls.localize('aideChatEditorLabelIcon', 'Icon of the chat editor label.')); - -export class AideChatEditorInput extends EditorInput { - static readonly countsInUse = new Set(); - - static readonly TypeID: string = 'workbench.input.aideChatSession'; - static readonly EditorID: string = 'workbench.editor.aideChatSession'; - - private readonly inputCount: number; - public sessionId: string | undefined; - - private model: IChatModel | undefined; - - static getNewEditorUri(): URI { - const handle = Math.floor(Math.random() * 1e9); - return ChatUri.generate(handle); - } - - static getNextCount(): number { - let count = 0; - while (AideChatEditorInput.countsInUse.has(count)) { - count++; - } - - return count; - } - - constructor( - readonly resource: URI, - readonly options: IChatEditorOptions, - @IAideChatService private readonly chatService: IAideChatService - ) { - super(); - - 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 : - undefined; - this.inputCount = AideChatEditorInput.getNextCount(); - AideChatEditorInput.countsInUse.add(this.inputCount); - this._register(toDisposable(() => AideChatEditorInput.countsInUse.delete(this.inputCount))); - } - - override get editorId(): string | undefined { - return AideChatEditorInput.EditorID; - } - - override get capabilities(): EditorInputCapabilities { - return super.capabilities | EditorInputCapabilities.Singleton; - } - - override matches(otherInput: EditorInput | IUntypedEditorInput): boolean { - return otherInput instanceof AideChatEditorInput && otherInput.resource.toString() === this.resource.toString(); - } - - override get typeId(): string { - return AideChatEditorInput.TypeID; - } - - override getName(): string { - return this.model?.title || nls.localize('aideChatEditorName', "Aide") + (this.inputCount > 0 ? ` ${this.inputCount + 1}` : ''); - } - - override getIcon(): ThemeIcon { - return ChatEditorIcon; - } - - override async resolve(): Promise { - if (typeof this.sessionId === 'string') { - this.model = this.chatService.getOrRestoreSession(this.sessionId); - } else if (!this.options.target) { - this.model = this.chatService.startSession(AideChatAgentLocation.Panel, CancellationToken.None); - } else if ('data' in this.options.target) { - this.model = this.chatService.loadSessionFromContent(this.options.target.data); - } - - if (!this.model) { - return null; - } - - this.sessionId = this.model.sessionId; - this._register(this.model.onDidChange(() => this._onDidChangeLabel.fire())); - - return this._register(new ChatEditorModel(this.model)); - } - - override dispose(): void { - super.dispose(); - if (this.sessionId) { - this.chatService.clearSession(this.sessionId); - } - } -} - -export class ChatEditorModel extends Disposable { - private _onWillDispose = this._register(new Emitter()); - readonly onWillDispose = this._onWillDispose.event; - - private _isDisposed = false; - private _isResolved = false; - - constructor( - readonly model: IChatModel - ) { super(); } - - async resolve(): Promise { - this._isResolved = true; - } - - isResolved(): boolean { - return this._isResolved; - } - - isDisposed(): boolean { - return this._isDisposed; - } - - override dispose(): void { - super.dispose(); - this._isDisposed = true; - } -} - -export namespace ChatUri { - - export const scheme = Schemas.vscodeChatSesssion; - - - export function generate(handle: number): URI { - return URI.from({ scheme, path: `chat-${handle}` }); - } - - export function parse(resource: URI): { handle: number } | undefined { - if (resource.scheme !== scheme) { - return undefined; - } - - const match = resource.path.match(/chat-(\d+)/); - const handleStr = match?.[1]; - if (typeof handleStr !== 'string') { - return undefined; - } - - const handle = parseInt(handleStr); - if (isNaN(handle)) { - return undefined; - } - - return { handle }; - } -} - -interface ISerializedChatEditorInput { - options: IChatEditorOptions; - sessionId: string; - resource: URI; -} - -export class ChatEditorInputSerializer implements IEditorSerializer { - canSerialize(input: EditorInput): input is AideChatEditorInput & { readonly sessionId: string } { - return input instanceof AideChatEditorInput && typeof input.sessionId === 'string'; - } - - serialize(input: EditorInput): string | undefined { - if (!this.canSerialize(input)) { - return undefined; - } - - const obj: ISerializedChatEditorInput = { - options: input.options, - sessionId: input.sessionId, - resource: input.resource - }; - return JSON.stringify(obj); - } - - deserialize(instantiationService: IInstantiationService, serializedEditor: string): EditorInput | undefined { - try { - const parsed: ISerializedChatEditorInput = JSON.parse(serializedEditor); - const resource = URI.revive(parsed.resource); - return instantiationService.createInstance(AideChatEditorInput, resource, { ...parsed.options, target: { sessionId: parsed.sessionId } }); - } catch (err) { - return undefined; - } - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/aideChatFollowups.ts b/src/vs/workbench/contrib/aideChat/browser/aideChatFollowups.ts deleted file mode 100644 index fae28f2aa85..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/aideChatFollowups.ts +++ /dev/null @@ -1,70 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from '../../../../base/browser/dom.js'; -import { Button, IButtonStyles } from '../../../../base/browser/ui/button/button.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { localize } from '../../../../nls.js'; -import { AideChatAgentLocation, IAideChatAgentService } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { chatAgentLeader, chatSubcommandLeader } from '../../../../workbench/contrib/aideChat/common/aideChatParserTypes.js'; -import { IAideChatFollowup } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; - -const $ = dom.$; - -export class ChatFollowups extends Disposable { - constructor( - container: HTMLElement, - followups: T[], - private readonly location: AideChatAgentLocation, - private readonly options: IButtonStyles | undefined, - private readonly clickHandler: (followup: T) => void, - @IAideChatAgentService private readonly chatAgentService: IAideChatAgentService - ) { - super(); - - const followupsContainer = dom.append(container, $('.aide-chat-followups')); - followups.forEach(followup => this.renderFollowup(followupsContainer, followup)); - } - - private renderFollowup(container: HTMLElement, followup: T): void { - - if (!this.chatAgentService.getDefaultAgent(this.location)) { - // No default agent yet, which affects how followups are rendered, so can't render this yet - return; - } - - let tooltipPrefix = ''; - if ('agentId' in followup && followup.agentId && followup.agentId !== this.chatAgentService.getDefaultAgent(this.location)?.id) { - const agent = this.chatAgentService.getAgent(followup.agentId); - if (!agent) { - // Refers to agent that doesn't exist - return; - } - - tooltipPrefix += `${chatAgentLeader}${agent.name} `; - if ('subCommand' in followup && followup.subCommand) { - tooltipPrefix += `${chatSubcommandLeader}${followup.subCommand} `; - } - } - - const baseTitle = followup.kind === 'reply' ? - (followup.title || followup.message) - : followup.title; - const message = followup.kind === 'reply' ? followup.message : followup.title; - const tooltip = (tooltipPrefix + - ('tooltip' in followup && followup.tooltip || message)).trim(); - const button = this._register(new Button(container, { ...this.options, title: tooltip })); - if (followup.kind === 'reply') { - button.element.classList.add('interactive-followup-reply'); - } else if (followup.kind === 'command') { - button.element.classList.add('interactive-followup-command'); - } - button.element.ariaLabel = localize('followUpAriaLabel', "Follow up question: {0}", baseTitle); - button.label = new MarkdownString(baseTitle); - - this._register(button.onDidClick(() => this.clickHandler(followup))); - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/aideChatInputPart.ts b/src/vs/workbench/contrib/aideChat/browser/aideChatInputPart.ts deleted file mode 100644 index 0487eb26cec..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/aideChatInputPart.ts +++ /dev/null @@ -1,715 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from '../../../../base/browser/dom.js'; -import { IHistoryNavigationWidget } from '../../../../base/browser/history.js'; -import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; -import * as aria from '../../../../base/browser/ui/aria/aria.js'; -import { Button } from '../../../../base/browser/ui/button/button.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { Emitter } from '../../../../base/common/event.js'; -import { HistoryNavigator2 } from '../../../../base/common/history.js'; -import { KeyCode } from '../../../../base/common/keyCodes.js'; -import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { FileAccess } from '../../../../base/common/network.js'; -import { basename, dirname } from '../../../../base/common/path.js'; -import { isMacintosh } from '../../../../base/common/platform.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { URI } from '../../../../base/common/uri.js'; -import { IEditorConstructionOptions } from '../../../../editor/browser/config/editorConfiguration.js'; -import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js'; -import { CodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; -import { EDITOR_FONT_DEFAULTS } from '../../../../editor/common/config/editorOptions.js'; -import { IDimension } from '../../../../editor/common/core/dimension.js'; -import { IPosition } from '../../../../editor/common/core/position.js'; -import { Range } from '../../../../editor/common/core/range.js'; -import { ITextModel } from '../../../../editor/common/model.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; -import { ContentHoverController } from '../../../../editor/contrib/hover/browser/contentHoverController.js'; -import { localize } from '../../../../nls.js'; -import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; -import { ActionViewItemWithKb } from '../../../../platform/actionbarWithKeybindings/browser/actionViewItemWithKb.js'; -import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; -import { MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js'; -import { IAIModelSelectionService } from '../../../../platform/aiModel/common/aiModels.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { FileKind } from '../../../../platform/files/common/files.js'; -import { registerAndCreateHistoryNavigationContext } from '../../../../platform/history/browser/contextScopedHistoryWidget.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; -import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { ResourceLabels } from '../../../../workbench/browser/labels.js'; -import { AccessibilityVerbositySettingId } from '../../../../workbench/contrib/accessibility/browser/accessibilityConfiguration.js'; -import { AccessibilityCommandId } from '../../../../workbench/contrib/accessibility/common/accessibilityCommands.js'; -import { ClearChatEditorAction } from '../../../../workbench/contrib/aideChat/browser/actions/aideChatClearActions.js'; -import { CancelAction, IChatExecuteActionContext, SubmitAction } from '../../../../workbench/contrib/aideChat/browser/actions/aideChatExecuteActions.js'; -import { IChatRequester, IChatWidget } from '../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { ChatFollowups } from '../../../../workbench/contrib/aideChat/browser/aideChatFollowups.js'; -import { AideChatAgentLocation } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { CONTEXT_CHAT_INPUT_CURSOR_AT_TOP, CONTEXT_CHAT_INPUT_HAS_FOCUS, CONTEXT_CHAT_INPUT_HAS_TEXT, CONTEXT_IN_CHAT_INPUT } from '../../../../workbench/contrib/aideChat/common/aideChatContextKeys.js'; -import { IAideChatRequestVariableEntry } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { IAideChatFollowup } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { IChatResponseViewModel } from '../../../../workbench/contrib/aideChat/common/aideChatViewModel.js'; -import { IAideChatWidgetHistoryService, IChatHistoryEntry } from '../../../../workbench/contrib/aideChat/common/aideChatWidgetHistoryService.js'; -import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions } from '../../../../workbench/contrib/codeEditor/browser/simpleEditorOptions.js'; -import { ModelSelectionIndicator } from '../../../../workbench/contrib/preferences/browser/modelSelectionIndicator.js'; -import { IExtensionService } from '../../../../workbench/services/extensions/common/extensions.js'; - -const $ = dom.$; - -const INPUT_EDITOR_MIN_HEIGHT = 100; - -interface IChatInputPartOptions { - renderFollowups: boolean; - renderStyle?: 'default' | 'compact'; - menus: { - executeToolbar: MenuId; - primaryToolbar: MenuId; - inputSideToolbar?: MenuId; - telemetrySource?: string; - }; - editorOverflowWidgetsDomNode?: HTMLElement; -} - -export class ChatInputPart extends Disposable implements IHistoryNavigationWidget { - static readonly INPUT_SCHEME = 'aideChatSessionInput'; - private static _counter = 0; - - private _onDidLoadInputState = this._register(new Emitter()); - readonly onDidLoadInputState = this._onDidLoadInputState.event; - - private _onDidChangeHeight = this._register(new Emitter()); - readonly onDidChangeHeight = this._onDidChangeHeight.event; - - private _onDidFocus = this._register(new Emitter()); - readonly onDidFocus = this._onDidFocus.event; - - private _onDidBlur = this._register(new Emitter()); - readonly onDidBlur = this._onDidBlur.event; - - private _onDidDeleteContext = this._register(new Emitter()); - readonly onDidDeleteContext = this._onDidDeleteContext.event; - - private _onDidAcceptFollowup = this._register(new Emitter<{ followup: IAideChatFollowup; response: IChatResponseViewModel | undefined }>()); - readonly onDidAcceptFollowup = this._onDidAcceptFollowup.event; - - public get attachedContext() { - return this._attachedContext; - } - - private _indexOfLastAttachedContextDeletedWithKeyboard: number = -1; - private readonly _attachedContext = new Set(); - - private readonly _onDidChangeVisibility = this._register(new Emitter()); - private readonly _contextResourceLabels = this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility.event }); - - private inputEditorHeight = 0; - private container!: HTMLElement; - - private inputSideToolbarContainer?: HTMLElement; - - private followupsContainer!: HTMLElement; - private readonly followupsDisposables = this._register(new DisposableStore()); - - private attachedContextContainer!: HTMLElement; - private readonly attachedContextDisposables = this._register(new DisposableStore()); - - private _inputPartHeight: number = 0; - get inputPartHeight() { - return this._inputPartHeight; - } - - private _inputEditor!: CodeEditorWidget; - private _inputEditorElement!: HTMLElement; - - protected requesterContainer: HTMLElement | undefined; - protected modelNameContainer: HTMLElement | undefined; - - private toolbar!: MenuWorkbenchToolBar; - private primaryToolbar!: MenuWorkbenchToolBar; - - get inputEditor() { - return this._inputEditor; - } - - private history: HistoryNavigator2; - private historyNavigationBackwardsEnablement!: IContextKey; - private historyNavigationForewardsEnablement!: IContextKey; - private inHistoryNavigation = false; - private inputModel: ITextModel | undefined; - private inputEditorHasText: IContextKey; - private chatCursorAtTop: IContextKey; - private inputEditorHasFocus: IContextKey; - - private cachedDimensions: dom.Dimension | undefined; - private cachedToolbarWidth: number | undefined; - - readonly inputUri = URI.parse(`${ChatInputPart.INPUT_SCHEME}:input-${ChatInputPart._counter++}`); - - constructor( - // private readonly editorOptions: ChatEditorOptions, // TODO this should be used - private readonly location: AideChatAgentLocation, - private readonly options: IChatInputPartOptions, - @IAideChatWidgetHistoryService private readonly historyService: IAideChatWidgetHistoryService, - @IModelService private readonly modelService: IModelService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IKeybindingService private readonly keybindingService: IKeybindingService, - @IAccessibilityService private readonly accessibilityService: IAccessibilityService, - @IAIModelSelectionService private readonly aiModelSelectionService: IAIModelSelectionService, - @ICommandService private readonly commandService: ICommandService, - @ILogService private readonly logService: ILogService, - @IExtensionService private readonly extensionService: IExtensionService - ) { - super(); - - this.inputEditorHasText = CONTEXT_CHAT_INPUT_HAS_TEXT.bindTo(contextKeyService); - this.chatCursorAtTop = CONTEXT_CHAT_INPUT_CURSOR_AT_TOP.bindTo(contextKeyService); - this.inputEditorHasFocus = CONTEXT_CHAT_INPUT_HAS_FOCUS.bindTo(contextKeyService); - - this.history = this.loadHistory(); - this._register(this.historyService.onDidClearHistory(() => this.history = new HistoryNavigator2([{ text: '' }], 50, historyKeyFn))); - - this._register(this.configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(AccessibilityVerbositySettingId.Chat)) { - this.inputEditor.updateOptions({ ariaLabel: this._getAriaLabel() }); - } - })); - this._register(this.aiModelSelectionService.onDidChangeModelSelection(() => { - this._renderModelName(); - })); - } - - private loadHistory(): HistoryNavigator2 { - const history = this.historyService.getHistory(this.location); - if (history.length === 0) { - history.push({ text: '' }); - } - - return new HistoryNavigator2(history, 50, historyKeyFn); - } - - private _getAriaLabel(): string { - const verbose = this.configurationService.getValue(AccessibilityVerbositySettingId.Chat); - if (verbose) { - const kbLabel = this.keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel(); - return kbLabel ? localize('actions.chat.accessibiltyHelp', "Chat Input, Type to ask questions or type / for topics, press enter to send out the request. Use {0} for Chat Accessibility Help.", kbLabel) : localize('aideChatInput.accessibilityHelpNoKb', "Chat Input, Type code here and press Enter to run. Use the Chat Accessibility Help command for more information."); - } - return localize('aideChatInput', "Chat Input"); - } - - updateState(inputState: Object): void { - if (this.inHistoryNavigation) { - return; - } - - const newEntry = { text: this._inputEditor.getValue(), state: inputState }; - - if (this.history.isAtEnd()) { - // The last history entry should always be the current input value - this.history.replaceLast(newEntry); - } else { - // Added a reference while in the middle of history navigation, it's a new entry - this.history.replaceLast(newEntry); - this.history.resetCursor(); - } - } - - initForNewChatModel(inputValue: string | undefined, inputState: Object): void { - this.history = this.loadHistory(); - this.history.add({ text: inputValue ?? this.history.current().text, state: inputState }); - - if (inputValue) { - this.setValue(inputValue, false); - } - } - - logInputHistory(): void { - const historyStr = [...this.history].map(entry => JSON.stringify(entry)).join('\n'); - this.logService.info(`[${this.location}] Chat input history:`, historyStr); - } - - setVisible(visible: boolean): void { - this._onDidChangeVisibility.fire(visible); - } - - get element(): HTMLElement { - return this.container; - } - - showPreviousValue(): void { - if (this.history.isAtEnd()) { - this.saveCurrentValue(); - } else { - if (!this.history.has({ text: this._inputEditor.getValue(), state: this.history.current().state })) { - this.saveCurrentValue(); - this.history.resetCursor(); - } - } - - this.navigateHistory(true); - } - - showNextValue(): void { - if (this.history.isAtEnd()) { - return; - } else { - if (!this.history.has({ text: this._inputEditor.getValue(), state: this.history.current().state })) { - this.saveCurrentValue(); - this.history.resetCursor(); - } - } - - this.navigateHistory(false); - } - - private navigateHistory(previous: boolean): void { - const historyEntry = previous ? - this.history.previous() : this.history.next(); - - aria.status(historyEntry.text); - - this.inHistoryNavigation = true; - this.setValue(historyEntry.text, true); - this.inHistoryNavigation = false; - - this._onDidLoadInputState.fire(historyEntry.state); - if (previous) { - this._inputEditor.setPosition({ lineNumber: 1, column: 1 }); - } else { - const model = this._inputEditor.getModel(); - if (!model) { - return; - } - - this._inputEditor.setPosition(getLastPosition(model)); - } - } - - setValue(value: string, transient: boolean): void { - this.inputEditor.setValue(value); - // always leave cursor at the end - this.inputEditor.setPosition({ lineNumber: 1, column: value.length + 1 }); - - if (!transient) { - this.saveCurrentValue(); - } - } - - private saveCurrentValue(): void { - const newEntry = { text: this._inputEditor.getValue(), state: this.history.current().state }; - this.history.replaceLast(newEntry); - } - - focus() { - this._inputEditor.focus(); - } - - hasFocus(): boolean { - return this._inputEditor.hasWidgetFocus(); - } - - /** - * Reset the input and update history. - * @param userQuery If provided, this will be added to the history. Followups and programmatic queries should not be passed. - */ - async acceptInput(isUserQuery?: boolean): Promise { - if (isUserQuery) { - const userQuery = this._inputEditor.getValue(); - const entry: IChatHistoryEntry = { text: userQuery, state: this.history.current().state }; - this.history.replaceLast(entry); - this.history.add({ text: '' }); - } - - this._onDidLoadInputState.fire({}); - if (this.accessibilityService.isScreenReaderOptimized() && isMacintosh) { - this._acceptInputForVoiceover(); - } else { - this._inputEditor.focus(); - this._inputEditor.setValue(''); - } - } - - private _acceptInputForVoiceover(): void { - const domNode = this._inputEditor.getDomNode(); - if (!domNode) { - return; - } - // Remove the input editor from the DOM temporarily to prevent VoiceOver - // from reading the cleared text (the request) to the user. - domNode.remove(); - this._inputEditor.setValue(''); - this._inputEditorElement.appendChild(domNode); - this._inputEditor.focus(); - } - - attachContext(...contentReferences: IAideChatRequestVariableEntry[]): void { - for (const reference of contentReferences) { - this.attachedContext.add(reference); - } - - this.initAttachedContext(this.attachedContextContainer); - } - - render(container: HTMLElement, initialValue: string, widget: IChatWidget) { - this.container = dom.append(container, $('.cschat-input-part')); - this.container.classList.toggle('compact', this.options.renderStyle === 'compact'); - - const secondChild = this.container.childNodes[1]; - const header = $('.header'); - this.container.insertBefore(header, secondChild); - const user = dom.append(header, $('.user')); - - const model = new Button($('.slow-model'), { - buttonBackground: undefined, - buttonBorder: undefined, - buttonForeground: undefined, - buttonHoverBackground: undefined, - buttonSecondaryBackground: undefined, - buttonSecondaryForeground: undefined, - buttonSecondaryHoverBackground: undefined, - buttonSeparator: undefined - }); - model.onDidClick(() => { - this.commandService.executeCommand(ModelSelectionIndicator.SWITCH_SLOW_MODEL_COMMAND_ID); - }); - this._renderModelName(); - - dom.append(header, model.element); - dom.append(user, $('.avatar-container')); - dom.append(user, $('h3.username')); - this.requesterContainer = user; - this.modelNameContainer = model.element; - this.modelNameContainer.style.display = 'none'; - this._renderRequester(); - - this.followupsContainer = dom.append(this.container, $('.interactive-input-followups')); - this.attachedContextContainer = dom.append(this.container, $('.chat-attached-context')); - this.initAttachedContext(this.attachedContextContainer); - const inputAndSideToolbar = dom.append(this.container, $('.interactive-input-and-side-toolbar')); - const inputContainer = dom.append(inputAndSideToolbar, $('.interactive-input-and-execute-toolbar')); - - const inputScopedContextKeyService = this._register(this.contextKeyService.createScoped(inputContainer)); - CONTEXT_IN_CHAT_INPUT.bindTo(inputScopedContextKeyService).set(true); - const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, inputScopedContextKeyService])); - - const { historyNavigationBackwardsEnablement, historyNavigationForwardsEnablement } = this._register(registerAndCreateHistoryNavigationContext(inputScopedContextKeyService, this)); - this.historyNavigationBackwardsEnablement = historyNavigationBackwardsEnablement; - this.historyNavigationForewardsEnablement = historyNavigationForwardsEnablement; - - const options: IEditorConstructionOptions = getSimpleEditorOptions(this.configurationService); - options.overflowWidgetsDomNode = this.options.editorOverflowWidgetsDomNode; - options.readOnly = false; - options.ariaLabel = this._getAriaLabel(); - options.fontFamily = EDITOR_FONT_DEFAULTS.fontFamily; - options.fontSize = 13; - options.lineHeight = 20; - options.padding = this.options.renderStyle === 'compact' ? { top: 2, bottom: 2 } : { top: 8, bottom: 8 }; - options.cursorWidth = 3; - options.wrappingStrategy = 'advanced'; - options.bracketPairColorization = { enabled: false }; - options.suggest = { - showIcons: false, - showSnippets: false, - showWords: true, - showStatusBar: false, - insertMode: 'replace', - }; - options.scrollbar = { ...(options.scrollbar ?? {}), vertical: 'hidden' }; - - this._inputEditorElement = dom.append(inputContainer, $('.interactive-input-editor')); - const editorOptions = getSimpleCodeEditorWidgetOptions(); - editorOptions.contributions?.push(...EditorExtensionsRegistry.getSomeEditorContributions([ContentHoverController.ID])); - this._inputEditor = this._register(scopedInstantiationService.createInstance(CodeEditorWidget, this._inputEditorElement, options, editorOptions)); - - this._register(this._inputEditor.onDidChangeModelContent(() => { - const currentHeight = Math.max(this._inputEditor.getContentHeight(), INPUT_EDITOR_MIN_HEIGHT); - if (currentHeight !== this.inputEditorHeight) { - this.inputEditorHeight = currentHeight; - this._onDidChangeHeight.fire(); - } - - const model = this._inputEditor.getModel(); - const inputHasText = !!model && model.getValue().trim().length > 0; - this.inputEditorHasText.set(inputHasText); - })); - this._register(this._inputEditor.onDidFocusEditorText(() => { - this.inputEditorHasFocus.set(true); - this._onDidFocus.fire(); - inputContainer.classList.toggle('focused', true); - })); - this._register(this._inputEditor.onDidBlurEditorText(() => { - this.inputEditorHasFocus.set(false); - inputContainer.classList.toggle('focused', false); - - this._onDidBlur.fire(); - })); - this._register(this._inputEditor.onDidChangeCursorPosition(e => { - const model = this._inputEditor.getModel(); - if (!model) { - return; - } - - const atTop = e.position.column === 1 && e.position.lineNumber === 1; - this.chatCursorAtTop.set(atTop); - - this.historyNavigationBackwardsEnablement.set(atTop); - this.historyNavigationForewardsEnablement.set(e.position.equals(getLastPosition(model))); - })); - - const toolbarsContainer = dom.append(inputContainer, $('.interactive-input-toolbars')); - - this.primaryToolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, toolbarsContainer, this.options.menus.primaryToolbar, { - telemetrySource: this.options.menus.telemetrySource, - menuOptions: { - shouldForwardArgs: true - } - })); - this.primaryToolbar.getElement().classList.add('interactive-aide-toolbar'); - this.primaryToolbar.context = { widget } satisfies IChatExecuteActionContext; - - this.toolbar = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, toolbarsContainer, this.options.menus.executeToolbar, { - telemetrySource: this.options.menus.telemetrySource, - menuOptions: { - shouldForwardArgs: true - }, - hiddenItemStrategy: HiddenItemStrategy.Ignore, // keep it lean when hiding items and avoid a "..." overflow menu - actionViewItemProvider: (action, options) => { - if (this.location === AideChatAgentLocation.Panel) { - if ((action.id === SubmitAction.ID || action.id === CancelAction.ID || action.id === ClearChatEditorAction.ID) && action instanceof MenuItemAction) { - return this.instantiationService.createInstance(ActionViewItemWithKb, action); - } - } - - return undefined; - } - })); - this.toolbar.getElement().classList.add('interactive-execute-toolbar'); - this.toolbar.context = { widget } satisfies IChatExecuteActionContext; - this._register(this.toolbar.onDidChangeMenuItems(() => { - if (this.cachedDimensions && typeof this.cachedToolbarWidth === 'number' && this.cachedToolbarWidth !== this.toolbar.getItemsWidth()) { - this.layout(this.cachedDimensions.height, this.cachedDimensions.width); - } - })); - - if (this.options.menus.inputSideToolbar) { - const toolbarSide = this._register(this.instantiationService.createInstance(MenuWorkbenchToolBar, inputAndSideToolbar, this.options.menus.inputSideToolbar, { - telemetrySource: this.options.menus.telemetrySource, - menuOptions: { - shouldForwardArgs: true - } - })); - this.inputSideToolbarContainer = toolbarSide.getElement(); - toolbarSide.getElement().classList.add('chat-side-toolbar'); - toolbarSide.context = { widget } satisfies IChatExecuteActionContext; - } - - let inputModel = this.modelService.getModel(this.inputUri); - if (!inputModel) { - inputModel = this.modelService.createModel('', null, this.inputUri, true); - this._register(inputModel); - } - - this.inputModel = inputModel; - this.inputModel.updateOptions({ bracketColorizationOptions: { enabled: false, independentColorPoolPerBracketType: false } }); - this._inputEditor.setModel(this.inputModel); - if (initialValue) { - this.inputModel.setValue(initialValue); - const lineNumber = this.inputModel.getLineCount(); - this._inputEditor.setPosition({ lineNumber, column: this.inputModel.getLineMaxColumn(lineNumber) }); - } - } - - private initAttachedContext(container: HTMLElement) { - dom.clearNode(container); - this.attachedContextDisposables.clear(); - dom.setVisibility(Boolean(this.attachedContext.size), this.attachedContextContainer); - if (!this.attachedContext.size) { - this._indexOfLastAttachedContextDeletedWithKeyboard = -1; - } - [...this.attachedContext.values()].forEach((attachment, index) => { - const widget = dom.append(container, $('.chat-attached-context-attachment.show-file-icons')); - const label = this._contextResourceLabels.create(widget, { supportIcons: true }); - const file = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined; - const range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined; - if (file && attachment.isFile) { - const fileBasename = basename(file.path); - const fileDirname = dirname(file.path); - const friendlyName = `${fileBasename} ${fileDirname}`; - const ariaLabel = range ? localize('aideChat.fileAttachmentWithRange', "Attached file, {0}, line {1} to line {2}", friendlyName, range.startLineNumber, range.endLineNumber) : localize('aideChat.fileAttachment', "Attached file, {0}", friendlyName); - - label.setFile(file, { - fileKind: FileKind.FILE, - hidePath: true, - range, - }); - widget.ariaLabel = ariaLabel; - widget.tabIndex = 0; - } else { - const attachmentLabel = attachment.fullName ?? attachment.name; - label.setLabel(attachmentLabel, undefined); - - widget.ariaLabel = localize('aideChat.attachment', "Attached context, {0}", attachment.name); - widget.tabIndex = 0; - } - - const clearButton = new Button(widget, { supportIcons: true }); - - // If this item is rendering in place of the last attached context item, focus the clear button so the user can continue deleting attached context items with the keyboard - if (index === Math.min(this._indexOfLastAttachedContextDeletedWithKeyboard, this.attachedContext.size - 1)) { - clearButton.focus(); - } - - this.attachedContextDisposables.add(clearButton); - clearButton.icon = Codicon.close; - const disp = clearButton.onDidClick((e) => { - this.attachedContext.delete(attachment); - disp.dispose(); - - // Set focus to the next attached context item if deletion was triggered by a keystroke (vs a mouse click) - if (dom.isKeyboardEvent(e)) { - const event = new StandardKeyboardEvent(e); - if (event.equals(KeyCode.Enter) || event.equals(KeyCode.Space)) { - this._indexOfLastAttachedContextDeletedWithKeyboard = index; - } - } - - this._onDidChangeHeight.fire(); - this._onDidDeleteContext.fire(attachment); - }); - this.attachedContextDisposables.add(disp); - }); - } - - async renderFollowups(items: IAideChatFollowup[] | undefined, response: IChatResponseViewModel | undefined): Promise { - if (!this.options.renderFollowups) { - return; - } - this.followupsDisposables.clear(); - dom.clearNode(this.followupsContainer); - - if (items && items.length > 0) { - this.followupsDisposables.add(this.instantiationService.createInstance, ChatFollowups>(ChatFollowups, this.followupsContainer, items, this.location, undefined, followup => this._onDidAcceptFollowup.fire({ followup, response }))); - } - this._onDidChangeHeight.fire(); - } - - get contentHeight(): number { - const data = this.getLayoutData(); - return data.followupsHeight + data.inputPartEditorHeight + data.inputPartVerticalPadding + data.inputEditorBorder + data.implicitContextHeight; - } - - layout(height: number, width: number) { - this.cachedDimensions = new dom.Dimension(width, height); - - return this._layout(height, width); - } - - private previousInputEditorDimension: IDimension | undefined; - private _layout(height: number, width: number, allowRecurse = true): void { - this.initAttachedContext(this.attachedContextContainer); - - const data = this.getLayoutData(); - - const inputEditorHeight = Math.min(data.inputPartEditorHeight, height - data.followupsHeight - data.inputPartVerticalPadding); - - const followupsWidth = width - data.inputPartHorizontalPadding; - this.followupsContainer.style.width = `${followupsWidth}px`; - - this._inputPartHeight = data.followupsHeight + inputEditorHeight + data.inputPartVerticalPadding + data.inputEditorBorder + data.implicitContextHeight + data.executeToolbarHeight; - - const initialEditorScrollWidth = this._inputEditor.getScrollWidth(); - const newEditorWidth = width - data.inputPartHorizontalPadding - data.editorBorder - data.editorPadding - data.executeToolbarWidth - data.sideToolbarWidth - data.toolbarPadding; - const newDimension = { width: newEditorWidth, height: inputEditorHeight }; - if (!this.previousInputEditorDimension || (this.previousInputEditorDimension.width !== newDimension.width || this.previousInputEditorDimension.height !== newDimension.height)) { - // This layout call has side-effects that are hard to understand. eg if we are calling this inside a onDidChangeContent handler, this can trigger the next onDidChangeContent handler - // to be invoked, and we have a lot of these on this editor. Only doing a layout this when the editor size has actually changed makes it much easier to follow. - this._inputEditor.layout(newDimension); - this.previousInputEditorDimension = newDimension; - } - - if (allowRecurse && initialEditorScrollWidth < 10) { - // This is probably the initial layout. Now that the editor is layed out with its correct width, it should report the correct contentHeight - return this._layout(height, width, false); - } - } - - private getLayoutData() { - return { - inputEditorBorder: 0, - followupsHeight: this.followupsContainer.offsetHeight, - inputPartEditorHeight: Math.max(this._inputEditor.getContentHeight(), INPUT_EDITOR_MIN_HEIGHT), - inputPartHorizontalPadding: 24, - inputPartVerticalPadding: 46, - implicitContextHeight: this.attachedContextContainer.offsetHeight, - editorBorder: 0, - editorPadding: 12, - toolbarPadding: 4, - executeToolbarHeight: dom.getTotalHeight(this.toolbar.getElement()), - executeToolbarWidth: 0, - sideToolbarWidth: this.inputSideToolbarContainer ? dom.getTotalWidth(this.inputSideToolbarContainer) + 4 /*gap*/ : 0, - }; - } - - saveState(): void { - const inputHistory = [...this.history]; - this.historyService.saveHistory(this.location, inputHistory); - } - - private _renderRequester(requester?: IChatRequester): void { - const username = requester?.username || localize('requester', "You"); - if (!this.requesterContainer) { - return; - } - - this.requesterContainer.querySelector('h3.username')!.textContent = username; - - const avatarContainer = this.requesterContainer.querySelector('.avatar-container')!; - if (requester?.avatarIconUri) { - const avatarImgIcon = $('img.icon'); - avatarImgIcon.src = FileAccess.uriToBrowserUri(requester.avatarIconUri).toString(true); - avatarContainer.replaceChildren($('.avatar', undefined, avatarImgIcon)); - } else { - this.extensionService.getExtension('codestory-ghost.codestoryai').then((ext) => { - if (ext?.extensionLocation) { - const iconUri = URI.joinPath(ext.extensionLocation, 'assets', 'aide-user.png'); - const avatarImgIcon = $('img.icon'); - avatarImgIcon.src = FileAccess.uriToBrowserUri(iconUri).toString(true); - avatarContainer.replaceChildren($('.avatar', undefined, avatarImgIcon)); - } - }).catch(() => { - const defaultIcon = Codicon.account; - const avatarIcon = $(ThemeIcon.asCSSSelector(defaultIcon)); - avatarContainer.replaceChildren($('.avatar.codicon-avatar', undefined, avatarIcon)); - }); - } - } - - private async _renderModelName(): Promise { - if (!this.modelNameContainer) { - return; - } - - const modelSelectionSettings = await this.aiModelSelectionService.getValidatedModelSelectionSettings(); - const modelName = modelSelectionSettings.models[modelSelectionSettings.slowModel].name; - - if (modelName) { - this.modelNameContainer.textContent = modelName; - this.modelNameContainer.style.display = 'block'; - } else { - this.modelNameContainer.style.display = 'none'; - } - } -} - -const historyKeyFn = (entry: IChatHistoryEntry) => JSON.stringify(entry); - -function getLastPosition(model: ITextModel): IPosition { - return { lineNumber: model.getLineCount(), column: model.getLineLength(model.getLineCount()) + 1 }; -} diff --git a/src/vs/workbench/contrib/aideChat/browser/aideChatListRenderer.ts b/src/vs/workbench/contrib/aideChat/browser/aideChatListRenderer.ts deleted file mode 100644 index a5beae27576..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/aideChatListRenderer.ts +++ /dev/null @@ -1,912 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from '../../../../base/browser/dom.js'; -import { renderFormattedText } from '../../../../base/browser/formattedTextRenderer.js'; -import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js'; -import { IActionViewItemOptions } from '../../../../base/browser/ui/actionbar/actionViewItems.js'; -import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; -import { IListVirtualDelegate } from '../../../../base/browser/ui/list/list.js'; -import { ITreeNode, ITreeRenderer } from '../../../../base/browser/ui/tree/tree.js'; -import { IAction } from '../../../../base/common/actions.js'; -import { coalesce, distinct } from '../../../../base/common/arrays.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { FuzzyScore } from '../../../../base/common/filters.js'; -import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js'; -import { KeyCode } from '../../../../base/common/keyCodes.js'; -import { Disposable, DisposableStore, IDisposable, dispose, toDisposable } from '../../../../base/common/lifecycle.js'; -import { ResourceMap } from '../../../../base/common/map.js'; -import { FileAccess } from '../../../../base/common/network.js'; -import { clamp } from '../../../../base/common/numbers.js'; -import { autorun } from '../../../../base/common/observable.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { URI } from '../../../../base/common/uri.js'; -import { MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; -import { localize } from '../../../../nls.js'; -import { IMenuEntryActionViewItemOptions, MenuEntryActionViewItem, createActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; -import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; -import { MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { ColorScheme } from '../../../../platform/theme/common/theme.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { ChatTreeItem, GeneratingPhrase, IChatCodeBlockInfo, IChatFileTreeInfo } from '../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { ChatAgentHover, getChatAgentHoverOptions } from '../../../../workbench/contrib/aideChat/browser/aideChatAgentHover.js'; -import { ChatCommandButtonContentPart } from '../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatCommandContentPart.js'; -import { ChatConfirmationContentPart } from '../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatConfirmationContentPart.js'; -import { IChatContentPart, IChatContentPartRenderContext } from '../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatContentParts.js'; -import { ChatMarkdownContentPart, EditorPool } from '../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatMarkdownContentPart.js'; -import { ChatProgressContentPart } from '../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatProgressContentPart.js'; -import { ChatReferencesContentPart, ContentReferencesListPool } from '../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatReferencesContentPart.js'; -import { ChatTaskContentPart } from '../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatTaskContentPart.js'; -import { ChatTextEditContentPart, DiffEditorPool } from '../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatTextEditContentPart.js'; -import { ChatTreeContentPart, TreePool } from '../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatTreeContentPart.js'; -import { ChatWarningContentPart } from '../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatWarningContentPart.js'; -import { ChatFollowups } from '../../../../workbench/contrib/aideChat/browser/aideChatFollowups.js'; -import { ChatMarkdownDecorationsRenderer } from '../../../../workbench/contrib/aideChat/browser/aideChatMarkdownDecorationsRenderer.js'; -import { ChatMarkdownRenderer } from '../../../../workbench/contrib/aideChat/browser/aideChatMarkdownRenderer.js'; -import { ChatEditorOptions } from '../../../../workbench/contrib/aideChat/browser/aideChatOptions.js'; -import { ChatCodeBlockContentProvider } from '../../../../workbench/contrib/aideChat/browser/codeBlockPart.js'; -import { AideChatAgentLocation, IAideChatAgentMetadata } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING, CONTEXT_REQUEST, CONTEXT_RESPONSE, CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND, CONTEXT_RESPONSE_FILTERED, CONTEXT_RESPONSE_VOTE } from '../../../../workbench/contrib/aideChat/common/aideChatContextKeys.js'; -import { IChatTextEditGroup } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { chatSubcommandLeader } from '../../../../workbench/contrib/aideChat/common/aideChatParserTypes.js'; -import { AideChatAgentVoteDirection, IAideChatConfirmation, IAideChatFollowup, IAideChatTask, IChatTreeData } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { IChatReferences, IChatRendererContent, IChatResponseViewModel, IChatWelcomeMessageViewModel, isRequestVM, isResponseVM, isWelcomeVM } from '../../../../workbench/contrib/aideChat/common/aideChatViewModel.js'; -import { getNWords } from '../../../../workbench/contrib/aideChat/common/aideChatWordCounter.js'; -import { annotateSpecialMarkdownContent } from '../common/annotations.js'; -import { CodeBlockModelCollection } from '../common/codeBlockModelCollection.js'; -import { IChatListItemRendererOptions } from './aideChat.js'; - -const $ = dom.$; - -interface IChatListItemTemplate { - currentElement?: ChatTreeItem; - renderedParts?: IChatContentPart[]; - readonly rowContainer: HTMLElement; - readonly titleToolbar?: MenuWorkbenchToolBar; - readonly avatarContainer: HTMLElement; - readonly username: HTMLElement; - readonly detail: HTMLElement; - readonly value: HTMLElement; - readonly contextKeyService: IContextKeyService; - readonly instantiationService: IInstantiationService; - readonly templateDisposables: IDisposable; - readonly elementDisposables: DisposableStore; - readonly agentHover: ChatAgentHover; -} - -interface IItemHeightChangeParams { - element: ChatTreeItem; - height: number; -} - -const forceVerboseLayoutTracing = false - // || Boolean("TRUE") // causes a linter warning so that it cannot be pushed - ; - -export interface IChatRendererDelegate { - getListLength(): number; - - readonly onDidScroll?: Event; -} - -export class ChatListItemRenderer extends Disposable implements ITreeRenderer { - static readonly ID = 'item'; - - private readonly codeBlocksByResponseId = new Map(); - private readonly codeBlocksByEditorUri = new ResourceMap(); - - private readonly fileTreesByResponseId = new Map(); - private readonly focusedFileTreesByResponseId = new Map(); - - private readonly renderer: MarkdownRenderer; - private readonly markdownDecorationsRenderer: ChatMarkdownDecorationsRenderer; - - protected readonly _onDidClickFollowup = this._register(new Emitter()); - readonly onDidClickFollowup: Event = this._onDidClickFollowup.event; - - private readonly _onDidClickRerunWithAgentOrCommandDetection = new Emitter(); - readonly onDidClickRerunWithAgentOrCommandDetection: Event = this._onDidClickRerunWithAgentOrCommandDetection.event; - - protected readonly _onDidChangeItemHeight = this._register(new Emitter()); - readonly onDidChangeItemHeight: Event = this._onDidChangeItemHeight.event; - - private readonly _editorPool: EditorPool; - private readonly _diffEditorPool: DiffEditorPool; - private readonly _treePool: TreePool; - private readonly _contentReferencesListPool: ContentReferencesListPool; - - private _currentLayoutWidth: number = 0; - private _isVisible = true; - private _onDidChangeVisibility = this._register(new Emitter()); - - constructor( - editorOptions: ChatEditorOptions, - private readonly location: AideChatAgentLocation, - private readonly rendererOptions: IChatListItemRendererOptions, - private readonly delegate: IChatRendererDelegate, - private readonly codeBlockModelCollection: CodeBlockModelCollection, - overflowWidgetsDomNode: HTMLElement | undefined, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IConfigurationService configService: IConfigurationService, - @ILogService private readonly logService: ILogService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IThemeService private readonly themeService: IThemeService, - @ICommandService private readonly commandService: ICommandService, - @IHoverService private readonly hoverService: IHoverService, - ) { - super(); - - this.renderer = this._register(this.instantiationService.createInstance(ChatMarkdownRenderer, undefined)); - this.markdownDecorationsRenderer = this.instantiationService.createInstance(ChatMarkdownDecorationsRenderer); - this._editorPool = this._register(this.instantiationService.createInstance(EditorPool, editorOptions, delegate, overflowWidgetsDomNode)); - this._diffEditorPool = this._register(this.instantiationService.createInstance(DiffEditorPool, editorOptions, delegate, overflowWidgetsDomNode)); - this._treePool = this._register(this.instantiationService.createInstance(TreePool, this._onDidChangeVisibility.event)); - this._contentReferencesListPool = this._register(this.instantiationService.createInstance(ContentReferencesListPool, this._onDidChangeVisibility.event)); - - this._register(this.instantiationService.createInstance(ChatCodeBlockContentProvider)); - } - - get templateId(): string { - return ChatListItemRenderer.ID; - } - - editorsInUse() { - return this._editorPool.inUse(); - } - - private traceLayout(method: string, message: string) { - if (forceVerboseLayoutTracing) { - this.logService.info(`ChatListItemRenderer#${method}: ${message}`); - } else { - this.logService.trace(`ChatListItemRenderer#${method}: ${message}`); - } - } - - private getProgressiveRenderRate(element: IChatResponseViewModel): number { - if (element.isComplete) { - return 80; - } - - if (element.contentUpdateTimings && element.contentUpdateTimings.impliedWordLoadRate) { - // words/s - const minRate = 12; - const maxRate = 80; - - // This doesn't account for dead time after the last update. When the previous update is the final one and the model is only waiting for followupQuestions, that's good. - // When there was one quick update and then you are waiting longer for the next one, that's not good since the rate should be decreasing. - // If it's an issue, we can change this to be based on the total time from now to the beginning. - const rateBoost = 1.5; - const rate = element.contentUpdateTimings.impliedWordLoadRate * rateBoost; - return clamp(rate, minRate, maxRate); - } - - return 8; - } - - getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[] { - const codeBlocks = this.codeBlocksByResponseId.get(response.id); - return codeBlocks ?? []; - } - - getCodeBlockInfoForEditor(uri: URI): IChatCodeBlockInfo | undefined { - return this.codeBlocksByEditorUri.get(uri); - } - - getFileTreeInfosForResponse(response: IChatResponseViewModel): IChatFileTreeInfo[] { - const fileTrees = this.fileTreesByResponseId.get(response.id); - return fileTrees ?? []; - } - - getLastFocusedFileTreeForResponse(response: IChatResponseViewModel): IChatFileTreeInfo | undefined { - const fileTrees = this.fileTreesByResponseId.get(response.id); - const lastFocusedFileTreeIndex = this.focusedFileTreesByResponseId.get(response.id); - if (fileTrees?.length && lastFocusedFileTreeIndex !== undefined && lastFocusedFileTreeIndex < fileTrees.length) { - return fileTrees[lastFocusedFileTreeIndex]; - } - return undefined; - } - - setVisible(visible: boolean): void { - this._isVisible = visible; - this._onDidChangeVisibility.fire(visible); - } - - layout(width: number): void { - this._currentLayoutWidth = width - (this.rendererOptions.noPadding ? 0 : 40); // padding - for (const editor of this._editorPool.inUse()) { - editor.layout(this._currentLayoutWidth); - } - for (const diffEditor of this._diffEditorPool.inUse()) { - diffEditor.layout(this._currentLayoutWidth); - } - } - - renderTemplate(container: HTMLElement): IChatListItemTemplate { - const templateDisposables = new DisposableStore(); - const rowContainer = dom.append(container, $('.cschat-item-container')); - if (this.rendererOptions.renderStyle === 'compact') { - rowContainer.classList.add('interactive-item-compact'); - } - if (this.rendererOptions.noPadding) { - rowContainer.classList.add('no-padding'); - } - - let headerParent = rowContainer; - let valueParent = rowContainer; - let detailContainerParent: HTMLElement | undefined; - let toolbarParent: HTMLElement | undefined; - - if (this.rendererOptions.renderStyle === 'minimal') { - rowContainer.classList.add('interactive-item-compact'); - rowContainer.classList.add('minimal'); - // ----------------------------------------------------- - // icon | details - // | references - // | value - // ----------------------------------------------------- - const lhsContainer = dom.append(rowContainer, $('.column.left')); - const rhsContainer = dom.append(rowContainer, $('.column')); - - headerParent = lhsContainer; - detailContainerParent = rhsContainer; - valueParent = rhsContainer; - toolbarParent = dom.append(rowContainer, $('.header')); - } - - const header = dom.append(headerParent, $('.header')); - const user = dom.append(header, $('.user')); - user.tabIndex = 0; - user.role = 'toolbar'; - const avatarContainer = dom.append(user, $('.avatar-container')); - const username = dom.append(user, $('h3.username')); - const detailContainer = dom.append(detailContainerParent ?? user, $('span.detail-container')); - const detail = dom.append(detailContainer, $('span.detail')); - dom.append(detailContainer, $('span.chat-animated-ellipsis')); - const value = dom.append(valueParent, $('.value')); - const elementDisposables = new DisposableStore(); - - const contextKeyService = templateDisposables.add(this.contextKeyService.createScoped(rowContainer)); - const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, contextKeyService])); - let titleToolbar: MenuWorkbenchToolBar | undefined; - if (this.rendererOptions.noHeader) { - header.classList.add('hidden'); - } else { - titleToolbar = templateDisposables.add(scopedInstantiationService.createInstance(MenuWorkbenchToolBar, toolbarParent ?? header, MenuId.AideChatMessageTitle, { - menuOptions: { - shouldForwardArgs: true - }, - toolbarOptions: { - shouldInlineSubmenu: submenu => submenu.actions.length <= 1 - }, - actionViewItemProvider: (action: IAction, options: IActionViewItemOptions) => { - if (action instanceof MenuItemAction && (action.item.id === 'workbench.action.aideChat.voteDown' || action.item.id === 'workbench.action.aideChat.voteUp')) { - return scopedInstantiationService.createInstance(ChatVoteButton, action, options as IMenuEntryActionViewItemOptions); - } - return createActionViewItem(scopedInstantiationService, action, options); - } - })); - } - - const agentHover = templateDisposables.add(this.instantiationService.createInstance(ChatAgentHover)); - const hoverContent = () => { - if (isResponseVM(template.currentElement) && template.currentElement.agent && !template.currentElement.agent.isDefault) { - agentHover.setAgent(template.currentElement.agent.id); - return agentHover.domNode; - } - - return undefined; - }; - const hoverOptions = getChatAgentHoverOptions(() => isResponseVM(template.currentElement) ? template.currentElement.agent : undefined, this.commandService); - templateDisposables.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), user, hoverContent, hoverOptions)); - templateDisposables.add(dom.addDisposableListener(user, dom.EventType.KEY_DOWN, e => { - const ev = new StandardKeyboardEvent(e); - if (ev.equals(KeyCode.Space) || ev.equals(KeyCode.Enter)) { - const content = hoverContent(); - if (content) { - this.hoverService.showHover({ content, target: user, trapFocus: true, actions: hoverOptions.actions }, true); - } - } else if (ev.equals(KeyCode.Escape)) { - this.hoverService.hideHover(); - } - })); - const template: IChatListItemTemplate = { avatarContainer, username, detail, value, rowContainer, elementDisposables, titleToolbar, templateDisposables, contextKeyService, instantiationService: scopedInstantiationService, agentHover }; - return template; - } - - renderElement(node: ITreeNode, index: number, templateData: IChatListItemTemplate): void { - this.renderChatTreeItem(node.element, index, templateData); - } - - renderChatTreeItem(element: ChatTreeItem, index: number, templateData: IChatListItemTemplate): void { - templateData.currentElement = element; - const kind = isRequestVM(element) ? 'request' : - isResponseVM(element) ? 'response' : - 'welcome'; - this.traceLayout('renderElement', `${kind}, index=${index}`); - - CONTEXT_RESPONSE.bindTo(templateData.contextKeyService).set(isResponseVM(element)); - CONTEXT_REQUEST.bindTo(templateData.contextKeyService).set(isRequestVM(element)); - CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND.bindTo(templateData.contextKeyService).set(isResponseVM(element) && element.agentOrSlashCommandDetected); - if (isResponseVM(element)) { - CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING.bindTo(templateData.contextKeyService).set(!!element.agent?.metadata.supportIssueReporting); - CONTEXT_RESPONSE_VOTE.bindTo(templateData.contextKeyService).set(element.vote === AideChatAgentVoteDirection.Up ? 'up' : element.vote === AideChatAgentVoteDirection.Down ? 'down' : ''); - } else { - CONTEXT_RESPONSE_VOTE.bindTo(templateData.contextKeyService).set(''); - } - - if (templateData.titleToolbar) { - templateData.titleToolbar.context = element; - } - - const isFiltered = !!(isResponseVM(element) && element.errorDetails?.responseIsFiltered); - CONTEXT_RESPONSE_FILTERED.bindTo(templateData.contextKeyService).set(isFiltered); - - templateData.rowContainer.classList.toggle('interactive-request', isRequestVM(element)); - templateData.rowContainer.classList.toggle('interactive-response', isResponseVM(element)); - templateData.rowContainer.classList.toggle('interactive-welcome', isWelcomeVM(element)); - templateData.rowContainer.classList.toggle('filtered-response', isFiltered); - templateData.rowContainer.classList.toggle('show-detail-progress', isResponseVM(element) && !element.isComplete && !element.progressMessages.length); - templateData.username.textContent = element.username; - if (!this.rendererOptions.noHeader) { - this.renderAvatar(element, templateData); - } - - dom.clearNode(templateData.detail); - if (isResponseVM(element)) { - this.renderDetail(element, templateData); - } - - // Do a progressive render if - // - This the last response in the list - // - And it has some content - // - And the response is not complete - // - Or, we previously started a progressive rendering of this element (if the element is complete, we will finish progressive rendering with a very fast rate) - if (isResponseVM(element) && index === this.delegate.getListLength() - 1 && (!element.isComplete || element.renderData) && element.response.value.length) { - this.traceLayout('renderElement', `start progressive render ${kind}, index=${index}`); - - const timer = templateData.elementDisposables.add(new dom.WindowIntervalTimer()); - const runProgressiveRender = (initial?: boolean) => { - try { - if (this.doNextProgressiveRender(element, index, templateData, !!initial)) { - timer.cancel(); - } - } catch (err) { - // Kill the timer if anything went wrong, avoid getting stuck in a nasty rendering loop. - timer.cancel(); - this.logService.error(err); - } - }; - timer.cancelAndSet(runProgressiveRender, 50, dom.getWindow(templateData.rowContainer)); - runProgressiveRender(true); - } else if (isResponseVM(element)) { - this.basicRenderElement(element, index, templateData); - } else if (isRequestVM(element)) { - this.basicRenderElement(element, index, templateData); - } else { - this.renderWelcomeMessage(element, templateData); - } - } - - private renderDetail(element: IChatResponseViewModel, templateData: IChatListItemTemplate): void { - templateData.elementDisposables.add(autorun(reader => { - this._renderDetail(element, templateData); - })); - } - - private _renderDetail(element: IChatResponseViewModel, templateData: IChatListItemTemplate): void { - - dom.clearNode(templateData.detail); - - if (element.slashCommand && element.agentOrSlashCommandDetected) { - const usingMsg = `${chatSubcommandLeader}${element.slashCommand.name}`; - const msg = localize('usedAgent', "used {0} [[(rerun without)]]", usingMsg); - - dom.reset(templateData.detail, renderFormattedText(msg, { - className: 'agentOrSlashCommandDetected', - inline: true, - actionHandler: { - disposables: templateData.elementDisposables, - callback: (content) => { - this._onDidClickRerunWithAgentOrCommandDetection.fire(element); - }, - } - })); - - } else if (!element.isComplete) { - templateData.detail.textContent = GeneratingPhrase; - } - } - - private renderAvatar(element: ChatTreeItem, templateData: IChatListItemTemplate): void { - const icon = isResponseVM(element) ? - this.getAgentIcon(element.agent?.metadata) : - (element.avatarIcon ?? Codicon.account); - if (icon instanceof URI) { - const avatarIcon = dom.$('img.icon'); - avatarIcon.src = FileAccess.uriToBrowserUri(icon).toString(true); - templateData.avatarContainer.replaceChildren(dom.$('.avatar', undefined, avatarIcon)); - } else { - const avatarIcon = dom.$(ThemeIcon.asCSSSelector(icon)); - templateData.avatarContainer.replaceChildren(dom.$('.avatar.codicon-avatar', undefined, avatarIcon)); - } - } - - private getAgentIcon(agent: IAideChatAgentMetadata | undefined): URI | ThemeIcon { - if (agent?.themeIcon) { - return agent.themeIcon; - } else if (agent?.iconDark && this.themeService.getColorTheme().type === ColorScheme.DARK) { - return agent.iconDark; - } else if (agent?.icon) { - return agent.icon; - } else { - return Codicon.copilot; - } - } - - private basicRenderElement(element: ChatTreeItem, index: number, templateData: IChatListItemTemplate) { - let value: IChatRendererContent[] = []; - if (isRequestVM(element)) { - const markdown = 'message' in element.message ? - element.message.message : - this.markdownDecorationsRenderer.convertParsedRequestToMarkdown(element.message); - value = [{ content: new MarkdownString(markdown), kind: 'markdownContent' }]; - } else if (isResponseVM(element)) { - value = annotateSpecialMarkdownContent(element.response.value); - if (element.contentReferences.length) { - value.unshift({ kind: 'references', references: element.contentReferences }); - } - } - - dom.clearNode(templateData.value); - - if (isResponseVM(element)) { - this.renderDetail(element, templateData); - } - - const parts: IChatContentPart[] = []; - value.forEach((data, index) => { - const context: IChatContentPartRenderContext = { - element, - index, - content: value, - preceedingContentParts: parts, - }; - const newPart = this.renderChatContentPart(data, templateData, context); - if (newPart) { - templateData.value.appendChild(newPart.domNode); - parts.push(newPart); - } - }); - templateData.renderedParts = parts; - - if (isResponseVM(element) && element.errorDetails?.message) { - const renderedError = this.instantiationService.createInstance(ChatWarningContentPart, element.errorDetails.responseIsFiltered ? 'info' : 'error', new MarkdownString(element.errorDetails.message), this.renderer); - templateData.elementDisposables.add(renderedError); - templateData.value.appendChild(renderedError.domNode); - } - - const newHeight = templateData.rowContainer.offsetHeight; - const fireEvent = !element.currentRenderedHeight || element.currentRenderedHeight !== newHeight; - element.currentRenderedHeight = newHeight; - if (fireEvent) { - const disposable = templateData.elementDisposables.add(dom.scheduleAtNextAnimationFrame(dom.getWindow(templateData.value), () => { - // Have to recompute the height here because codeblock rendering is currently async and it may have changed. - // If it becomes properly sync, then this could be removed. - element.currentRenderedHeight = templateData.rowContainer.offsetHeight; - disposable.dispose(); - this._onDidChangeItemHeight.fire({ element, height: element.currentRenderedHeight }); - })); - } - } - - private updateItemHeight(templateData: IChatListItemTemplate): void { - if (!templateData.currentElement) { - return; - } - - const newHeight = templateData.rowContainer.offsetHeight; - templateData.currentElement.currentRenderedHeight = newHeight; - this._onDidChangeItemHeight.fire({ element: templateData.currentElement, height: newHeight }); - } - - private renderWelcomeMessage(element: IChatWelcomeMessageViewModel, templateData: IChatListItemTemplate) { - dom.clearNode(templateData.value); - - element.content.forEach((item, i) => { - if (Array.isArray(item)) { - const scopedInstaService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, templateData.contextKeyService])); - templateData.elementDisposables.add( - scopedInstaService.createInstance, ChatFollowups>( - ChatFollowups, - templateData.value, - item, - this.location, - undefined, - followup => this._onDidClickFollowup.fire(followup))); - } else { - const context: IChatContentPartRenderContext = { - element, - index: i, - // NA for welcome msg - content: [], - preceedingContentParts: [] - }; - const result = this.renderMarkdown(item, templateData, context); - templateData.value.appendChild(result.domNode); - templateData.elementDisposables.add(result); - } - }); - - const newHeight = templateData.rowContainer.offsetHeight; - const fireEvent = !element.currentRenderedHeight || element.currentRenderedHeight !== newHeight; - element.currentRenderedHeight = newHeight; - if (fireEvent) { - const disposable = templateData.elementDisposables.add(dom.scheduleAtNextAnimationFrame(dom.getWindow(templateData.value), () => { - // Have to recompute the height here because codeblock rendering is currently async and it may have changed. - // If it becomes properly sync, then this could be removed. - element.currentRenderedHeight = templateData.rowContainer.offsetHeight; - disposable.dispose(); - this._onDidChangeItemHeight.fire({ element, height: element.currentRenderedHeight }); - })); - } - } - - /** - * @returns true if progressive rendering should be considered complete- the element's data is fully rendered or the view is not visible - */ - private doNextProgressiveRender(element: IChatResponseViewModel, index: number, templateData: IChatListItemTemplate, isInRenderElement: boolean): boolean { - if (!this._isVisible) { - return true; - } - - let isFullyRendered = false; - if (element.isCanceled) { - this.traceLayout('doNextProgressiveRender', `canceled, index=${index}`); - element.renderData = undefined; - this.basicRenderElement(element, index, templateData); - isFullyRendered = true; - // TODO Maybe return here, shouldn't need to fire onDidChangeItemHeight here - } else { - this.traceLayout('doNextProgressiveRender', `START progressive render, index=${index}, renderData=${JSON.stringify(element.renderData)}`); - const contentForThisTurn = this.getNextProgressiveRenderContent(element); - const partsToRender = this.diff(templateData.renderedParts ?? [], contentForThisTurn, element); - isFullyRendered = partsToRender.every(part => part === null); - - if (isFullyRendered && element.isComplete) { - // Response is done and content is rendered, so do a normal render - this.traceLayout('doNextProgressiveRender', `END progressive render, index=${index} and clearing renderData, response is complete`); - element.renderData = undefined; - this.basicRenderElement(element, index, templateData); - // TODO return here - } else if (!isFullyRendered) { - this.traceLayout('doNextProgressiveRender', `doing progressive render, ${partsToRender.length} parts to render`); - this.renderChatContentDiff(partsToRender, contentForThisTurn, element, templateData); - } else { - // Nothing new to render, not done, keep waiting - return false; - } - } - - // Some render happened - update the height - const height = templateData.rowContainer.offsetHeight; - element.currentRenderedHeight = height; - if (!isInRenderElement) { - this._onDidChangeItemHeight.fire({ element, height: templateData.rowContainer.offsetHeight }); - } - - return isFullyRendered; - } - - private renderChatContentDiff(partsToRender: ReadonlyArray, contentForThisTurn: ReadonlyArray, element: IChatResponseViewModel, templateData: IChatListItemTemplate): void { - const renderedParts = templateData.renderedParts ?? []; - templateData.renderedParts = renderedParts; - partsToRender.forEach((partToRender, index) => { - if (!partToRender) { - // null=no change - return; - } - - const alreadyRenderedPart = templateData.renderedParts?.[index]; - if (alreadyRenderedPart) { - alreadyRenderedPart.dispose(); - } - - const preceedingContentParts = renderedParts.slice(0, index); - const context: IChatContentPartRenderContext = { - element, - content: contentForThisTurn, - preceedingContentParts, - index - }; - const newPart = this.renderChatContentPart(partToRender, templateData, context); - if (newPart) { - // Maybe the part can't be rendered in this context, but this shouldn't really happen - if (alreadyRenderedPart) { - try { - // This method can throw HierarchyRequestError - alreadyRenderedPart.domNode.replaceWith(newPart.domNode); - } catch (err) { - this.logService.error('ChatListItemRenderer#renderChatContentDiff: error replacing part', err); - } - } else { - templateData.value.appendChild(newPart.domNode); - } - - renderedParts[index] = newPart; - } else if (alreadyRenderedPart) { - alreadyRenderedPart.domNode.remove(); - } - }); - } - - /** - * Returns all content parts that should be rendered, and trimmed markdown content. We will diff this with the current rendered set. - */ - private getNextProgressiveRenderContent(element: IChatResponseViewModel): IChatRendererContent[] { - const data = this.getDataForProgressiveRender(element); - - const renderableResponse = annotateSpecialMarkdownContent(element.response.value); - - this.traceLayout('getNextProgressiveRenderContent', `Want to render ${data.numWordsToRender}, counting...`); - let numNeededWords = data.numWordsToRender; - const partsToRender: IChatRendererContent[] = []; - if (element.contentReferences.length) { - partsToRender.push({ kind: 'references', references: element.contentReferences }); - } - - for (const part of renderableResponse) { - if (numNeededWords <= 0) { - break; - } - - if (part.kind === 'markdownContent') { - const wordCountResult = getNWords(part.content.value, numNeededWords); - if (wordCountResult.isFullString) { - partsToRender.push(part); - } else { - partsToRender.push({ kind: 'markdownContent', content: new MarkdownString(wordCountResult.value, part.content) }); - } - - this.traceLayout('getNextProgressiveRenderContent', ` Want to render ${numNeededWords} words and found ${wordCountResult.returnedWordCount} words. Total words in chunk: ${wordCountResult.totalWordCount}`); - numNeededWords -= wordCountResult.returnedWordCount; - } else { - partsToRender.push(part); - } - } - - this.traceLayout('getNextProgressiveRenderContent', `Want to render ${data.numWordsToRender} words and ${data.numWordsToRender - numNeededWords} words available`); - const newRenderedWordCount = data.numWordsToRender - numNeededWords; - if (newRenderedWordCount !== element.renderData?.renderedWordCount) { - // Only update lastRenderTime when we actually render new content - element.renderData = { lastRenderTime: Date.now(), renderedWordCount: newRenderedWordCount, renderedParts: partsToRender }; - } - - return partsToRender; - } - - private getDataForProgressiveRender(element: IChatResponseViewModel) { - const renderData = element.renderData ?? { lastRenderTime: 0, renderedWordCount: 0 }; - - const rate = this.getProgressiveRenderRate(element); - const numWordsToRender = renderData.lastRenderTime === 0 ? - 1 : - renderData.renderedWordCount + - // Additional words to render beyond what's already rendered - Math.floor((Date.now() - renderData.lastRenderTime) / 1000 * rate); - - return { - numWordsToRender, - rate - }; - } - - private diff(renderedParts: ReadonlyArray, contentToRender: ReadonlyArray, element: ChatTreeItem): ReadonlyArray { - const diff: (IChatRendererContent | null)[] = []; - for (let i = 0; i < contentToRender.length; i++) { - const content = contentToRender[i]; - const renderedPart = renderedParts[i]; - - if (!renderedPart || !renderedPart.hasSameContent(content, contentToRender.slice(i + 1), element)) { - diff.push(content); - } else { - // null -> no change - diff.push(null); - } - } - - return diff; - } - - private renderChatContentPart(content: IChatRendererContent, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart | undefined { - if (content.kind === 'treeData') { - return this.renderTreeData(content, templateData, context); - } else if (content.kind === 'progressMessage') { - return this.instantiationService.createInstance(ChatProgressContentPart, content, this.renderer, context); - } else if (content.kind === 'progressTask') { - return this.renderProgressTask(content, templateData, context); - } else if (content.kind === 'command') { - return this.instantiationService.createInstance(ChatCommandButtonContentPart, content, context); - } else if (content.kind === 'textEditGroup') { - return this.renderTextEdit(context, content, templateData); - } else if (content.kind === 'confirmation') { - return this.renderConfirmation(context, content, templateData); - } else if (content.kind === 'warning') { - return this.instantiationService.createInstance(ChatWarningContentPart, 'warning', content.content, this.renderer); - } else if (content.kind === 'markdownContent') { - return this.renderMarkdown(content.content, templateData, context); - } else if (content.kind === 'references') { - return this.renderContentReferencesListData(content, undefined, context, templateData); - } - - return undefined; - } - - private renderTreeData(content: IChatTreeData, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart { - const data = content.treeData; - const treeDataIndex = context.preceedingContentParts.filter(part => part instanceof ChatTreeContentPart).length; - const treePart = this.instantiationService.createInstance(ChatTreeContentPart, data, context.element, this._treePool, treeDataIndex); - - treePart.addDisposable(treePart.onDidChangeHeight(() => { - this.updateItemHeight(templateData); - })); - - if (isResponseVM(context.element)) { - const fileTreeFocusInfo = { - treeDataId: data.uri.toString(), - treeIndex: treeDataIndex, - focus() { - treePart.domFocus(); - } - }; - - // TODO@roblourens there's got to be a better way to navigate trees - treePart.addDisposable(treePart.onDidFocus(() => { - this.focusedFileTreesByResponseId.set(context.element.id, fileTreeFocusInfo.treeIndex); - })); - - const fileTrees = this.fileTreesByResponseId.get(context.element.id) ?? []; - fileTrees.push(fileTreeFocusInfo); - this.fileTreesByResponseId.set(context.element.id, distinct(fileTrees, (v) => v.treeDataId)); - treePart.addDisposable(toDisposable(() => this.fileTreesByResponseId.set(context.element.id, fileTrees.filter(v => v.treeDataId !== data.uri.toString())))); - } - - return treePart; - } - - private renderContentReferencesListData(references: IChatReferences, labelOverride: string | undefined, context: IChatContentPartRenderContext, templateData: IChatListItemTemplate): ChatReferencesContentPart { - const referencesPart = this.instantiationService.createInstance(ChatReferencesContentPart, references.references, labelOverride, context.element as IChatResponseViewModel, this._contentReferencesListPool); - referencesPart.addDisposable(referencesPart.onDidChangeHeight(() => { - this.updateItemHeight(templateData); - })); - - return referencesPart; - } - - private renderProgressTask(task: IAideChatTask, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart | undefined { - if (!isResponseVM(context.element)) { - return; - } - - const taskPart = this.instantiationService.createInstance(ChatTaskContentPart, task, this._contentReferencesListPool, this.renderer, context); - taskPart.addDisposable(taskPart.onDidChangeHeight(() => { - this.updateItemHeight(templateData); - })); - return taskPart; - } - - private renderConfirmation(context: IChatContentPartRenderContext, confirmation: IAideChatConfirmation, templateData: IChatListItemTemplate): IChatContentPart { - const part = this.instantiationService.createInstance(ChatConfirmationContentPart, confirmation, context); - part.addDisposable(part.onDidChangeHeight(() => this.updateItemHeight(templateData))); - return part; - } - - private renderTextEdit(context: IChatContentPartRenderContext, chatTextEdit: IChatTextEditGroup, templateData: IChatListItemTemplate): IChatContentPart { - const textEditPart = this.instantiationService.createInstance(ChatTextEditContentPart, chatTextEdit, context, this.rendererOptions, this._diffEditorPool, this._currentLayoutWidth); - textEditPart.addDisposable(textEditPart.onDidChangeHeight(() => { - textEditPart.layout(this._currentLayoutWidth); - this.updateItemHeight(templateData); - })); - - return textEditPart; - } - - private renderMarkdown(markdown: IMarkdownString, templateData: IChatListItemTemplate, context: IChatContentPartRenderContext): IChatContentPart { - const element = context.element; - const fillInIncompleteTokens = isResponseVM(element) && (!element.isComplete || element.isCanceled || element.errorDetails?.responseIsFiltered || element.errorDetails?.responseIsIncomplete); - const codeBlockStartIndex = context.preceedingContentParts.reduce((acc, part) => acc + (part instanceof ChatMarkdownContentPart ? part.codeblocks.length : 0), 0); - const markdownPart = this.instantiationService.createInstance(ChatMarkdownContentPart, markdown, context, this._editorPool, fillInIncompleteTokens, codeBlockStartIndex, this.renderer, this._currentLayoutWidth, this.codeBlockModelCollection, this.rendererOptions); - markdownPart.addDisposable(markdownPart.onDidChangeHeight(() => { - markdownPart.layout(this._currentLayoutWidth); - this.updateItemHeight(templateData); - })); - - const codeBlocksByResponseId = this.codeBlocksByResponseId.get(element.id) ?? []; - this.codeBlocksByResponseId.set(element.id, codeBlocksByResponseId); - markdownPart.addDisposable(toDisposable(() => { - const codeBlocksByResponseId = this.codeBlocksByResponseId.get(element.id); - if (codeBlocksByResponseId) { - markdownPart.codeblocks.forEach((info, i) => delete codeBlocksByResponseId[codeBlockStartIndex + i]); - } - })); - - markdownPart.codeblocks.forEach((info, i) => { - codeBlocksByResponseId[codeBlockStartIndex + i] = info; - if (info.uri) { - const uri = info.uri; - this.codeBlocksByEditorUri.set(uri, info); - markdownPart.addDisposable(toDisposable(() => this.codeBlocksByEditorUri.delete(uri))); - } - }); - - return markdownPart; - } - - disposeElement(node: ITreeNode, index: number, templateData: IChatListItemTemplate): void { - this.traceLayout('disposeElement', `Disposing element, index=${index}`); - - // We could actually reuse a template across a renderElement call? - if (templateData.renderedParts) { - try { - dispose(coalesce(templateData.renderedParts)); - templateData.renderedParts = undefined; - dom.clearNode(templateData.value); - } catch (err) { - throw err; - } - } - - templateData.currentElement = undefined; - templateData.elementDisposables.clear(); - } - - disposeTemplate(templateData: IChatListItemTemplate): void { - templateData.templateDisposables.dispose(); - } -} - -export class ChatListDelegate implements IListVirtualDelegate { - constructor( - private readonly defaultElementHeight: number, - @ILogService private readonly logService: ILogService - ) { } - - private _traceLayout(method: string, message: string) { - if (forceVerboseLayoutTracing) { - this.logService.info(`ChatListDelegate#${method}: ${message}`); - } else { - this.logService.trace(`ChatListDelegate#${method}: ${message}`); - } - } - - getHeight(element: ChatTreeItem): number { - const kind = isRequestVM(element) ? 'request' : 'response'; - const height = ('currentRenderedHeight' in element ? element.currentRenderedHeight : undefined) ?? this.defaultElementHeight; - this._traceLayout('getHeight', `${kind}, height=${height}`); - return height; - } - - getTemplateId(element: ChatTreeItem): string { - return ChatListItemRenderer.ID; - } - - hasDynamicHeight(element: ChatTreeItem): boolean { - return true; - } -} - -class ChatVoteButton extends MenuEntryActionViewItem { - override render(container: HTMLElement): void { - super.render(container); - container.classList.toggle('checked', this.action.checked); - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/aideChatMarkdownDecorationsRenderer.ts b/src/vs/workbench/contrib/aideChat/browser/aideChatMarkdownDecorationsRenderer.ts deleted file mode 100644 index cdce4497ccf..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/aideChatMarkdownDecorationsRenderer.ts +++ /dev/null @@ -1,259 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from '../../../../base/browser/dom.js'; -import { Button } from '../../../../base/browser/ui/button/button.js'; -import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; -import { toErrorMessage } from '../../../../base/common/errorMessage.js'; -import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; -import { revive } from '../../../../base/common/marshalling.js'; -import { URI } from '../../../../base/common/uri.js'; -import { Location } from '../../../../editor/common/languages.js'; -import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; -import { ILabelService } from '../../../../platform/label/common/label.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { asCssVariable } from '../../../../platform/theme/common/colorUtils.js'; -import { IAideChatWidgetService } from '../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { ChatAgentHover, getChatAgentHoverOptions } from '../../../../workbench/contrib/aideChat/browser/aideChatAgentHover.js'; -import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IAideChatAgentNameService, IAideChatAgentService } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { chatSlashCommandBackground, chatSlashCommandForeground } from '../../../../workbench/contrib/aideChat/common/aideChatColors.js'; -import { chatAgentLeader, ChatRequestAgentPart, ChatRequestDynamicVariablePart, ChatRequestTextPart, chatSubcommandLeader, IParsedChatRequest } from '../../../../workbench/contrib/aideChat/common/aideChatParserTypes.js'; -import { IAideChatService } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { contentRefUrl } from '../common/annotations.js'; -import { Lazy } from '../../../../base/common/lazy.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; - -/** For rendering slash commands, variables */ -const decorationRefUrl = `http://_vscodedecoration_`; - -/** For rendering agent decorations with hover */ -const agentRefUrl = `http://_chatagent_`; - -/** For rendering agent decorations with hover */ -const agentSlashRefUrl = `http://_chatslash_`; - -export function agentToMarkdown(agent: IChatAgentData, isClickable: boolean, accessor: ServicesAccessor): string { - const chatAgentNameService = accessor.get(IAideChatAgentNameService); - const chatAgentService = accessor.get(IAideChatAgentService); - - const isAllowed = chatAgentNameService.getAgentNameRestriction(agent); - let name = `${isAllowed ? agent.name : getFullyQualifiedId(agent)}`; - const isDupe = isAllowed && chatAgentService.agentHasDupeName(agent.id); - if (isDupe) { - name += ` (${agent.publisherDisplayName})`; - } - - const args: IAgentWidgetArgs = { agentId: agent.id, name, isClickable }; - return `[${agent.name}](${agentRefUrl}?${encodeURIComponent(JSON.stringify(args))})`; -} - -interface IAgentWidgetArgs { - agentId: string; - name: string; - isClickable?: boolean; -} - -export function agentSlashCommandToMarkdown(agent: IChatAgentData, command: IChatAgentCommand): string { - const text = `${chatSubcommandLeader}${command.name}`; - const args: ISlashCommandWidgetArgs = { agentId: agent.id, command: command.name }; - return `[${text}](${agentSlashRefUrl}?${encodeURIComponent(JSON.stringify(args))})`; -} - -interface ISlashCommandWidgetArgs { - agentId: string; - command: string; -} - -export class ChatMarkdownDecorationsRenderer { - constructor( - @IKeybindingService private readonly keybindingService: IKeybindingService, - @ILabelService private readonly labelService: ILabelService, - @ILogService private readonly logService: ILogService, - @IAideChatAgentService private readonly chatAgentService: IAideChatAgentService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IHoverService private readonly hoverService: IHoverService, - @IAideChatService private readonly chatService: IAideChatService, - @IAideChatWidgetService private readonly chatWidgetService: IAideChatWidgetService, - @ICommandService private readonly commandService: ICommandService, - ) { } - - convertParsedRequestToMarkdown(parsedRequest: IParsedChatRequest): string { - let result = ''; - for (const part of parsedRequest.parts) { - if (part instanceof ChatRequestTextPart) { - result += part.text; - } else if (part instanceof ChatRequestAgentPart) { - result += this.instantiationService.invokeFunction(accessor => agentToMarkdown(part.agent, false, accessor)); - } else { - const uri = part instanceof ChatRequestDynamicVariablePart && part.data instanceof URI ? - part.data : - undefined; - const title = uri ? encodeURIComponent(this.labelService.getUriLabel(uri, { relative: true })) : - part instanceof ChatRequestAgentPart ? part.agent.id : - ''; - - const text = part.text; - result += `[${text}](${decorationRefUrl}?${title})`; - } - } - - return result; - } - - walkTreeAndAnnotateReferenceLinks(element: HTMLElement): IDisposable { - const store = new DisposableStore(); - element.querySelectorAll('a').forEach(a => { - const href = a.getAttribute('data-href'); - if (href) { - if (href.startsWith(agentRefUrl)) { - let args: IAgentWidgetArgs | undefined; - try { - args = JSON.parse(decodeURIComponent(href.slice(agentRefUrl.length + 1))); - } catch (e) { - this.logService.error('Invalid chat widget render data JSON', toErrorMessage(e)); - } - - if (args) { - a.parentElement!.replaceChild( - this.renderAgentWidget(args, store), - a); - } - } else if (href.startsWith(agentSlashRefUrl)) { - let args: ISlashCommandWidgetArgs | undefined; - try { - args = JSON.parse(decodeURIComponent(href.slice(agentRefUrl.length + 1))); - } catch (e) { - this.logService.error('Invalid chat slash command render data JSON', toErrorMessage(e)); - } - - if (args) { - a.parentElement!.replaceChild( - this.renderSlashCommandWidget(a.textContent!, args, store), - a); - } - } else if (href.startsWith(decorationRefUrl)) { - const title = decodeURIComponent(href.slice(decorationRefUrl.length + 1)); - a.parentElement!.replaceChild( - this.renderResourceWidget(a.textContent!, title), - a); - } else if (href.startsWith(contentRefUrl)) { - this.renderFileWidget(href, a); - } else if (href.startsWith('command:')) { - this.injectKeybindingHint(a, href, this.keybindingService); - } - } - }); - - return store; - } - - private renderAgentWidget(args: IAgentWidgetArgs, store: DisposableStore): HTMLElement { - const nameWithLeader = `${chatAgentLeader}${args.name}`; - let container: HTMLElement; - if (args.isClickable) { - container = dom.$('span.chat-agent-widget'); - const button = store.add(new Button(container, { - buttonBackground: asCssVariable(chatSlashCommandBackground), - buttonForeground: asCssVariable(chatSlashCommandForeground), - buttonHoverBackground: undefined - })); - button.label = nameWithLeader; - store.add(button.onDidClick(() => { - const agent = this.chatAgentService.getAgent(args.agentId); - const widget = this.chatWidgetService.lastFocusedWidget; - if (!widget || !agent) { - return; - } - - this.chatService.sendRequest(widget.viewModel!.sessionId, agent.metadata.sampleRequest ?? '', { location: widget.location, agentId: agent.id }); - })); - } else { - container = this.renderResourceWidget(nameWithLeader, undefined); - } - - const agent = this.chatAgentService.getAgent(args.agentId); - const hover: Lazy = new Lazy(() => store.add(this.instantiationService.createInstance(ChatAgentHover))); - store.add(this.hoverService.setupManagedHover(getDefaultHoverDelegate('element'), container, () => { - hover.value.setAgent(args.agentId); - return hover.value.domNode; - }, agent && getChatAgentHoverOptions(() => agent, this.commandService))); - return container; - } - - private renderSlashCommandWidget(name: string, args: ISlashCommandWidgetArgs, store: DisposableStore): HTMLElement { - const container = dom.$('span.chat-agent-widget.chat-command-widget'); - const agent = this.chatAgentService.getAgent(args.agentId); - const button = store.add(new Button(container, { - buttonBackground: asCssVariable(chatSlashCommandBackground), - buttonForeground: asCssVariable(chatSlashCommandForeground), - buttonHoverBackground: undefined - })); - button.label = name; - store.add(button.onDidClick(() => { - const widget = this.chatWidgetService.lastFocusedWidget; - if (!widget || !agent) { - return; - } - - const command = agent.slashCommands.find(c => c.name === args.command); - this.chatService.sendRequest(widget.viewModel!.sessionId, command?.sampleRequest ?? '', { location: widget.location, agentId: agent.id, slashCommand: args.command }); - })); - - return container; - } - - private renderFileWidget(href: string, a: HTMLAnchorElement): void { - // TODO this can be a nicer FileLabel widget with an icon. Do a simple link for now. - const fullUri = URI.parse(href); - let location: Location | { uri: URI; range: undefined }; - try { - location = revive(JSON.parse(fullUri.fragment)); - } catch (err) { - this.logService.error('Invalid chat widget render data JSON', toErrorMessage(err)); - return; - } - - if (!location.uri || !URI.isUri(location.uri)) { - this.logService.error(`Invalid chat widget render data: ${fullUri.fragment}`); - return; - } - - const fragment = location.range ? `${location.range.startLineNumber}-${location.range.endLineNumber}` : ''; - a.setAttribute('data-href', location.uri.with({ fragment }).toString()); - - const label = this.labelService.getUriLabel(location.uri, { relative: true }); - a.title = location.range ? - `${label}#${location.range.startLineNumber}-${location.range.endLineNumber}` : - label; - } - - - private renderResourceWidget(name: string, title: string | undefined): HTMLElement { - const container = dom.$('span.chat-resource-widget'); - const alias = dom.$('span', undefined, name); - if (title) { - alias.title = title; - } - - container.appendChild(alias); - return container; - } - - - private injectKeybindingHint(a: HTMLAnchorElement, href: string, keybindingService: IKeybindingService): void { - const command = href.match(/command:([^\)]+)/)?.[1]; - if (command) { - const kb = keybindingService.lookupKeybinding(command); - if (kb) { - const keybinding = kb.getLabel(); - if (keybinding) { - a.textContent = `${a.textContent} (${keybinding})`; - } - } - } - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/aideChatMarkdownRenderer.ts b/src/vs/workbench/contrib/aideChat/browser/aideChatMarkdownRenderer.ts deleted file mode 100644 index 8a589f717e5..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/aideChatMarkdownRenderer.ts +++ /dev/null @@ -1,81 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { MarkdownRenderOptions, MarkedOptions } from '../../../../base/browser/markdownRenderer.js'; -import { IMarkdownString } from '../../../../base/common/htmlContent.js'; -import { IMarkdownRendererOptions, IMarkdownRenderResult, MarkdownRenderer } from '../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; -import { ILanguageService } from '../../../../editor/common/languages/language.js'; -import { IOpenerService } from '../../../../platform/opener/common/opener.js'; -import { ITrustedDomainService } from '../../../../workbench/contrib/url/browser/trustedDomainService.js'; - -const allowedHtmlTags = [ - 'b', - 'blockquote', - 'br', - 'code', - 'em', - 'h1', - 'h2', - 'h3', - 'h4', - 'h5', - 'h6', - 'hr', - 'i', - 'li', - 'ol', - 'p', - 'pre', - 'strong', - 'table', - 'tbody', - 'td', - 'th', - 'thead', - 'tr', - 'ul', - 'a', - 'img', - - // TODO@roblourens when we sanitize attributes in markdown source, we can ban these elements at that step. microsoft/vscode-copilot#5091 - // Not in the official list, but used for codicons and other vscode markdown extensions - 'span', - 'div', -]; - -/** - * This wraps the MarkdownRenderer and applies sanitizer options needed for Chat. - */ -export class ChatMarkdownRenderer extends MarkdownRenderer { - constructor( - options: IMarkdownRendererOptions | undefined, - @ILanguageService languageService: ILanguageService, - @IOpenerService openerService: IOpenerService, - @ITrustedDomainService private readonly trustedDomainService: ITrustedDomainService, - ) { - super(options ?? {}, languageService, openerService); - } - - override render(markdown: IMarkdownString | undefined, options?: MarkdownRenderOptions, markedOptions?: MarkedOptions): IMarkdownRenderResult { - options = { - ...options, - remoteImageIsAllowed: (uri) => this.trustedDomainService.isValid(uri), - sanitizerOptions: { - replaceWithPlaintext: true, - allowedTags: allowedHtmlTags, - } - }; - - const mdWithBody: IMarkdownString | undefined = (markdown && markdown.supportHtml) ? - { - ...markdown, - - // dompurify uses DOMParser, which strips leading comments. Wrapping it all in 'body' prevents this. - value: `${markdown.value}`, - } - : markdown; - return super.render(mdWithBody, options, markedOptions); - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/aideChatOptions.ts b/src/vs/workbench/contrib/aideChat/browser/aideChatOptions.ts deleted file mode 100644 index 68292bf87d4..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/aideChatOptions.ts +++ /dev/null @@ -1,129 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Color } from '../../../../base/common/color.js'; -import { Emitter } from '../../../../base/common/event.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { IBracketPairColorizationOptions, IEditorOptions } from '../../../../editor/common/config/editorOptions.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { IViewDescriptorService } from '../../../../workbench/common/views.js'; - -export interface IChatConfiguration { - editor: { - readonly fontSize: number; - readonly fontFamily: string; - readonly lineHeight: number; - readonly fontWeight: string; - readonly wordWrap: 'off' | 'on'; - }; -} - -export interface IChatEditorConfiguration { - readonly foreground: Color | undefined; - readonly inputEditor: IChatInputEditorOptions; - readonly resultEditor: IChatResultEditorOptions; -} - -export interface IChatInputEditorOptions { - readonly backgroundColor: Color | undefined; - readonly accessibilitySupport: string; -} - -export interface IChatResultEditorOptions { - readonly fontSize: number; - readonly fontFamily: string | undefined; - readonly lineHeight: number; - readonly fontWeight: string; - readonly backgroundColor: Color | undefined; - readonly bracketPairColorization: IBracketPairColorizationOptions; - readonly fontLigatures: boolean | string | undefined; - readonly wordWrap: 'off' | 'on'; - - // Bring these back if we make the editors editable - // readonly cursorBlinking: string; - // readonly accessibilitySupport: string; -} - - -export class ChatEditorOptions extends Disposable { - private static readonly lineHeightEm = 1.4; - - private readonly _onDidChange = this._register(new Emitter()); - readonly onDidChange = this._onDidChange.event; - - private _config!: IChatEditorConfiguration; - public get configuration(): IChatEditorConfiguration { - return this._config; - } - - private static readonly relevantSettingIds = [ - 'aideChat.editor.lineHeight', - 'aideChat.editor.fontSize', - 'aideChat.editor.fontFamily', - 'aideChat.editor.fontWeight', - 'aideChat.editor.wordWrap', - 'editor.cursorBlinking', - 'editor.fontLigatures', - 'editor.accessibilitySupport', - 'editor.bracketPairColorization.enabled', - 'editor.bracketPairColorization.independentColorPoolPerBracketType', - ]; - - constructor( - viewId: string | undefined, - private readonly foreground: string, - private readonly inputEditorBackgroundColor: string, - private readonly resultEditorBackgroundColor: string, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IThemeService private readonly themeService: IThemeService, - @IViewDescriptorService private readonly viewDescriptorService: IViewDescriptorService - ) { - super(); - - this._register(this.themeService.onDidColorThemeChange(e => this.update())); - this._register(this.viewDescriptorService.onDidChangeLocation(e => { - if (e.views.some(v => v.id === viewId)) { - this.update(); - } - })); - this._register(this.configurationService.onDidChangeConfiguration(e => { - if (ChatEditorOptions.relevantSettingIds.some(id => e.affectsConfiguration(id))) { - this.update(); - } - })); - this.update(); - } - - private update() { - const editorConfig = this.configurationService.getValue('editor'); - - // TODO shouldn't the setting keys be more specific? - const chatEditorConfig = this.configurationService.getValue('aideChat')?.editor; - const accessibilitySupport = this.configurationService.getValue<'auto' | 'off' | 'on'>('editor.accessibilitySupport'); - this._config = { - foreground: this.themeService.getColorTheme().getColor(this.foreground), - inputEditor: { - backgroundColor: this.themeService.getColorTheme().getColor(this.inputEditorBackgroundColor), - accessibilitySupport, - }, - resultEditor: { - backgroundColor: this.themeService.getColorTheme().getColor(this.resultEditorBackgroundColor), - fontSize: chatEditorConfig.fontSize, - fontFamily: chatEditorConfig.fontFamily === 'default' ? editorConfig.fontFamily : chatEditorConfig.fontFamily, - fontWeight: chatEditorConfig.fontWeight, - lineHeight: chatEditorConfig.lineHeight ? chatEditorConfig.lineHeight : ChatEditorOptions.lineHeightEm * chatEditorConfig.fontSize, - bracketPairColorization: { - enabled: this.configurationService.getValue('editor.bracketPairColorization.enabled'), - independentColorPoolPerBracketType: this.configurationService.getValue('editor.bracketPairColorization.independentColorPoolPerBracketType'), - }, - wordWrap: chatEditorConfig.wordWrap, - fontLigatures: editorConfig.fontLigatures, - } - - }; - this._onDidChange.fire(); - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/aideChatParticipantContributions.ts b/src/vs/workbench/contrib/aideChat/browser/aideChatParticipantContributions.ts deleted file mode 100644 index 10a14e47871..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/aideChatParticipantContributions.ts +++ /dev/null @@ -1,272 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { isNonEmptyArray } from '../../../../base/common/arrays.js'; -import * as strings from '../../../../base/common/strings.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { Disposable, DisposableMap, DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { localize, localize2 } from '../../../../nls.js'; -import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; -import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { Registry } from '../../../../platform/registry/common/platform.js'; -import { ViewPaneContainer } from '../../../../workbench/browser/parts/views/viewPaneContainer.js'; -import { IWorkbenchContribution } from '../../../../workbench/common/contributions.js'; -import { IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation, Extensions as ViewExtensions } from '../../../../workbench/common/views.js'; -import { CHAT_VIEW_ID } from '../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { CHAT_SIDEBAR_PANEL_ID, ChatViewPane } from '../../../../workbench/contrib/aideChat/browser/aideChatViewPane.js'; -import { AideChatAgentLocation, IChatAgentData, IAideChatAgentService } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { IRawChatParticipantContribution } from '../../../../workbench/contrib/aideChat/common/aideChatParticipantContribTypes.js'; -import { isProposedApiEnabled } from '../../../../workbench/services/extensions/common/extensions.js'; -import * as extensionsRegistry from '../../../../workbench/services/extensions/common/extensionsRegistry.js'; -import { INotificationService, Severity } from '../../../../platform/notification/common/notification.js'; -import { Action } from '../../../../base/common/actions.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; - -const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ - extensionPoint: 'aideChatParticipants', - jsonSchema: { - description: localize('vscode.extension.contributes.aideChatParticipant', 'Contributes a chat participant'), - type: 'array', - items: { - additionalProperties: false, - type: 'object', - defaultSnippets: [{ body: { name: '', description: '' } }], - required: ['name', 'id'], - properties: { - id: { - description: localize('aideChatParticipantId', "A unique id for this chat participant."), - type: 'string' - }, - name: { - description: localize('aideChatParticipantName', "User-facing name for this chat participant. The user will use '@' with this name to invoke the participant. Name must not contain whitespace."), - type: 'string', - pattern: '^[\\w0-9_-]+$' - }, - fullName: { - markdownDescription: localize('aideChatParticipantFullName', "The full name of this chat participant, which is shown as the label for responses coming from this participant. If not provided, {0} is used.", '`name`'), - type: 'string' - }, - description: { - description: localize('aideChatParticipantDescription', "A description of this chat participant, shown in the UI."), - type: 'string' - }, - isSticky: { - description: localize('aideChatCommandSticky', "Whether invoking the command puts the chat into a persistent mode, where the command is automatically added to the chat input for the next message."), - type: 'boolean' - }, - sampleRequest: { - description: localize('aideChatSampleRequest', "When the user clicks this participant in `/help`, this text will be submitted to the participant."), - type: 'string' - }, - when: { - description: localize('aideChatParticipantWhen', "A condition which must be true to enable this participant."), - type: 'string' - }, - commands: { - markdownDescription: localize('aideChatCommandsDescription', "Commands available for this chat participant, which the user can invoke with a `/`."), - type: 'array', - items: { - additionalProperties: false, - type: 'object', - defaultSnippets: [{ body: { name: '', description: '' } }], - required: ['name'], - properties: { - name: { - description: localize('aideChatCommand', "A short name by which this command is referred to in the UI, e.g. `fix` or * `explain` for commands that fix an issue or explain code. The name should be unique among the commands provided by this participant."), - type: 'string' - }, - description: { - description: localize('aideChatCommandDescription', "A description of this command."), - type: 'string' - }, - when: { - description: localize('aideChatCommandWhen', "A condition which must be true to enable this command."), - type: 'string' - }, - sampleRequest: { - description: localize('aideChatCommandSampleRequest', "When the user clicks this command in `/help`, this text will be submitted to the participant."), - type: 'string' - }, - isSticky: { - description: localize('aideChatCommandSticky', "Whether invoking the command puts the chat into a persistent mode, where the command is automatically added to the chat input for the next message."), - type: 'boolean' - }, - } - } - }, - } - } - }, - activationEventsGenerator: (contributions: IRawChatParticipantContribution[], result: { push(item: string): void }) => { - for (const contrib of contributions) { - result.push(`onChatParticipant:${contrib.id}`); - } - }, -}); - -export class ChatExtensionPointHandler implements IWorkbenchContribution { - - static readonly ID = 'workbench.contrib.aideChatExtensionPointHandler'; - - private _viewContainer: ViewContainer; - private _participantRegistrationDisposables = new DisposableMap(); - - constructor( - @IAideChatAgentService private readonly _chatAgentService: IAideChatAgentService, - @ILogService private readonly logService: ILogService, - @INotificationService private readonly notificationService: INotificationService, - @ICommandService private readonly commandService: ICommandService, - ) { - this._viewContainer = this.registerViewContainer(); - this.handleAndRegisterChatExtensions(); - } - - private handleAndRegisterChatExtensions(): void { - chatParticipantExtensionPoint.setHandler((extensions, delta) => { - for (const extension of delta.added) { - // Detect old version of Copilot Chat extension. - // TODO@roblourens remove after one release, after this we will rely on things like the API version - if (extension.value.some(participant => participant.id === 'github.copilot.default' && !participant.fullName)) { - this.notificationService.notify({ - severity: Severity.Error, - message: localize('aideChatFailErrorMessage', "Chat failed to load. Please ensure that the GitHub Copilot Chat extension is up to date."), - actions: { - primary: [ - new Action('showExtension', localize('action.showExtension', "Show Extension"), undefined, true, () => { - return this.commandService.executeCommand('workbench.extensions.action.showExtensionsWithIds', ['GitHub.copilot-chat']); - }) - ] - } - }); - continue; - } - - for (const providerDescriptor of extension.value) { - if (!providerDescriptor.name.match(/^[\w0-9_-]+$/)) { - this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant with invalid name: ${providerDescriptor.name}. Name must match /^[\\w0-9_-]+$/.`); - continue; - } - - if (providerDescriptor.fullName && strings.AmbiguousCharacters.getInstance(new Set()).containsAmbiguousCharacter(providerDescriptor.fullName)) { - this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant with fullName that contains ambiguous characters: ${providerDescriptor.fullName}.`); - continue; - } - - // Spaces are allowed but considered "invisible" - if (providerDescriptor.fullName && strings.InvisibleCharacters.containsInvisibleCharacter(providerDescriptor.fullName.replace(/ /g, ''))) { - this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant with fullName that contains invisible characters: ${providerDescriptor.fullName}.`); - continue; - } - - if (providerDescriptor.isDefault && !isProposedApiEnabled(extension.description, 'defaultChatParticipant')) { - this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT use API proposal: defaultChatParticipant.`); - continue; - } - - if ((providerDescriptor.defaultImplicitVariables || providerDescriptor.locations) && !isProposedApiEnabled(extension.description, 'chatParticipantAdditions')) { - this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT use API proposal: chatParticipantAdditions.`); - continue; - } - - if (!providerDescriptor.id || !providerDescriptor.name) { - this.logService.error(`Extension '${extension.description.identifier.value}' CANNOT register participant without both id and name.`); - continue; - } - - const store = new DisposableStore(); - if (providerDescriptor.isDefault && (!providerDescriptor.locations || providerDescriptor.locations?.includes(AideChatAgentLocation.Panel))) { - store.add(this.registerDefaultParticipantView(providerDescriptor)); - } - - store.add(this._chatAgentService.registerAgent( - providerDescriptor.id, - { - extensionId: extension.description.identifier, - publisherDisplayName: extension.description.publisherDisplayName ?? extension.description.publisher, // May not be present in OSS - extensionPublisherId: extension.description.publisher, - extensionDisplayName: extension.description.displayName ?? extension.description.name, - id: providerDescriptor.id, - description: providerDescriptor.description, - when: providerDescriptor.when, - metadata: { - isSticky: providerDescriptor.isSticky, - sampleRequest: providerDescriptor.sampleRequest, - }, - name: providerDescriptor.name, - fullName: providerDescriptor.fullName, - isDefault: providerDescriptor.isDefault, - defaultImplicitVariables: providerDescriptor.defaultImplicitVariables, - locations: isNonEmptyArray(providerDescriptor.locations) ? - providerDescriptor.locations.map(AideChatAgentLocation.fromRaw) : - [AideChatAgentLocation.Panel], - slashCommands: providerDescriptor.commands ?? [] - } satisfies IChatAgentData)); - - this._participantRegistrationDisposables.set( - getParticipantKey(extension.description.identifier, providerDescriptor.id), - store - ); - } - } - - for (const extension of delta.removed) { - for (const providerDescriptor of extension.value) { - this._participantRegistrationDisposables.deleteAndDispose(getParticipantKey(extension.description.identifier, providerDescriptor.name)); - } - } - }); - } - - private registerViewContainer(): ViewContainer { - // Register View Container - const title = localize2('aideChat.viewContainer.label', "Chat"); - const icon = Codicon.commentDiscussion; - const viewContainerId = CHAT_SIDEBAR_PANEL_ID; - const viewContainer: ViewContainer = Registry.as(ViewExtensions.ViewContainersRegistry).registerViewContainer({ - id: viewContainerId, - title, - icon, - ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [viewContainerId, { mergeViewWithContainerWhenSingleView: true }]), - storageId: viewContainerId, - hideIfEmpty: true, - order: 100, - }, ViewContainerLocation.Sidebar); - - return viewContainer; - } - - private hasRegisteredDefaultParticipantView = false; - private registerDefaultParticipantView(defaultParticipantDescriptor: IRawChatParticipantContribution): IDisposable { - if (this.hasRegisteredDefaultParticipantView) { - this.logService.warn(`Tried to register a second default chat participant view for "${defaultParticipantDescriptor.id}"`); - return Disposable.None; - } - - // Register View - const name = defaultParticipantDescriptor.fullName ?? defaultParticipantDescriptor.name; - const viewDescriptor: IViewDescriptor[] = [{ - id: CHAT_VIEW_ID, - containerIcon: this._viewContainer.icon, - containerTitle: this._viewContainer.title.value, - singleViewPaneContainerTitle: this._viewContainer.title.value, - name: { value: name, original: name }, - canToggleVisibility: false, - canMoveView: false, - ctorDescriptor: new SyncDescriptor(ChatViewPane), - }]; - this.hasRegisteredDefaultParticipantView = true; - Registry.as(ViewExtensions.ViewsRegistry).registerViews(viewDescriptor, this._viewContainer); - - return toDisposable(() => { - this.hasRegisteredDefaultParticipantView = false; - Registry.as(ViewExtensions.ViewsRegistry).deregisterViews(viewDescriptor, this._viewContainer); - }); - } -} - -function getParticipantKey(extensionId: ExtensionIdentifier, participantName: string): string { - return `${extensionId.value}_${participantName}`; -} diff --git a/src/vs/workbench/contrib/aideChat/browser/aideChatQuick.ts b/src/vs/workbench/contrib/aideChat/browser/aideChatQuick.ts deleted file mode 100644 index ac5f5ff0a4d..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/aideChatQuick.ts +++ /dev/null @@ -1,342 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from '../../../../base/browser/dom.js'; -import { Orientation, Sash } from '../../../../base/browser/ui/sash/sash.js'; -import { disposableTimeout } from '../../../../base/common/async.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { Selection } from '../../../../editor/common/core/selection.js'; -import { MenuId } from '../../../../platform/actions/common/actions.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; -import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js'; -import { IQuickInputService, IQuickWidget } from '../../../../platform/quickinput/common/quickInput.js'; -import { editorBackground, inputBackground, quickInputBackground, quickInputForeground } from '../../../../platform/theme/common/colorRegistry.js'; -import { IQuickChatOpenOptions, IQuickChatService, showChatView } from '../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { ChatWidget } from '../../../../workbench/contrib/aideChat/browser/aideChatWidget.js'; -import { AideChatAgentLocation } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { ChatModel } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { IParsedChatRequest } from '../../../../workbench/contrib/aideChat/common/aideChatParserTypes.js'; -import { IAideChatProgress, IAideChatService } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; - -export class QuickChatService extends Disposable implements IQuickChatService { - readonly _serviceBrand: undefined; - - private readonly _onDidClose = this._register(new Emitter()); - readonly onDidClose = this._onDidClose.event; - - private _input: IQuickWidget | undefined; - // TODO@TylerLeonhardt: support multiple chat providers eventually - private _currentChat: QuickChat | undefined; - private _container: HTMLElement | undefined; - - constructor( - @IQuickInputService private readonly quickInputService: IQuickInputService, - @IAideChatService private readonly chatService: IAideChatService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - ) { - super(); - } - - get enabled(): boolean { - return !!this.chatService.isEnabled(AideChatAgentLocation.Panel); - } - - get focused(): boolean { - const widget = this._input?.widget as HTMLElement | undefined; - if (!widget) { - return false; - } - return dom.isAncestorOfActiveElement(widget); - } - - toggle(options?: IQuickChatOpenOptions): void { - // If the input is already shown, hide it. This provides a toggle behavior of the quick - // pick. This should not happen when there is a query. - if (this.focused && !options?.query) { - this.close(); - } else { - this.open(options); - // If this is a partial query, the value should be cleared when closed as otherwise it - // would remain for the next time the quick chat is opened in any context. - if (options?.isPartialQuery) { - const disposable = this._store.add(Event.once(this.onDidClose)(() => { - this._currentChat?.clearValue(); - this._store.delete(disposable); - })); - } - } - } - - open(options?: IQuickChatOpenOptions): void { - if (this._input) { - if (this._currentChat && options?.query) { - this._currentChat.focus(); - this._currentChat.setValue(options.query, options.selection); - if (!options.isPartialQuery) { - this._currentChat.acceptInput(); - } - return; - } - return this.focus(); - } - - const disposableStore = new DisposableStore(); - - this._input = this.quickInputService.createQuickWidget(); - this._input.contextKey = 'chatInputVisible'; - this._input.ignoreFocusOut = true; - disposableStore.add(this._input); - - this._container ??= dom.$('.cschat-session'); - this._input.widget = this._container; - - this._input.show(); - if (!this._currentChat) { - this._currentChat = this.instantiationService.createInstance(QuickChat); - - // show needs to come after the quickpick is shown - this._currentChat.render(this._container); - } else { - this._currentChat.show(); - } - - disposableStore.add(this._input.onDidHide(() => { - disposableStore.dispose(); - this._currentChat!.hide(); - this._input = undefined; - this._onDidClose.fire(); - })); - - this._currentChat.focus(); - - if (options?.query) { - this._currentChat.setValue(options.query, options.selection); - if (!options.isPartialQuery) { - this._currentChat.acceptInput(); - } - } - } - focus(): void { - this._currentChat?.focus(); - } - close(): void { - this._input?.dispose(); - this._input = undefined; - } - async openInChatView(): Promise { - await this._currentChat?.openChatView(); - this.close(); - } -} - -class QuickChat extends Disposable { - // TODO@TylerLeonhardt: be responsive to window size - static DEFAULT_MIN_HEIGHT = 200; - private static readonly DEFAULT_HEIGHT_OFFSET = 100; - - private widget!: ChatWidget; - private sash!: Sash; - private model: ChatModel | undefined; - private _currentQuery: string | undefined; - private readonly maintainScrollTimer: MutableDisposable = this._register(new MutableDisposable()); - private _deferUpdatingDynamicLayout: boolean = false; - - constructor( - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IAideChatService private readonly chatService: IAideChatService, - @ILayoutService private readonly layoutService: ILayoutService, - @IViewsService private readonly viewsService: IViewsService, - ) { - super(); - } - - clear() { - this.model?.dispose(); - this.model = undefined; - this.updateModel(); - this.widget.inputEditor.setValue(''); - } - - focus(selection?: Selection): void { - if (this.widget) { - this.widget.focusInput(); - const value = this.widget.inputEditor.getValue(); - if (value) { - this.widget.inputEditor.setSelection(selection ?? { - startLineNumber: 1, - startColumn: 1, - endLineNumber: 1, - endColumn: value.length + 1 - }); - } - } - } - - hide(): void { - this.widget.setVisible(false); - // Maintain scroll position for a short time so that if the user re-shows the chat - // the same scroll position will be used. - this.maintainScrollTimer.value = disposableTimeout(() => { - // At this point, clear this mutable disposable which will be our signal that - // the timer has expired and we should stop maintaining scroll position - this.maintainScrollTimer.clear(); - }, 30 * 1000); // 30 seconds - } - - show(): void { - this.widget.setVisible(true); - // If the mutable disposable is set, then we are keeping the existing scroll position - // so we should not update the layout. - if (this._deferUpdatingDynamicLayout) { - this._deferUpdatingDynamicLayout = false; - this.widget.updateDynamicChatTreeItemLayout(2, this.maxHeight); - } - if (!this.maintainScrollTimer.value) { - this.widget.layoutDynamicChatTreeItemMode(); - } - } - - render(parent: HTMLElement): void { - if (this.widget) { - // NOTE: if this changes, we need to make sure disposables in this function are tracked differently. - throw new Error('Cannot render quick chat twice'); - } - const scopedInstantiationService = this._register(this.instantiationService.createChild( - new ServiceCollection([ - IContextKeyService, - this._register(this.contextKeyService.createScoped(parent)) - ]) - )); - this.widget = this._register( - scopedInstantiationService.createInstance( - ChatWidget, - AideChatAgentLocation.Panel, - { resource: true }, - { renderInputOnTop: true, renderStyle: 'compact', menus: { inputSideToolbar: MenuId.AideChatInputSide } }, - { - listForeground: quickInputForeground, - listBackground: quickInputBackground, - inputEditorBackground: inputBackground, - resultEditorBackground: editorBackground - })); - this.widget.render(parent); - this.widget.setVisible(true); - this.widget.setDynamicChatTreeItemLayout(2, this.maxHeight); - this.updateModel(); - this.sash = this._register(new Sash(parent, { getHorizontalSashTop: () => parent.offsetHeight }, { orientation: Orientation.HORIZONTAL })); - this.registerListeners(parent); - } - - private get maxHeight(): number { - return this.layoutService.mainContainerDimension.height - QuickChat.DEFAULT_HEIGHT_OFFSET; - } - - private registerListeners(parent: HTMLElement): void { - this._register(this.layoutService.onDidLayoutMainContainer(() => { - if (this.widget.visible) { - this.widget.updateDynamicChatTreeItemLayout(2, this.maxHeight); - } else { - // If the chat is not visible, then we should defer updating the layout - // because it relies on offsetHeight which only works correctly - // when the chat is visible. - this._deferUpdatingDynamicLayout = true; - } - })); - this._register(this.widget.inputEditor.onDidChangeModelContent((e) => { - this._currentQuery = this.widget.inputEditor.getValue(); - })); - this._register(this.widget.onDidClear(() => this.clear())); - this._register(this.widget.onDidChangeHeight((e) => this.sash.layout())); - const width = parent.offsetWidth; - this._register(this.sash.onDidStart(() => { - this.widget.isDynamicChatTreeItemLayoutEnabled = false; - })); - this._register(this.sash.onDidChange((e) => { - if (e.currentY < QuickChat.DEFAULT_MIN_HEIGHT || e.currentY > this.maxHeight) { - return; - } - this.widget.layout(e.currentY, width); - this.sash.layout(); - })); - this._register(this.sash.onDidReset(() => { - this.widget.isDynamicChatTreeItemLayoutEnabled = true; - this.widget.layoutDynamicChatTreeItemMode(); - })); - } - - async acceptInput() { - return this.widget.acceptInput(); - } - - async openChatView(): Promise { - const widget = await showChatView(this.viewsService); - if (!widget?.viewModel || !this.model) { - return; - } - - for (const request of this.model.getRequests()) { - if (request.response?.response.value || request.response?.result) { - - - const message: IAideChatProgress[] = []; - for (const item of request.response.response.value) { - if (item.kind === 'textEditGroup') { - for (const group of item.edits) { - message.push({ - kind: 'textEdit', - edits: group, - uri: item.uri - }); - } - } else { - message.push(item); - } - } - - this.chatService.addCompleteRequest(widget.viewModel.sessionId, - request.message as IParsedChatRequest, - request.variableData, - request.attempt, - { - message, - result: request.response.result, - followups: request.response.followups - }); - } else if (request.message) { - - } - } - - const value = this.widget.inputEditor.getValue(); - if (value) { - widget.inputEditor.setValue(value); - } - widget.focusInput(); - } - - setValue(value: string, selection?: Selection): void { - this.widget.inputEditor.setValue(value); - this.focus(selection); - } - - clearValue(): void { - this.widget.inputEditor.setValue(''); - } - - private updateModel(): void { - this.model ??= this.chatService.startSession(AideChatAgentLocation.Panel, CancellationToken.None); - if (!this.model) { - throw new Error('Could not start chat session'); - } - - this.widget.setModel(this.model, { inputValue: this._currentQuery }); - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/aideChatVariables.ts b/src/vs/workbench/contrib/aideChat/browser/aideChatVariables.ts deleted file mode 100644 index d0e6b0f6a82..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/aideChatVariables.ts +++ /dev/null @@ -1,185 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { basename } from '../../../../base/common/path.js'; -import { coalesce } from '../../../../base/common/arrays.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { onUnexpectedExternalError } from '../../../../base/common/errors.js'; -import { Iterable } from '../../../../base/common/iterator.js'; -import { IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { URI } from '../../../../base/common/uri.js'; -import { Location } from '../../../../editor/common/languages.js'; -import { IAideChatWidgetService, showChatView } from '../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { ChatDynamicVariableModel } from '../../../../workbench/contrib/aideChat/browser/contrib/aideChatDynamicVariables.js'; -import { AideChatAgentLocation } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { IChatModel, IChatRequestVariableData, IAideChatRequestVariableEntry } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { ChatRequestDynamicVariablePart, ChatRequestVariablePart, IParsedChatRequest } from '../../../../workbench/contrib/aideChat/common/aideChatParserTypes.js'; -import { IAideChatContentReference } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { IAideChatRequestVariableValue, IAideChatVariableData, IChatVariableResolver, IAideChatVariableResolverProgress, IAideChatVariablesService, IDynamicVariable } from '../../../../workbench/contrib/aideChat/common/aideChatVariables.js'; -import { ChatContextAttachments } from '../../../../workbench/contrib/aideChat/browser/contrib/aideChatContextAttachments.js'; -import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; - -interface IChatData { - data: IAideChatVariableData; - resolver: IChatVariableResolver; -} - -export class ChatVariablesService implements IAideChatVariablesService { - declare _serviceBrand: undefined; - - private _resolver = new Map(); - - constructor( - @IAideChatWidgetService private readonly chatWidgetService: IAideChatWidgetService, - @IViewsService private readonly viewsService: IViewsService, - ) { - } - - async resolveVariables(prompt: IParsedChatRequest, attachedContextVariables: IAideChatRequestVariableEntry[] | undefined, model: IChatModel, progress: (part: IAideChatVariableResolverProgress) => void, token: CancellationToken): Promise { - let resolvedVariables: IAideChatRequestVariableEntry[] = []; - const jobs: Promise[] = []; - - prompt.parts - .forEach((part, i) => { - if (part instanceof ChatRequestVariablePart) { - const data = this._resolver.get(part.variableName.toLowerCase()); - if (data) { - const references: IAideChatContentReference[] = []; - const variableProgressCallback = (item: IAideChatVariableResolverProgress) => { - if (item.kind === 'reference') { - references.push(item); - return; - } - progress(item); - }; - jobs.push(data.resolver(prompt.text, part.variableArg, model, variableProgressCallback, token).then(value => { - if (value) { - resolvedVariables[i] = { id: data.data.id, modelDescription: data.data.modelDescription, name: part.variableName, range: part.range, value, references }; - } - }).catch(onUnexpectedExternalError)); - } - } else if (part instanceof ChatRequestDynamicVariablePart) { - resolvedVariables[i] = { id: part.id, name: part.referenceText, range: part.range, value: part.data }; - } - }); - - const resolvedAttachedContext: IAideChatRequestVariableEntry[] = []; - attachedContextVariables - ?.forEach((attachment, i) => { - const data = this._resolver.get(attachment.name?.toLowerCase()); - if (data) { - const references: IAideChatContentReference[] = []; - const variableProgressCallback = (item: IAideChatVariableResolverProgress) => { - if (item.kind === 'reference') { - references.push(item); - return; - } - progress(item); - }; - jobs.push(data.resolver(prompt.text, '', model, variableProgressCallback, token).then(value => { - if (value) { - resolvedAttachedContext[i] = { id: data.data.id, modelDescription: data.data.modelDescription, name: attachment.name, range: attachment.range, value, references }; - } - }).catch(onUnexpectedExternalError)); - } else if (attachment.isDynamic) { - resolvedAttachedContext[i] = { id: attachment.id, name: attachment.name, value: attachment.value }; - } - }); - - await Promise.allSettled(jobs); - - // Make array not sparse - resolvedVariables = coalesce(resolvedVariables); - - // "reverse", high index first so that replacement is simple - resolvedVariables.sort((a, b) => b.range!.start - a.range!.start); - - // resolvedAttachedContext is a sparse array - resolvedVariables.push(...coalesce(resolvedAttachedContext)); - - - return { - variables: resolvedVariables, - }; - } - - async resolveVariable(variableName: string, promptText: string, model: IChatModel, progress: (part: IAideChatVariableResolverProgress) => void, token: CancellationToken): Promise { - const data = this._resolver.get(variableName.toLowerCase()); - if (!data) { - return undefined; - } - - return (await data.resolver(promptText, undefined, model, progress, token)); - } - - hasVariable(name: string): boolean { - return this._resolver.has(name.toLowerCase()); - } - - getVariable(name: string): IAideChatVariableData | undefined { - return this._resolver.get(name.toLowerCase())?.data; - } - - getVariables(): Iterable> { - const all = Iterable.map(this._resolver.values(), data => data.data); - return Iterable.filter(all, data => !data.hidden); - } - - getDynamicVariables(sessionId: string): ReadonlyArray { - // This is slightly wrong... the parser pulls dynamic references from the input widget, but there is no guarantee that message came from the input here. - // Need to ... - // - Parser takes list of dynamic references (annoying) - // - Or the parser is known to implicitly act on the input widget, and we need to call it before calling the chat service (maybe incompatible with the future, but easy) - const widget = this.chatWidgetService.getWidgetBySessionId(sessionId); - if (!widget || !widget.viewModel || !widget.supportsFileReferences) { - return []; - } - - const model = widget.getContrib(ChatDynamicVariableModel.ID); - if (!model) { - return []; - } - - return model.variables; - } - - registerVariable(data: IAideChatVariableData, resolver: IChatVariableResolver): IDisposable { - const key = data.name.toLowerCase(); - if (this._resolver.has(key)) { - throw new Error(`A chat variable with the name '${data.name}' already exists.`); - } - this._resolver.set(key, { data, resolver }); - return toDisposable(() => { - this._resolver.delete(key); - }); - } - - async attachContext(name: string, value: string | URI | Location, location: AideChatAgentLocation) { - if (location !== AideChatAgentLocation.Panel) { - return; - } - - await showChatView(this.viewsService); - const widget = this.chatWidgetService.lastFocusedWidget; - if (!widget || !widget.viewModel) { - return; - } - - const key = name.toLowerCase(); - if (key === 'file' && typeof value !== 'string') { - const uri = URI.isUri(value) ? value : value.uri; - const range = 'range' in value ? value.range : undefined; - widget.getContrib(ChatContextAttachments.ID)?.setContext(false, { value, id: uri.toString() + (range?.toString() ?? ''), name: basename(uri.path), isFile: true, isDynamic: true }); - return; - } - - const resolved = this._resolver.get(key); - if (!resolved) { - return; - } - - widget.getContrib(ChatContextAttachments.ID)?.setContext(false, { ...resolved.data, value }); - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/aideChatViewPane.ts b/src/vs/workbench/contrib/aideChat/browser/aideChatViewPane.ts deleted file mode 100644 index 36e1b640948..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/aideChatViewPane.ts +++ /dev/null @@ -1,223 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; -import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; -import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { IOpenerService } from '../../../../platform/opener/common/opener.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { IViewPaneOptions, ViewPane } from '../../../../workbench/browser/parts/views/viewPane.js'; -import { Memento } from '../../../../workbench/common/memento.js'; -import { SIDE_BAR_FOREGROUND } from '../../../../workbench/common/theme.js'; -import { IViewDescriptorService } from '../../../../workbench/common/views.js'; -import { IChatViewPane } from '../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { IChatViewState, ChatWidget } from '../../../../workbench/contrib/aideChat/browser/aideChatWidget.js'; -import { AideChatAgentLocation, IAideChatAgentService } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { CHAT_PROVIDER_ID } from '../../../../workbench/contrib/aideChat/common/aideChatParticipantContribTypes.js'; -import { ChatModelInitState, IChatModel } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { IAideChatService } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { IChatViewTitleActionContext } from '../../../../workbench/contrib/aideChat/browser/actions/aideChatActions.js'; - -interface IViewPaneState extends IChatViewState { - sessionId?: string; -} - -export const CHAT_SIDEBAR_PANEL_ID = 'workbench.panel.aideChatSidebar'; -export class ChatViewPane extends ViewPane implements IChatViewPane { - private _widget!: ChatWidget; - get widget(): ChatWidget { return this._widget; } - - private readonly modelDisposables = this._register(new DisposableStore()); - private memento: Memento; - private readonly viewState: IViewPaneState; - private didProviderRegistrationFail = false; - private didUnregisterProvider = false; - - constructor( - options: IViewPaneOptions, - @IKeybindingService keybindingService: IKeybindingService, - @IContextMenuService contextMenuService: IContextMenuService, - @IConfigurationService configurationService: IConfigurationService, - @IContextKeyService contextKeyService: IContextKeyService, - @IViewDescriptorService viewDescriptorService: IViewDescriptorService, - @IInstantiationService instantiationService: IInstantiationService, - @IOpenerService openerService: IOpenerService, - @IThemeService themeService: IThemeService, - @ITelemetryService telemetryService: ITelemetryService, - @IHoverService hoverService: IHoverService, - @IStorageService private readonly storageService: IStorageService, - @IAideChatService private readonly chatService: IAideChatService, - @IAideChatAgentService private readonly chatAgentService: IAideChatAgentService, - @ILogService private readonly logService: ILogService, - ) { - super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, telemetryService, hoverService); - - // View state for the ViewPane is currently global per-provider basically, but some other strictly per-model state will require a separate memento. - this.memento = new Memento('aide-chat-view-' + CHAT_PROVIDER_ID, this.storageService); - this.viewState = this.memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE) as IViewPaneState; - this._register(this.chatAgentService.onDidChangeAgents(() => { - if (this.chatAgentService.getDefaultAgent(AideChatAgentLocation.Panel)) { - if (!this._widget?.viewModel) { - const sessionId = this.getSessionId(); - const model = sessionId ? this.chatService.getOrRestoreSession(sessionId) : undefined; - - // The widget may be hidden at this point, because welcome views were allowed. Use setVisible to - // avoid doing a render while the widget is hidden. This is changing the condition in `shouldShowWelcome` - // so it should fire onDidChangeViewWelcomeState. - try { - this._widget.setVisible(false); - this.updateModel(model); - this.didProviderRegistrationFail = false; - this.didUnregisterProvider = false; - this._onDidChangeViewWelcomeState.fire(); - } finally { - this.widget.setVisible(true); - } - } - } else if (this._widget?.viewModel?.initState === ChatModelInitState.Initialized) { - // Model is initialized, and the default agent disappeared, so show welcome view - this.didUnregisterProvider = true; - this._onDidChangeViewWelcomeState.fire(); - } - })); - } - - override getActionsContext(): IChatViewTitleActionContext { - return { - chatView: this - }; - } - - private updateModel(model?: IChatModel | undefined, viewState?: IViewPaneState): void { - this.modelDisposables.clear(); - - model = model ?? (this.chatService.transferredSessionData?.sessionId - ? this.chatService.getOrRestoreSession(this.chatService.transferredSessionData.sessionId) - : this.chatService.startSession(AideChatAgentLocation.Panel, CancellationToken.None)); - if (!model) { - throw new Error('Could not start chat session'); - } - - this._widget.setModel(model, { ...(viewState ?? this.viewState) }); - this.viewState.sessionId = model.sessionId; - } - - override shouldShowWelcome(): boolean { - const noPersistedSessions = !this.chatService.hasSessions(); - return this.didUnregisterProvider || !this._widget?.viewModel && (noPersistedSessions || this.didProviderRegistrationFail); - } - - private getSessionId() { - let sessionId: string | undefined; - if (this.chatService.transferredSessionData) { - sessionId = this.chatService.transferredSessionData.sessionId; - this.viewState.inputValue = this.chatService.transferredSessionData.inputValue; - } else { - sessionId = this.viewState.sessionId; - } - return sessionId; - } - - protected override renderBody(parent: HTMLElement): void { - try { - super.renderBody(parent); - - const scopedInstantiationService = this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])); - const locationBasedColors = this.getLocationBasedColors(); - this._widget = this._register(scopedInstantiationService.createInstance( - ChatWidget, - AideChatAgentLocation.Panel, - { viewId: this.id }, - { supportsFileReferences: true }, - { - listForeground: SIDE_BAR_FOREGROUND, - listBackground: locationBasedColors.background, - inputEditorBackground: locationBasedColors.background, - resultEditorBackground: editorBackground - })); - this._register(this.onDidChangeBodyVisibility(visible => { - this._widget.setVisible(visible); - })); - this._register(this._widget.onDidClear(() => this.clear())); - this._widget.render(parent); - - const sessionId = this.getSessionId(); - // Render the welcome view if this session gets disposed at any point, - // including if the provider registration fails - const disposeListener = sessionId ? this._register(this.chatService.onDidDisposeSession((e) => { - if (e.reason === 'initializationFailed') { - this.didProviderRegistrationFail = true; - disposeListener?.dispose(); - this._onDidChangeViewWelcomeState.fire(); - } - })) : undefined; - const model = sessionId ? this.chatService.getOrRestoreSession(sessionId) : undefined; - - this.updateModel(model); - } catch (e) { - this.logService.error(e); - throw e; - } - } - - acceptInput(query?: string): void { - this._widget.acceptInput(query); - } - - clear(): void { - if (this.widget.viewModel) { - this.chatService.clearSession(this.widget.viewModel.sessionId); - } - this.updateModel(undefined, { ...this.viewState, inputValue: undefined }); - } - - loadSession(sessionId: string): void { - if (this.widget.viewModel) { - this.chatService.clearSession(this.widget.viewModel.sessionId); - } - - const newModel = this.chatService.getOrRestoreSession(sessionId); - this.updateModel(newModel); - } - - focusInput(): void { - this._widget.focusInput(); - } - - override focus(): void { - super.focus(); - this._widget.focusInput(); - } - - protected override layoutBody(height: number, width: number): void { - super.layoutBody(height, width); - this._widget.layout(height, width); - } - - override saveState(): void { - if (this._widget) { - // Since input history is per-provider, this is handled by a separate service and not the memento here. - // TODO multiple chat views will overwrite each other - this._widget.saveState(); - - const widgetViewState = this._widget.getViewState(); - this.viewState.inputValue = widgetViewState.inputValue; - this.viewState.inputState = widgetViewState.inputState; - this.memento.saveMemento(); - } - - super.saveState(); - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/aideChatWidget.ts b/src/vs/workbench/contrib/aideChat/browser/aideChatWidget.ts deleted file mode 100644 index 1bc1249749b..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/aideChatWidget.ts +++ /dev/null @@ -1,994 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from '../../../../base/browser/dom.js'; -import { ITreeContextMenuEvent, ITreeElement } from '../../../../base/browser/ui/tree/tree.js'; -import { disposableTimeout, timeout } from '../../../../base/common/async.js'; -import { toErrorMessage } from '../../../../base/common/errorMessage.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { Disposable, DisposableStore, IDisposable, MutableDisposable, combinedDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { extUri, isEqual } from '../../../../base/common/resources.js'; -import { isDefined } from '../../../../base/common/types.js'; -import { URI } from '../../../../base/common/uri.js'; -import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; -import { MenuId } from '../../../../platform/actions/common/actions.js'; -import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js'; -import { ITextResourceEditorInput } from '../../../../platform/editor/common/editor.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; -import { WorkbenchObjectTree } from '../../../../platform/list/browser/listService.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { IThemeService } from '../../../../platform/theme/common/themeService.js'; -import { ChatTreeItem, IAideChatAccessibilityService, IAideChatWidgetService, IChatCodeBlockInfo, IChatFileTreeInfo, IChatWidget, IChatWidgetViewContext, IChatWidgetViewOptions } from '../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { ChatAccessibilityProvider } from '../../../../workbench/contrib/aideChat/browser/aideChatAccessibilityProvider.js'; -import { ChatInputPart } from '../../../../workbench/contrib/aideChat/browser/aideChatInputPart.js'; -import { ChatListDelegate, ChatListItemRenderer, IChatRendererDelegate } from '../../../../workbench/contrib/aideChat/browser/aideChatListRenderer.js'; -import { ChatEditorOptions } from '../../../../workbench/contrib/aideChat/browser/aideChatOptions.js'; -import { AideChatAgentLocation, IAideChatAgentService, IChatAgentCommand, IChatAgentData } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { CONTEXT_CHAT_HAS_REQUESTS, CONTEXT_CHAT_INPUT_HAS_AGENT, CONTEXT_CHAT_LOCATION, CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_SESSION, CONTEXT_IN_QUICK_CHAT, CONTEXT_RESPONSE_FILTERED } from '../../../../workbench/contrib/aideChat/common/aideChatContextKeys.js'; -import { ChatModelInitState, IAideChatRequestVariableEntry, IChatModel, IChatResponseModel } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { ChatRequestAgentPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader } from '../../../../workbench/contrib/aideChat/common/aideChatParserTypes.js'; -import { ChatRequestParser } from '../../../../workbench/contrib/aideChat/common/aideChatRequestParser.js'; -import { IAideChatFollowup, IAideChatService } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { IAideChatSlashCommandService } from '../../../../workbench/contrib/aideChat/common/aideChatSlashCommands.js'; -import { ChatViewModel, IChatResponseViewModel, isRequestVM, isResponseVM, isWelcomeVM } from '../../../../workbench/contrib/aideChat/common/aideChatViewModel.js'; -import { CodeBlockModelCollection } from '../../../../workbench/contrib/aideChat/common/codeBlockModelCollection.js'; -import { IChatListItemRendererOptions } from './aideChat.js'; -import './media/chat.css'; -import './media/chatAgentHover.css'; - -const $ = dom.$; - -function revealLastElement(list: WorkbenchObjectTree) { - list.scrollTop = list.scrollHeight - list.renderHeight; -} - -export type IChatInputState = Record; -export interface IChatViewState { - inputValue?: string; - inputState?: IChatInputState; -} - -export interface IChatWidgetStyles { - listForeground: string; - listBackground: string; - inputEditorBackground: string; - resultEditorBackground: string; -} - -export interface IChatWidgetContrib extends IDisposable { - readonly id: string; - - /** - * A piece of state which is related to the input editor of the chat widget - */ - getInputState?(): any; - - onDidChangeInputState?: Event; - - /** - * Called with the result of getInputState when navigating input history. - */ - setInputState?(s: any): void; -} - -export class ChatWidget extends Disposable implements IChatWidget { - public static readonly CONTRIBS: { new(...args: [IChatWidget, ...any]): IChatWidgetContrib }[] = []; - - private readonly _onDidSubmitAgent = this._register(new Emitter<{ agent: IChatAgentData; slashCommand?: IChatAgentCommand }>()); - public readonly onDidSubmitAgent = this._onDidSubmitAgent.event; - - private _onDidFocus = this._register(new Emitter()); - readonly onDidFocus = this._onDidFocus.event; - - private _onDidChangeViewModel = this._register(new Emitter()); - readonly onDidChangeViewModel = this._onDidChangeViewModel.event; - - private _onDidScroll = this._register(new Emitter()); - readonly onDidScroll = this._onDidScroll.event; - - private _onDidClear = this._register(new Emitter()); - readonly onDidClear = this._onDidClear.event; - - private _onDidAcceptInput = this._register(new Emitter()); - readonly onDidAcceptInput = this._onDidAcceptInput.event; - - private _onDidDeleteContext = this._register(new Emitter()); - readonly onDidDeleteContext = this._onDidDeleteContext.event; - - private _onDidHide = this._register(new Emitter()); - readonly onDidHide = this._onDidHide.event; - - private _onDidChangeParsedInput = this._register(new Emitter()); - readonly onDidChangeParsedInput = this._onDidChangeParsedInput.event; - - private readonly _onWillMaybeChangeHeight = new Emitter(); - readonly onWillMaybeChangeHeight: Event = this._onWillMaybeChangeHeight.event; - - private _onDidChangeHeight = this._register(new Emitter()); - readonly onDidChangeHeight = this._onDidChangeHeight.event; - - private readonly _onDidChangeContentHeight = new Emitter(); - readonly onDidChangeContentHeight: Event = this._onDidChangeContentHeight.event; - - private contribs: ReadonlyArray = []; - - private tree!: WorkbenchObjectTree; - private renderer!: ChatListItemRenderer; - private readonly _codeBlockModelCollection: CodeBlockModelCollection; - - private inputPart!: ChatInputPart; - private editorOptions!: ChatEditorOptions; - - private listContainer!: HTMLElement; - private container!: HTMLElement; - - private bodyDimension: dom.Dimension | undefined; - private visibleChangeCount = 0; - private requestInProgress: IContextKey; - private hasRequests: IContextKey; - private agentInInput: IContextKey; - - private _visible = false; - public get visible() { - return this._visible; - } - - private previousTreeScrollHeight: number = 0; - - private readonly viewModelDisposables = this._register(new DisposableStore()); - private _viewModel: ChatViewModel | undefined; - private set viewModel(viewModel: ChatViewModel | undefined) { - if (this._viewModel === viewModel) { - return; - } - - this.viewModelDisposables.clear(); - - this._viewModel = viewModel; - if (viewModel) { - this.viewModelDisposables.add(viewModel); - } - - this._onDidChangeViewModel.fire(); - } - - get viewModel() { - return this._viewModel; - } - - private parsedChatRequest: IParsedChatRequest | undefined; - get parsedInput() { - if (this.parsedChatRequest === undefined) { - this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.viewModel!.sessionId, this.getInput(), this.location, { selectedAgent: this._lastSelectedAgent }); - - this.agentInInput.set((!!this.parsedChatRequest.parts.find(part => part instanceof ChatRequestAgentPart))); - } - - return this.parsedChatRequest; - } - - get scopedContextKeyService(): IContextKeyService { - return this.contextKeyService; - } - - constructor( - readonly location: AideChatAgentLocation, - readonly viewContext: IChatWidgetViewContext, - private readonly viewOptions: IChatWidgetViewOptions, - private readonly styles: IChatWidgetStyles, - @ICodeEditorService codeEditorService: ICodeEditorService, - @IContextKeyService private readonly contextKeyService: IContextKeyService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IAideChatService private readonly chatService: IAideChatService, - @IAideChatAgentService private readonly chatAgentService: IAideChatAgentService, - @IAideChatWidgetService chatWidgetService: IAideChatWidgetService, - @IContextMenuService private readonly contextMenuService: IContextMenuService, - @IAideChatAccessibilityService private readonly chatAccessibilityService: IAideChatAccessibilityService, - @ILogService private readonly logService: ILogService, - @IThemeService private readonly themeService: IThemeService, - @IAideChatSlashCommandService private readonly chatSlashCommandService: IAideChatSlashCommandService, - ) { - super(); - CONTEXT_IN_CHAT_SESSION.bindTo(contextKeyService).set(true); - CONTEXT_CHAT_LOCATION.bindTo(contextKeyService).set(location); - CONTEXT_IN_QUICK_CHAT.bindTo(contextKeyService).set('resource' in viewContext); - this.agentInInput = CONTEXT_CHAT_INPUT_HAS_AGENT.bindTo(contextKeyService); - this.hasRequests = CONTEXT_CHAT_HAS_REQUESTS.bindTo(contextKeyService); - this.requestInProgress = CONTEXT_CHAT_REQUEST_IN_PROGRESS.bindTo(contextKeyService); - - this._register((chatWidgetService as ChatWidgetService).register(this)); - - this._codeBlockModelCollection = this._register(instantiationService.createInstance(CodeBlockModelCollection)); - - this._register(codeEditorService.registerCodeEditorOpenHandler(async (input: ITextResourceEditorInput, _source: ICodeEditor | null, _sideBySide?: boolean): Promise => { - const resource = input.resource; - if (resource.scheme !== Schemas.vscodeChatCodeBlock) { - return null; - } - - const responseId = resource.path.split('/').at(1); - if (!responseId) { - return null; - } - - const item = this.viewModel?.getItems().find(item => item.id === responseId); - if (!item) { - return null; - } - - // TODO: needs to reveal the chat view - - this.reveal(item); - - await timeout(0); // wait for list to actually render - - for (const editor of this.renderer.editorsInUse() ?? []) { - if (extUri.isEqual(editor.uri, resource, true)) { - const inner = editor.editor; - if (input.options?.selection) { - inner.setSelection({ - startLineNumber: input.options.selection.startLineNumber, - startColumn: input.options.selection.startColumn, - endLineNumber: input.options.selection.startLineNumber ?? input.options.selection.endLineNumber, - endColumn: input.options.selection.startColumn ?? input.options.selection.endColumn - }); - } - return inner; - } - } - return null; - })); - } - - private _lastSelectedAgent: IChatAgentData | undefined; - set lastSelectedAgent(agent: IChatAgentData | undefined) { - this.parsedChatRequest = undefined; - this._lastSelectedAgent = agent; - this._onDidChangeParsedInput.fire(); - } - - get lastSelectedAgent(): IChatAgentData | undefined { - return this._lastSelectedAgent; - } - - get supportsFileReferences(): boolean { - return !!this.viewOptions.supportsFileReferences; - } - - get input(): ChatInputPart { - return this.inputPart; - } - - get inputEditor(): ICodeEditor { - return this.inputPart.inputEditor; - } - - get inputUri(): URI { - return this.inputPart.inputUri; - } - - get contentHeight(): number { - return this.inputPart.contentHeight + this.tree.contentHeight; - } - - render(parent: HTMLElement): void { - const viewId = 'viewId' in this.viewContext ? this.viewContext.viewId : undefined; - this.editorOptions = this._register(this.instantiationService.createInstance(ChatEditorOptions, viewId, this.styles.listForeground, this.styles.inputEditorBackground, this.styles.resultEditorBackground)); - const renderInputOnTop = this.viewOptions.renderInputOnTop ?? false; - const renderFollowups = this.viewOptions.renderFollowups ?? !renderInputOnTop; - const renderStyle = this.viewOptions.renderStyle; - - this.container = dom.append(parent, $('.cschat-session')); - if (renderInputOnTop) { - this.createInput(this.container, { renderFollowups, renderStyle }); - this.listContainer = dom.append(this.container, $(`.cschat-list`)); - } else { - this.listContainer = dom.append(this.container, $(`.cschat-list`)); - this.createInput(this.container, { renderFollowups, renderStyle }); - } - - this.createList(this.listContainer, { ...this.viewOptions.rendererOptions, renderStyle }); - - this._register(this.editorOptions.onDidChange(() => this.onDidStyleChange())); - this.onDidStyleChange(); - - // Do initial render - if (this.viewModel) { - this.onDidChangeItems(); - revealLastElement(this.tree); - } - - this.contribs = ChatWidget.CONTRIBS.map(contrib => { - try { - return this._register(this.instantiationService.createInstance(contrib, this)); - } catch (err) { - this.logService.error('Failed to instantiate chat widget contrib', toErrorMessage(err)); - return undefined; - } - }).filter(isDefined); - - this.contribs.forEach(c => { - if (c.onDidChangeInputState) { - this._register(c.onDidChangeInputState(() => { - const state = this.collectInputState(); - this.inputPart.updateState(state); - })); - } - }); - } - - getContrib(id: string): T | undefined { - return this.contribs.find(c => c.id === id) as T; - } - - focusInput(): void { - this.inputPart.focus(); - } - - hasInputFocus(): boolean { - return this.inputPart.hasFocus(); - } - - moveFocus(item: ChatTreeItem, type: 'next' | 'previous'): void { - if (!isResponseVM(item)) { - return; - } - const items = this.viewModel?.getItems(); - if (!items) { - return; - } - const responseItems = items.filter(i => isResponseVM(i)); - const targetIndex = responseItems.indexOf(item); - if (targetIndex === undefined) { - return; - } - const indexToFocus = type === 'next' ? targetIndex + 1 : targetIndex - 1; - if (indexToFocus < 0 || indexToFocus > responseItems.length - 1) { - return; - } - this.focus(responseItems[indexToFocus]); - } - - clear(): void { - if (this._dynamicMessageLayoutData) { - this._dynamicMessageLayoutData.enabled = true; - } - this._onDidClear.fire(); - } - - private onDidChangeItems(skipDynamicLayout?: boolean) { - if (this.tree && this._visible) { - const treeItems = (this.viewModel?.getItems() ?? []) - .map((item): ITreeElement => { - return { - element: item, - collapsed: false, - collapsible: false - }; - }); - - this._onWillMaybeChangeHeight.fire(); - - this.tree.setChildren(null, treeItems, { - diffIdentityProvider: { - getId: (element) => { - return ((isResponseVM(element) || isRequestVM(element)) ? element.dataId : element.id) + - // TODO? We can give the welcome message a proper VM or get rid of the rest of the VMs - ((isWelcomeVM(element) && this.viewModel) ? `_${ChatModelInitState[this.viewModel.initState]}` : '') + - // Ensure re-rendering an element once slash commands are loaded, so the colorization can be applied. - `${(isRequestVM(element) || isWelcomeVM(element)) /* && !!this.lastSlashCommands ? '_scLoaded' : '' */}` + - // If a response is in the process of progressive rendering, we need to ensure that it will - // be re-rendered so progressive rendering is restarted, even if the model wasn't updated. - `${isResponseVM(element) && element.renderData ? `_${this.visibleChangeCount}` : ''}` + - // Re-render once content references are loaded - (isResponseVM(element) ? `_${element.contentReferences.length}` : ''); - }, - } - }); - - if (!skipDynamicLayout && this._dynamicMessageLayoutData) { - this.layoutDynamicChatTreeItemMode(); - } - - const lastItem = treeItems[treeItems.length - 1]?.element; - if (lastItem && isResponseVM(lastItem) && lastItem.isComplete) { - this.renderFollowups(lastItem.replyFollowups, lastItem); - } else if (lastItem && isWelcomeVM(lastItem)) { - this.renderFollowups(lastItem.sampleQuestions); - } else { - this.renderFollowups(undefined); - } - } - } - - private async renderFollowups(items: IAideChatFollowup[] | undefined, response?: IChatResponseViewModel): Promise { - this.inputPart.renderFollowups(items, response); - - if (this.bodyDimension) { - this.layout(this.bodyDimension.height, this.bodyDimension.width); - } - } - - setVisible(visible: boolean): void { - const wasVisible = this._visible; - this._visible = visible; - this.visibleChangeCount++; - this.renderer.setVisible(visible); - this.input.setVisible(visible); - - if (visible) { - this._register(disposableTimeout(() => { - // Progressive rendering paused while hidden, so start it up again. - // Do it after a timeout because the container is not visible yet (it should be but offsetHeight returns 0 here) - if (this._visible) { - this.onDidChangeItems(true); - } - }, 0)); - } else if (wasVisible) { - this._onDidHide.fire(); - } - } - - private createList(listContainer: HTMLElement, options: IChatListItemRendererOptions): void { - const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService]))); - const delegate = scopedInstantiationService.createInstance(ChatListDelegate, this.viewOptions.defaultElementHeight ?? 200); - const rendererDelegate: IChatRendererDelegate = { - getListLength: () => this.tree.getNode(null).visibleChildrenCount, - onDidScroll: this.onDidScroll, - }; - - // Create a dom element to hold UI from editor widgets embedded in chat messages - const overflowWidgetsContainer = document.createElement('div'); - overflowWidgetsContainer.classList.add('chat-overflow-widget-container', 'monaco-editor'); - listContainer.append(overflowWidgetsContainer); - - this.renderer = this._register(scopedInstantiationService.createInstance( - ChatListItemRenderer, - this.editorOptions, - this.location, - options, - rendererDelegate, - this._codeBlockModelCollection, - overflowWidgetsContainer, - )); - this._register(this.renderer.onDidClickFollowup(item => { - // is this used anymore? - this.acceptInput(item.message); - })); - this._register(this.renderer.onDidClickRerunWithAgentOrCommandDetection(item => { - const request = this.chatService.getSession(item.sessionId)?.getRequests().find(candidate => candidate.id === item.requestId); - if (request) { - this.chatService.resendRequest(request, { noCommandDetection: true, attempt: request.attempt, location: this.location }).catch(e => this.logService.error('FAILED to rerun request', e)); - } - })); - - this.tree = >scopedInstantiationService.createInstance( - WorkbenchObjectTree, - 'Aide', - listContainer, - delegate, - [this.renderer], - { - identityProvider: { getId: (e: ChatTreeItem) => e.id }, - horizontalScrolling: false, - alwaysConsumeMouseWheel: false, - supportDynamicHeights: true, - hideTwistiesOfChildlessElements: true, - accessibilityProvider: this.instantiationService.createInstance(ChatAccessibilityProvider), - keyboardNavigationLabelProvider: { getKeyboardNavigationLabel: (e: ChatTreeItem) => isRequestVM(e) ? e.message : isResponseVM(e) ? e.response.value : '' }, // TODO - setRowLineHeight: false, - filter: this.viewOptions.filter ? { filter: this.viewOptions.filter.bind(this.viewOptions), } : undefined, - overrideStyles: { - listFocusBackground: this.styles.listBackground, - listInactiveFocusBackground: this.styles.listBackground, - listActiveSelectionBackground: this.styles.listBackground, - listFocusAndSelectionBackground: this.styles.listBackground, - listInactiveSelectionBackground: this.styles.listBackground, - listHoverBackground: this.styles.listBackground, - listBackground: this.styles.listBackground, - listFocusForeground: this.styles.listForeground, - listHoverForeground: this.styles.listForeground, - listInactiveFocusForeground: this.styles.listForeground, - listInactiveSelectionForeground: this.styles.listForeground, - listActiveSelectionForeground: this.styles.listForeground, - listFocusAndSelectionForeground: this.styles.listForeground, - } - }); - this._register(this.tree.onContextMenu(e => this.onContextMenu(e))); - - this._register(this.tree.onDidChangeContentHeight(() => { - this.onDidChangeTreeContentHeight(); - })); - this._register(this.renderer.onDidChangeItemHeight(e => { - this.tree.updateElementHeight(e.element, e.height); - })); - this._register(this.tree.onDidFocus(() => { - this._onDidFocus.fire(); - })); - this._register(this.tree.onDidScroll(() => { - this._onDidScroll.fire(); - })); - } - - private onContextMenu(e: ITreeContextMenuEvent): void { - e.browserEvent.preventDefault(); - e.browserEvent.stopPropagation(); - - const selected = e.element; - const scopedContextKeyService = this.contextKeyService.createOverlay([ - [CONTEXT_RESPONSE_FILTERED.key, isResponseVM(selected) && !!selected.errorDetails?.responseIsFiltered] - ]); - this.contextMenuService.showContextMenu({ - menuId: MenuId.AideChatContext, - menuActionOptions: { shouldForwardArgs: true }, - contextKeyService: scopedContextKeyService, - getAnchor: () => e.anchor, - getActionsContext: () => selected, - }); - } - - private onDidChangeTreeContentHeight(): void { - if (this.tree.scrollHeight !== this.previousTreeScrollHeight) { - // Due to rounding, the scrollTop + renderHeight will not exactly match the scrollHeight. - // Consider the tree to be scrolled all the way down if it is within 2px of the bottom. - const lastElementWasVisible = this.tree.scrollTop + this.tree.renderHeight >= this.previousTreeScrollHeight - 2; - if (lastElementWasVisible) { - dom.scheduleAtNextAnimationFrame(dom.getWindow(this.listContainer), () => { - // Can't set scrollTop during this event listener, the list might overwrite the change - revealLastElement(this.tree); - }, 0); - } - } - - this.previousTreeScrollHeight = this.tree.scrollHeight; - this._onDidChangeContentHeight.fire(); - } - - private createInput(container: HTMLElement, options?: { renderFollowups: boolean; renderStyle?: 'default' | 'compact' | 'minimal' }): void { - this.inputPart = this._register(this.instantiationService.createInstance(ChatInputPart, - this.location, - { - renderFollowups: options?.renderFollowups ?? true, - renderStyle: options?.renderStyle === 'minimal' ? 'compact' : options?.renderStyle, - menus: { executeToolbar: MenuId.AideChatExecute, primaryToolbar: MenuId.AideChatExecutePrimary, ...this.viewOptions.menus }, - editorOverflowWidgetsDomNode: this.viewOptions.editorOverflowWidgetsDomNode, - } - )); - this.inputPart.render(container, '', this); - - this._register(this.inputPart.onDidLoadInputState(state => { - this.contribs.forEach(c => { - if (c.setInputState) { - const contribState = (typeof state === 'object' && state?.[c.id]) ?? {}; - c.setInputState(contribState); - } - }); - })); - this._register(this.inputPart.onDidFocus(() => this._onDidFocus.fire())); - this._register(this.inputPart.onDidDeleteContext((e) => this._onDidDeleteContext.fire(e))); - this._register(this.inputPart.onDidAcceptFollowup(e => { - if (!this.viewModel) { - return; - } - - let msg = ''; - if (e.followup.agentId && e.followup.agentId !== this.chatAgentService.getDefaultAgent(this.location)?.id) { - const agent = this.chatAgentService.getAgent(e.followup.agentId); - if (!agent) { - return; - } - - this.lastSelectedAgent = agent; - msg = `${chatAgentLeader}${agent.name} `; - if (e.followup.subCommand) { - msg += `${chatSubcommandLeader}${e.followup.subCommand} `; - } - } else if (!e.followup.agentId && e.followup.subCommand && this.chatSlashCommandService.hasCommand(e.followup.subCommand)) { - msg = `${chatSubcommandLeader}${e.followup.subCommand} `; - } - - msg += e.followup.message; - this.acceptInput(msg); - - if (!e.response) { - // Followups can be shown by the welcome message, then there is no response associated. - // At some point we probably want telemetry for these too. - return; - } - - this.chatService.notifyUserAction({ - sessionId: this.viewModel.sessionId, - requestId: e.response.requestId, - agentId: e.response.agent?.id, - result: e.response.result, - action: { - kind: 'followUp', - followup: e.followup - }, - }); - })); - this._register(this.inputPart.onDidChangeHeight(() => { - if (this.bodyDimension) { - this.layout(this.bodyDimension.height, this.bodyDimension.width); - } - this._onDidChangeContentHeight.fire(); - })); - this._register(this.inputEditor.onDidChangeModelContent(() => this.parsedChatRequest = undefined)); - this._register(this.chatAgentService.onDidChangeAgents(() => this.parsedChatRequest = undefined)); - } - - private onDidStyleChange(): void { - this.container.style.setProperty('--vscode-interactive-result-editor-background-color', this.editorOptions.configuration.resultEditor.backgroundColor?.toString() ?? ''); - this.container.style.setProperty('--vscode-interactive-session-foreground', this.editorOptions.configuration.foreground?.toString() ?? ''); - this.container.style.setProperty('--vscode-chat-list-background', this.themeService.getColorTheme().getColor(this.styles.listBackground)?.toString() ?? ''); - } - - setModel(model: IChatModel, viewState: IChatViewState): void { - if (!this.container) { - throw new Error('Call render() before setModel()'); - } - - this._register(model.onDidChange(e => { - if (e.kind === 'initialize') { - this.inputPart.updateState(viewState.inputValue ?? ''); - } - })); - - this._codeBlockModelCollection.clear(); - - this.container.setAttribute('data-session-id', model.sessionId); - this.viewModel = this.instantiationService.createInstance(ChatViewModel, model, this._codeBlockModelCollection); - this.viewModelDisposables.add(Event.accumulate(this.viewModel.onDidChange, 0)(events => { - if (!this.viewModel) { - return; - } - - this.requestInProgress.set(this.viewModel.requestInProgress); - if (this.viewModel.getItems().length > 1) { - this.hasRequests.set(true); - } else { - this.hasRequests.set(false); - } - - this.onDidChangeItems(); - if (events.some(e => e?.kind === 'addRequest') && this.visible) { - revealLastElement(this.tree); - this.focusInput(); - } - })); - this.viewModelDisposables.add(this.viewModel.onDidDisposeModel(() => { - // Ensure that view state is saved here, because we will load it again when a new model is assigned - this.inputPart.saveState(); - - // Disposes the viewmodel and listeners - this.viewModel = undefined; - this.onDidChangeItems(); - })); - this.inputPart.initForNewChatModel(viewState.inputValue, viewState.inputState ?? this.collectInputState()); - this.contribs.forEach(c => { - if (c.setInputState && viewState.inputState?.[c.id]) { - c.setInputState(viewState.inputState?.[c.id]); - } - }); - - if (this.tree) { - this.onDidChangeItems(); - revealLastElement(this.tree); - } - - } - - getFocus(): ChatTreeItem | undefined { - return this.tree.getFocus()[0] ?? undefined; - } - - reveal(item: ChatTreeItem): void { - this.tree.reveal(item); - } - - focus(item: ChatTreeItem): void { - const items = this.tree.getNode(null).children; - const node = items.find(i => i.element?.id === item.id); - if (!node) { - return; - } - - this.tree.setFocus([node.element]); - this.tree.domFocus(); - } - - refilter() { - this.tree.refilter(); - } - - setInputPlaceholder(placeholder: string): void { - this.viewModel?.setInputPlaceholder(placeholder); - } - - resetInputPlaceholder(): void { - this.viewModel?.resetInputPlaceholder(); - } - - setInput(value = ''): void { - this.inputPart.setValue(value, false); - } - - getInput(): string { - return this.inputPart.inputEditor.getValue(); - } - - logInputHistory(): void { - this.inputPart.logInputHistory(); - } - - async acceptInput(query?: string): Promise { - return this._acceptInput(query ? { query } : undefined); - } - - async acceptInputWithPrefix(prefix: string): Promise { - this._acceptInput({ prefix }); - } - - private collectInputState(): IChatInputState { - const inputState: IChatInputState = {}; - this.contribs.forEach(c => { - if (c.getInputState) { - inputState[c.id] = c.getInputState(); - } - }); - return inputState; - } - - private async _acceptInput(opts: { query: string } | { prefix: string } | undefined): Promise { - if (this.viewModel) { - this._onDidAcceptInput.fire(); - - const editorValue = this.getInput(); - const requestId = this.chatAccessibilityService.acceptRequest(); - const input = !opts ? editorValue : - 'query' in opts ? opts.query : - `${opts.prefix} ${editorValue}`; - const isUserQuery = !opts || 'prefix' in opts; - const result = await this.chatService.sendRequest(this.viewModel.sessionId, input, { location: this.location, parserContext: { selectedAgent: this._lastSelectedAgent }, attachedContext: [...this.inputPart.attachedContext.values()] }); - - if (result) { - this.inputPart.attachedContext.clear(); - this.inputPart.acceptInput(isUserQuery); - this._onDidSubmitAgent.fire({ agent: result.agent, slashCommand: result.slashCommand }); - this.inputPart.updateState(this.collectInputState()); - result.responseCompletePromise.then(() => { - const responses = this.viewModel?.getItems().filter(isResponseVM); - const lastResponse = responses?.[responses.length - 1]; - this.chatAccessibilityService.acceptResponse(lastResponse, requestId); - }); - return result.responseCreatedPromise; - } - } - return undefined; - } - - - setContext(overwrite: boolean, ...contentReferences: IAideChatRequestVariableEntry[]) { - if (overwrite) { - this.inputPart.attachedContext.clear(); - } - this.inputPart.attachContext(...contentReferences); - - if (this.bodyDimension) { - this.layout(this.bodyDimension.height, this.bodyDimension.width); - } - } - - getCodeBlockInfosForResponse(response: IChatResponseViewModel): IChatCodeBlockInfo[] { - return this.renderer.getCodeBlockInfosForResponse(response); - } - - getCodeBlockInfoForEditor(uri: URI): IChatCodeBlockInfo | undefined { - return this.renderer.getCodeBlockInfoForEditor(uri); - } - - getFileTreeInfosForResponse(response: IChatResponseViewModel): IChatFileTreeInfo[] { - return this.renderer.getFileTreeInfosForResponse(response); - } - - getLastFocusedFileTreeForResponse(response: IChatResponseViewModel): IChatFileTreeInfo | undefined { - return this.renderer.getLastFocusedFileTreeForResponse(response); - } - - focusLastMessage(): void { - if (!this.viewModel) { - return; - } - - const items = this.tree.getNode(null).children; - const lastItem = items[items.length - 1]; - if (!lastItem) { - return; - } - - this.tree.setFocus([lastItem.element]); - this.tree.domFocus(); - } - - layout(height: number, width: number): void { - width = Math.min(width, 850); - this.bodyDimension = new dom.Dimension(width, height); - - this.inputPart.layout(height, width); - const inputPartHeight = this.inputPart.inputPartHeight; - const lastElementVisible = this.tree.scrollTop + this.tree.renderHeight >= this.tree.scrollHeight; - - const listHeight = height - inputPartHeight; - - this.tree.layout(listHeight, width); - this.tree.getHTMLElement().style.height = `${listHeight}px`; - this.renderer.layout(width); - if (lastElementVisible) { - revealLastElement(this.tree); - } - - this.listContainer.style.height = `${height - inputPartHeight}px`; - - this._onDidChangeHeight.fire(height); - } - - private _dynamicMessageLayoutData?: { numOfMessages: number; maxHeight: number; enabled: boolean }; - - // An alternative to layout, this allows you to specify the number of ChatTreeItems - // you want to show, and the max height of the container. It will then layout the - // tree to show that many items. - // TODO@TylerLeonhardt: This could use some refactoring to make it clear which layout strategy is being used - setDynamicChatTreeItemLayout(numOfChatTreeItems: number, maxHeight: number) { - this._dynamicMessageLayoutData = { numOfMessages: numOfChatTreeItems, maxHeight, enabled: true }; - this._register(this.renderer.onDidChangeItemHeight(() => this.layoutDynamicChatTreeItemMode())); - - const mutableDisposable = this._register(new MutableDisposable()); - this._register(this.tree.onDidScroll((e) => { - // TODO@TylerLeonhardt this should probably just be disposed when this is disabled - // and then set up again when it is enabled again - if (!this._dynamicMessageLayoutData?.enabled) { - return; - } - mutableDisposable.value = dom.scheduleAtNextAnimationFrame(dom.getWindow(this.listContainer), () => { - if (!e.scrollTopChanged || e.heightChanged || e.scrollHeightChanged) { - return; - } - const renderHeight = e.height; - const diff = e.scrollHeight - renderHeight - e.scrollTop; - if (diff === 0) { - return; - } - - const possibleMaxHeight = (this._dynamicMessageLayoutData?.maxHeight ?? maxHeight); - const width = this.bodyDimension?.width ?? this.container.offsetWidth; - this.inputPart.layout(possibleMaxHeight, width); - const inputPartHeight = this.inputPart.inputPartHeight; - const newHeight = Math.min(renderHeight + diff, possibleMaxHeight - inputPartHeight); - this.layout(newHeight + inputPartHeight, width); - }); - })); - } - - updateDynamicChatTreeItemLayout(numOfChatTreeItems: number, maxHeight: number) { - this._dynamicMessageLayoutData = { numOfMessages: numOfChatTreeItems, maxHeight, enabled: true }; - let hasChanged = false; - let height = this.bodyDimension!.height; - let width = this.bodyDimension!.width; - if (maxHeight < this.bodyDimension!.height) { - height = maxHeight; - hasChanged = true; - } - const containerWidth = this.container.offsetWidth; - if (this.bodyDimension?.width !== containerWidth) { - width = containerWidth; - hasChanged = true; - } - if (hasChanged) { - this.layout(height, width); - } - } - - get isDynamicChatTreeItemLayoutEnabled(): boolean { - return this._dynamicMessageLayoutData?.enabled ?? false; - } - - set isDynamicChatTreeItemLayoutEnabled(value: boolean) { - if (!this._dynamicMessageLayoutData) { - return; - } - this._dynamicMessageLayoutData.enabled = value; - } - - layoutDynamicChatTreeItemMode(): void { - if (!this.viewModel || !this._dynamicMessageLayoutData?.enabled) { - return; - } - - const width = this.bodyDimension?.width ?? this.container.offsetWidth; - this.inputPart.layout(this._dynamicMessageLayoutData.maxHeight, width); - const inputHeight = this.inputPart.inputPartHeight; - - const totalMessages = this.viewModel.getItems(); - // grab the last N messages - const messages = totalMessages.slice(-this._dynamicMessageLayoutData.numOfMessages); - - const needsRerender = messages.some(m => m.currentRenderedHeight === undefined); - const listHeight = needsRerender - ? this._dynamicMessageLayoutData.maxHeight - : messages.reduce((acc, message) => acc + message.currentRenderedHeight!, 0); - - this.layout( - Math.min( - // we add an additional 18px in order to show that there is scrollable content - inputHeight + listHeight + (totalMessages.length > 2 ? 18 : 0), - this._dynamicMessageLayoutData.maxHeight - ), - width - ); - - if (needsRerender || !listHeight) { - // TODO: figure out a better place to reveal the last element - revealLastElement(this.tree); - } - } - - saveState(): void { - this.inputPart.saveState(); - } - - getViewState(): IChatViewState { - this.inputPart.saveState(); - return { inputValue: this.getInput(), inputState: this.collectInputState() }; - } - - -} - -export class ChatWidgetService implements IAideChatWidgetService { - - declare readonly _serviceBrand: undefined; - - private _widgets: ChatWidget[] = []; - private _lastFocusedWidget: ChatWidget | undefined = undefined; - - get lastFocusedWidget(): ChatWidget | undefined { - return this._lastFocusedWidget; - } - - constructor() { } - - getWidgetByInputUri(uri: URI): ChatWidget | undefined { - return this._widgets.find(w => isEqual(w.inputUri, uri)); - } - - getWidgetBySessionId(sessionId: string): ChatWidget | undefined { - return this._widgets.find(w => w.viewModel?.sessionId === sessionId); - } - - private setLastFocusedWidget(widget: ChatWidget | undefined): void { - if (widget === this._lastFocusedWidget) { - return; - } - - this._lastFocusedWidget = widget; - } - - register(newWidget: ChatWidget): IDisposable { - if (this._widgets.some(widget => widget === newWidget)) { - throw new Error('Cannot register the same widget multiple times'); - } - - this._widgets.push(newWidget); - - return combinedDisposable( - newWidget.onDidFocus(() => this.setLastFocusedWidget(newWidget)), - toDisposable(() => this._widgets.splice(this._widgets.indexOf(newWidget), 1)) - ); - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/aideKeybindingPill.ts b/src/vs/workbench/contrib/aideChat/browser/aideKeybindingPill.ts deleted file mode 100644 index 588726406bf..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/aideKeybindingPill.ts +++ /dev/null @@ -1,120 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from '../../../../base/browser/dom.js'; -import { Button } from '../../../../base/browser/ui/button/button.js'; -import { Event } from '../../../../base/common/event.js'; -import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; -import { IPosition } from '../../../../editor/common/core/position.js'; -import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; -import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js'; - -export const addContextCommandId = 'workbench.action.aideChat.addContext'; - -export class KeybindingPillWidget extends Disposable implements IContentWidget { - public static readonly ID = 'editor.contrib.keybindingPillWidget'; - - private readonly _toDispose = new DisposableStore(); - private readonly _domNode: HTMLElement; - - private isVisible: boolean = false; - private addContextButton: Button | undefined; - - private addContextLabel?: string; - - constructor( - private readonly _editor: ICodeEditor, - @IKeybindingService keybindingService: IKeybindingService - ) { - super(); - - this._domNode = dom.$('div.keybindingPillWidget'); - this.renderButtons(); - this.updateLabels(keybindingService); - - this._register(Event.runAndSubscribe(keybindingService.onDidUpdateKeybindings, () => { - this.updateLabels(keybindingService); - })); - } - - private renderButtons() { - this.addContextButton = new Button(this._domNode, defaultButtonStyles); - this._toDispose.add(this.addContextButton); - this._toDispose.add(this.addContextButton.onDidClick(() => { - this._editor.trigger('keyboard', addContextCommandId, null); - this.hide(); - })); - - this.addContextButton.enabled = true; - this.addContextButton.element.classList.add('keybinding-pill'); - } - - private updateLabels(keybindingService: IKeybindingService) { - this.addContextLabel = `${keybindingService.lookupKeybinding(addContextCommandId)?.getLabel() ?? ''} Add to chat`; - if (this.addContextButton) { - this.addContextButton.label = this.addContextLabel; - } - } - - override dispose(): void { - super.dispose(); - this._toDispose.dispose(); - - this.addContextButton = undefined; - - this._editor.removeContentWidget(this); - } - - getId(): string { - return 'KeybindingPillWidget'; - } - - getDomNode(): HTMLElement { - return this._domNode; - } - - getPosition(): IContentWidgetPosition | null { - const selection = this._editor.getSelection(); - if (!selection) { - return null; - } - const selectionStartLine = selection.selectionStartLineNumber; - if (selection.startLineNumber === selectionStartLine) { - // this is from a top-to-bottom-selection - // so we want to show the widget on the top - return { - position: { - lineNumber: selectionStartLine, - column: selection.startColumn, - }, - preference: [ContentWidgetPositionPreference.ABOVE] - }; - } else { - // this is from bottom-to-top selection - // so we want to return the position to the bottom - return { - position: { - lineNumber: selection.endLineNumber, - column: selection.endColumn, - }, - preference: [ContentWidgetPositionPreference.BELOW], - }; - } - } - - showAt(position: IPosition) { - if (this.isVisible) { - this._editor.layoutContentWidget(this); - } else { - this._editor.addContentWidget(this); - } - } - - hide() { - this._editor.removeContentWidget(this); - this.isVisible = false; - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatCollections.ts b/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatCollections.ts deleted file mode 100644 index 4432e44201a..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatCollections.ts +++ /dev/null @@ -1,43 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IDisposable, Disposable } from '../../../../../base/common/lifecycle.js'; - -export class ResourcePool extends Disposable { - private readonly pool: T[] = []; - - private _inUse = new Set; - public get inUse(): ReadonlySet { - return this._inUse; - } - - constructor( - private readonly _itemFactory: () => T, - ) { - super(); - } - - get(): T { - if (this.pool.length > 0) { - const item = this.pool.pop()!; - this._inUse.add(item); - return item; - } - - const item = this._register(this._itemFactory()); - this._inUse.add(item); - return item; - } - - release(item: T): void { - this._inUse.delete(item); - this.pool.push(item); - } -} - -export interface IDisposableReference extends IDisposable { - object: T; - isStale: () => boolean; -} diff --git a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatCommandContentPart.ts b/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatCommandContentPart.ts deleted file mode 100644 index 9b00f3a8fd0..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatCommandContentPart.ts +++ /dev/null @@ -1,46 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from '../../../../../base/browser/dom.js'; -import { Button } from '../../../../../base/browser/ui/button/button.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { localize } from '../../../../../nls.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; -import { IChatContentPart, IChatContentPartRenderContext } from '../../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatContentParts.js'; -import { IChatProgressRenderableResponseContent } from '../../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { IAideChatCommandButton } from '../../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { isResponseVM } from '../../../../../workbench/contrib/aideChat/common/aideChatViewModel.js'; - -const $ = dom.$; - -export class ChatCommandButtonContentPart extends Disposable implements IChatContentPart { - public readonly domNode: HTMLElement; - - constructor( - commandButton: IAideChatCommandButton, - context: IChatContentPartRenderContext, - @ICommandService private readonly commandService: ICommandService - ) { - super(); - - this.domNode = $('.chat-command-button'); - const enabled = !isResponseVM(context.element) || !context.element.isStale; - const tooltip = enabled ? - commandButton.command.tooltip : - localize('commandButtonDisabled', "Button not available in restored chat"); - const button = this._register(new Button(this.domNode, { ...defaultButtonStyles, supportIcons: true, title: tooltip })); - button.label = commandButton.command.title; - button.enabled = enabled; - - // TODO still need telemetry for command buttons - this._register(button.onDidClick(() => this.commandService.executeCommand(commandButton.command.id, ...(commandButton.command.arguments ?? [])))); - } - - hasSameContent(other: IChatProgressRenderableResponseContent): boolean { - // No other change allowed for this content type - return other.kind === 'command'; - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatConfirmationContentPart.ts b/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatConfirmationContentPart.ts deleted file mode 100644 index 0a1b73e301c..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatConfirmationContentPart.ts +++ /dev/null @@ -1,64 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Emitter } from '../../../../../base/common/event.js'; -import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; -import { localize } from '../../../../../nls.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { ChatConfirmationWidget } from '../../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatConfirmationWidget.js'; -import { IChatContentPart, IChatContentPartRenderContext } from '../../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatContentParts.js'; -import { IChatProgressRenderableResponseContent } from '../../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { IAideChatConfirmation, IChatSendRequestOptions, IAideChatService } from '../../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { isResponseVM } from '../../../../../workbench/contrib/aideChat/common/aideChatViewModel.js'; - -export class ChatConfirmationContentPart extends Disposable implements IChatContentPart { - public readonly domNode: HTMLElement; - - private readonly _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; - - constructor( - confirmation: IAideChatConfirmation, - context: IChatContentPartRenderContext, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IAideChatService private readonly chatService: IAideChatService, - ) { - super(); - - const element = context.element; - const confirmationWidget = this._register(this.instantiationService.createInstance(ChatConfirmationWidget, confirmation.title, confirmation.message, [ - { label: localize('accept', "Accept"), data: confirmation.data }, - { label: localize('dismiss', "Dismiss"), data: confirmation.data, isSecondary: true }, - ])); - confirmationWidget.setShowButtons(!confirmation.isUsed); - - this._register(confirmationWidget.onDidClick(async e => { - if (isResponseVM(element)) { - const prompt = `${e.label}: "${confirmation.title}"`; - const data: IChatSendRequestOptions = e.isSecondary ? - { rejectedConfirmationData: [e.data] } : - { acceptedConfirmationData: [e.data] }; - data.agentId = element.agent?.id; - data.slashCommand = element.slashCommand?.name; - if (await this.chatService.sendRequest(element.sessionId, prompt, data)) { - confirmation.isUsed = true; - confirmationWidget.setShowButtons(false); - this._onDidChangeHeight.fire(); - } - } - })); - - this.domNode = confirmationWidget.domNode; - } - - hasSameContent(other: IChatProgressRenderableResponseContent): boolean { - // No other change allowed for this content type - return other.kind === 'confirmation'; - } - - addDisposable(disposable: IDisposable): void { - this._register(disposable); - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatConfirmationWidget.ts b/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatConfirmationWidget.ts deleted file mode 100644 index 182e5fa408d..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatConfirmationWidget.ts +++ /dev/null @@ -1,63 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from '../../../../../base/browser/dom.js'; -import './media/chatConfirmationWidget.css'; -import { Button } from '../../../../../base/browser/ui/button/button.js'; -import { Emitter, Event } from '../../../../../base/common/event.js'; -import { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { defaultButtonStyles } from '../../../../../platform/theme/browser/defaultStyles.js'; - -export interface IChatConfirmationButton { - label: string; - isSecondary?: boolean; - data: any; -} - -export class ChatConfirmationWidget extends Disposable { - private _onDidClick = this._register(new Emitter()); - get onDidClick(): Event { return this._onDidClick.event; } - - private _domNode: HTMLElement; - get domNode(): HTMLElement { - return this._domNode; - } - - setShowButtons(showButton: boolean): void { - this.domNode.classList.toggle('hideButtons', !showButton); - } - - constructor( - title: string, - message: string, - buttons: IChatConfirmationButton[], - @IInstantiationService private readonly instantiationService: IInstantiationService, - ) { - super(); - - const elements = dom.h('.chat-confirmation-widget@root', [ - dom.h('.chat-confirmation-widget-title@title'), - dom.h('.chat-confirmation-widget-message@message'), - dom.h('.chat-confirmation-buttons-container@buttonsContainer'), - ]); - this._domNode = elements.root; - const renderer = this._register(this.instantiationService.createInstance(MarkdownRenderer, {})); - - const renderedTitle = this._register(renderer.render(new MarkdownString(title))); - elements.title.appendChild(renderedTitle.element); - - const renderedMessage = this._register(renderer.render(new MarkdownString(message))); - elements.message.appendChild(renderedMessage.element); - - buttons.forEach(buttonData => { - const button = new Button(elements.buttonsContainer, { ...defaultButtonStyles, secondary: buttonData.isSecondary }); - button.label = buttonData.label; - this._register(button.onDidClick(() => this._onDidClick.fire(buttonData))); - }); - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatContentParts.ts b/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatContentParts.ts deleted file mode 100644 index 9deda992c5d..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatContentParts.ts +++ /dev/null @@ -1,26 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IDisposable } from '../../../../../base/common/lifecycle.js'; -import { ChatTreeItem } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { IChatRendererContent } from '../../../../../workbench/contrib/aideChat/common/aideChatViewModel.js'; - -export interface IChatContentPart extends IDisposable { - domNode: HTMLElement; - - /** - * Returns true if the other content is equivalent to what is already rendered in this content part. - * Returns false if a rerender is needed. - * followingContent is all the content that will be rendered after this content part (to support progress messages' behavior). - */ - hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean; -} - -export interface IChatContentPartRenderContext { - element: ChatTreeItem; - index: number; - content: ReadonlyArray; - preceedingContentParts: ReadonlyArray; -} diff --git a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatMarkdownContentPart.ts b/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatMarkdownContentPart.ts deleted file mode 100644 index d38ae263d27..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatMarkdownContentPart.ts +++ /dev/null @@ -1,176 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from '../../../../../base/browser/dom.js'; -import { Emitter } from '../../../../../base/common/event.js'; -import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; -import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; -import { equalsIgnoreCase } from '../../../../../base/common/strings.js'; -import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; -import { Range } from '../../../../../editor/common/core/range.js'; -import { IResolvedTextEditorModel, ITextModelService } from '../../../../../editor/common/services/resolverService.js'; -import { MenuId } from '../../../../../platform/actions/common/actions.js'; -import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IChatCodeBlockInfo, IChatListItemRendererOptions } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { IDisposableReference, ResourcePool } from '../../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatCollections.js'; -import { IChatContentPart, IChatContentPartRenderContext } from '../../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatContentParts.js'; -import { IChatRendererDelegate } from '../../../../../workbench/contrib/aideChat/browser/aideChatListRenderer.js'; -import { ChatMarkdownDecorationsRenderer } from '../../../../../workbench/contrib/aideChat/browser/aideChatMarkdownDecorationsRenderer.js'; -import { ChatEditorOptions } from '../../../../../workbench/contrib/aideChat/browser/aideChatOptions.js'; -import { CodeBlockPart, ICodeBlockData, localFileLanguageId, parseLocalFileData } from '../../../../../workbench/contrib/aideChat/browser/codeBlockPart.js'; -import { IMarkdownVulnerability } from '../../../../../workbench/contrib/aideChat/common/annotations.js'; -import { IChatProgressRenderableResponseContent } from '../../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { isRequestVM, isResponseVM } from '../../../../../workbench/contrib/aideChat/common/aideChatViewModel.js'; -import { CodeBlockModelCollection } from '../../../../../workbench/contrib/aideChat/common/codeBlockModelCollection.js'; - -const $ = dom.$; - -export class ChatMarkdownContentPart extends Disposable implements IChatContentPart { - public readonly domNode: HTMLElement; - private readonly allRefs: IDisposableReference[] = []; - - private readonly _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; - - public readonly codeblocks: IChatCodeBlockInfo[] = []; - - constructor( - private readonly markdown: IMarkdownString, - context: IChatContentPartRenderContext, - private readonly editorPool: EditorPool, - fillInIncompleteTokens = false, - codeBlockStartIndex = 0, - renderer: MarkdownRenderer, - currentWidth: number, - private readonly codeBlockModelCollection: CodeBlockModelCollection, - rendererOptions: IChatListItemRendererOptions, - @IContextKeyService contextKeyService: IContextKeyService, - @ITextModelService private readonly textModelService: ITextModelService, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super(); - - const element = context.element; - const markdownDecorationsRenderer = instantiationService.createInstance(ChatMarkdownDecorationsRenderer); - - // We release editors in order so that it's more likely that the same editor will be assigned if this element is re-rendered right away, like it often is during progressive rendering - const orderedDisposablesList: IDisposable[] = []; - let codeBlockIndex = codeBlockStartIndex; - const result = this._register(renderer.render(markdown, { - fillInIncompleteTokens, - codeBlockRendererSync: (languageId, text) => { - const index = codeBlockIndex++; - let textModel: Promise; - let range: Range | undefined; - let vulns: readonly IMarkdownVulnerability[] | undefined; - if (equalsIgnoreCase(languageId, localFileLanguageId)) { - try { - const parsedBody = parseLocalFileData(text); - range = parsedBody.range && Range.lift(parsedBody.range); - textModel = this.textModelService.createModelReference(parsedBody.uri).then(ref => ref.object); - } catch (e) { - return $('div'); - } - } else { - if (!isRequestVM(element) && !isResponseVM(element)) { - console.error('Trying to render code block in welcome', element.id, index); - return $('div'); - } - - const sessionId = isResponseVM(element) || isRequestVM(element) ? element.sessionId : ''; - const modelEntry = this.codeBlockModelCollection.getOrCreate(sessionId, element, index); - vulns = modelEntry.vulns; - textModel = modelEntry.model; - } - - const hideToolbar = isResponseVM(element) && element.errorDetails?.responseIsFiltered; - const ref = this.renderCodeBlock({ languageId, textModel, codeBlockIndex: index, element, range, hideToolbar, parentContextKeyService: contextKeyService, vulns }, text, currentWidth, rendererOptions.editableCodeBlock); - this.allRefs.push(ref); - - // Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping) - // not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render) - this._register(ref.object.onDidChangeContentHeight(() => this._onDidChangeHeight.fire())); - - const info: IChatCodeBlockInfo = { - codeBlockIndex: index, - element, - focus() { - ref.object.focus(); - }, - uri: ref.object.uri - }; - this.codeblocks.push(info); - orderedDisposablesList.push(ref); - return ref.object.element; - }, - asyncRenderCallback: () => this._onDidChangeHeight.fire(), - })); - - this._register(markdownDecorationsRenderer.walkTreeAndAnnotateReferenceLinks(result.element)); - - orderedDisposablesList.reverse().forEach(d => this._register(d)); - this.domNode = result.element; - } - - private renderCodeBlock(data: ICodeBlockData, text: string, currentWidth: number, editableCodeBlock: boolean | undefined): IDisposableReference { - const ref = this.editorPool.get(); - const editorInfo = ref.object; - if (isResponseVM(data.element)) { - this.codeBlockModelCollection.update(data.element.sessionId, data.element, data.codeBlockIndex, { text, languageId: data.languageId }); - } - - editorInfo.render(data, currentWidth, editableCodeBlock); - - return ref; - } - - hasSameContent(other: IChatProgressRenderableResponseContent): boolean { - return other.kind === 'markdownContent' && other.content.value === this.markdown.value; - } - - layout(width: number): void { - this.allRefs.forEach(ref => ref.object.layout(width)); - } - - addDisposable(disposable: IDisposable): void { - this._register(disposable); - } -} - -export class EditorPool extends Disposable { - - private readonly _pool: ResourcePool; - - public inUse(): Iterable { - return this._pool.inUse; - } - - constructor( - options: ChatEditorOptions, - delegate: IChatRendererDelegate, - overflowWidgetsDomNode: HTMLElement | undefined, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super(); - this._pool = this._register(new ResourcePool(() => { - return instantiationService.createInstance(CodeBlockPart, options, MenuId.AideChatCodeBlock, delegate, overflowWidgetsDomNode); - })); - } - - get(): IDisposableReference { - const codeBlock = this._pool.get(); - let stale = false; - return { - object: codeBlock, - isStale: () => stale, - dispose: () => { - codeBlock.reset(); - stale = true; - this._pool.release(codeBlock); - } - }; - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatProgressContentPart.ts b/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatProgressContentPart.ts deleted file mode 100644 index 73ef5c4e33c..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatProgressContentPart.ts +++ /dev/null @@ -1,65 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { $ } from '../../../../../base/browser/dom.js'; -import { alert } from '../../../../../base/browser/ui/aria/aria.js'; -import { Codicon } from '../../../../../base/common/codicons.js'; -import { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; -import { ChatTreeItem } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { IChatContentPart, IChatContentPartRenderContext } from '../../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatContentParts.js'; -import { IAideChatProgressMessage, IAideChatTask } from '../../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { IChatRendererContent, isResponseVM } from '../../../../../workbench/contrib/aideChat/common/aideChatViewModel.js'; - -export class ChatProgressContentPart extends Disposable implements IChatContentPart { - public readonly domNode: HTMLElement; - - private readonly showSpinner: boolean; - - constructor( - progress: IAideChatProgressMessage | IAideChatTask, - renderer: MarkdownRenderer, - context: IChatContentPartRenderContext, - forceShowSpinner?: boolean, - forceShowMessage?: boolean - ) { - super(); - - const followingContent = context.content.slice(context.index + 1); - this.showSpinner = forceShowSpinner ?? shouldShowSpinner(followingContent, context.element); - const hideMessage = forceShowMessage !== true && followingContent.some(part => part.kind !== 'progressMessage'); - if (hideMessage) { - // Placeholder, don't show the progress message - this.domNode = $(''); - return; - } - - if (this.showSpinner) { - // TODO@roblourens is this the right place for this? - // this step is in progress, communicate it to SR users - alert(progress.content.value); - } - const codicon = this.showSpinner ? ThemeIcon.modify(Codicon.loading, 'spin').id : Codicon.check.id; - const markdown = new MarkdownString(`$(${codicon}) ${progress.content.value}`, { - supportThemeIcons: true - }); - const result = this._register(renderer.render(markdown)); - result.element.classList.add('progress-step'); - - this.domNode = result.element; - } - - hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { - // Needs rerender when spinner state changes - const showSpinner = shouldShowSpinner(followingContent, element); - return other.kind === 'progressMessage' && this.showSpinner === showSpinner; - } -} - -function shouldShowSpinner(followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { - return isResponseVM(element) && !element.isComplete && followingContent.length === 0; -} diff --git a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatReferencesContentPart.ts b/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatReferencesContentPart.ts deleted file mode 100644 index 437bd995729..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatReferencesContentPart.ts +++ /dev/null @@ -1,299 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from '../../../../../base/browser/dom.js'; -import { Button } from '../../../../../base/browser/ui/button/button.js'; -import { IListRenderer, IListVirtualDelegate } from '../../../../../base/browser/ui/list/list.js'; -import { Codicon } from '../../../../../base/common/codicons.js'; -import { Emitter, Event } from '../../../../../base/common/event.js'; -import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; -import { matchesSomeScheme, Schemas } from '../../../../../base/common/network.js'; -import { basename } from '../../../../../base/common/path.js'; -import { basenameOrAuthority } from '../../../../../base/common/resources.js'; -import { ThemeIcon } from '../../../../../base/common/themables.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { localize } from '../../../../../nls.js'; -import { FileKind } from '../../../../../platform/files/common/files.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { WorkbenchList } from '../../../../../platform/list/browser/listService.js'; -import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; -import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; -import { IResourceLabel, ResourceLabels } from '../../../../../workbench/browser/labels.js'; -import { ColorScheme } from '../../../../../workbench/browser/web.api.js'; -import { ChatTreeItem } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { IDisposableReference, ResourcePool } from '../../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatCollections.js'; -import { IChatContentPart } from '../../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatContentParts.js'; -import { IAideChatContentReference, IAideChatWarningMessage } from '../../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { IAideChatVariablesService } from '../../../../../workbench/contrib/aideChat/common/aideChatVariables.js'; -import { IChatRendererContent, IChatResponseViewModel } from '../../../../../workbench/contrib/aideChat/common/aideChatViewModel.js'; -import { createFileIconThemableTreeContainerScope } from '../../../../../workbench/contrib/files/browser/views/explorerView.js'; - -const $ = dom.$; - -export class ChatReferencesContentPart extends Disposable implements IChatContentPart { - public readonly domNode: HTMLElement; - - private readonly _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; - - constructor( - private readonly data: ReadonlyArray, - labelOverride: string | undefined, - element: IChatResponseViewModel, - contentReferencesListPool: ContentReferencesListPool, - @IOpenerService openerService: IOpenerService, - ) { - super(); - - const referencesLabel = labelOverride ?? (data.length > 1 ? - localize('usedReferencesPlural', "Used {0} references", data.length) : - localize('usedReferencesSingular', "Used {0} reference", 1)); - const iconElement = $('.chat-used-context-icon'); - const icon = (element: IChatResponseViewModel) => element.usedReferencesExpanded ? Codicon.chevronDown : Codicon.chevronRight; - iconElement.classList.add(...ThemeIcon.asClassNameArray(icon(element))); - const buttonElement = $('.chat-used-context-label', undefined); - - const collapseButton = this._register(new Button(buttonElement, { - buttonBackground: undefined, - buttonBorder: undefined, - buttonForeground: undefined, - buttonHoverBackground: undefined, - buttonSecondaryBackground: undefined, - buttonSecondaryForeground: undefined, - buttonSecondaryHoverBackground: undefined, - buttonSeparator: undefined - })); - this.domNode = $('.chat-used-context', undefined, buttonElement); - collapseButton.label = referencesLabel; - collapseButton.element.prepend(iconElement); - this.updateAriaLabel(collapseButton.element, referencesLabel, element.usedReferencesExpanded); - this.domNode.classList.toggle('chat-used-context-collapsed', !element.usedReferencesExpanded); - this._register(collapseButton.onDidClick(() => { - iconElement.classList.remove(...ThemeIcon.asClassNameArray(icon(element))); - element.usedReferencesExpanded = !element.usedReferencesExpanded; - iconElement.classList.add(...ThemeIcon.asClassNameArray(icon(element))); - this.domNode.classList.toggle('chat-used-context-collapsed', !element.usedReferencesExpanded); - this._onDidChangeHeight.fire(); - this.updateAriaLabel(collapseButton.element, referencesLabel, element.usedReferencesExpanded); - })); - - const ref = this._register(contentReferencesListPool.get()); - const list = ref.object; - this.domNode.appendChild(list.getHTMLElement().parentElement!); - - this._register(list.onDidOpen((e) => { - if (e.element && 'reference' in e.element) { - const uriOrLocation = 'variableName' in e.element.reference ? e.element.reference.value : e.element.reference; - const uri = URI.isUri(uriOrLocation) ? uriOrLocation : - uriOrLocation?.uri; - if (uri) { - openerService.open( - uri, - { - fromUserGesture: true, - editorOptions: { - ...e.editorOptions, - ...{ - selection: uriOrLocation && 'range' in uriOrLocation ? uriOrLocation.range : undefined - } - } - }); - } - } - })); - this._register(list.onContextMenu((e) => { - e.browserEvent.preventDefault(); - e.browserEvent.stopPropagation(); - })); - - const maxItemsShown = 6; - const itemsShown = Math.min(data.length, maxItemsShown); - const height = itemsShown * 22; - list.layout(height); - list.getHTMLElement().style.height = `${height}px`; - list.splice(0, list.length, data); - } - - hasSameContent(other: IChatRendererContent, followingContent: IChatRendererContent[], element: ChatTreeItem): boolean { - return other.kind === 'references' && other.references.length === this.data.length; - } - - private updateAriaLabel(element: HTMLElement, label: string, expanded?: boolean): void { - element.ariaLabel = expanded ? localize('usedReferencesExpanded', "{0}, expanded", label) : localize('usedReferencesCollapsed', "{0}, collapsed", label); - } - - addDisposable(disposable: IDisposable): void { - this._register(disposable); - } -} - -export class ContentReferencesListPool extends Disposable { - private _pool: ResourcePool>; - - public get inUse(): ReadonlySet> { - return this._pool.inUse; - } - - constructor( - private _onDidChangeVisibility: Event, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IThemeService private readonly themeService: IThemeService, - ) { - super(); - this._pool = this._register(new ResourcePool(() => this.listFactory())); - } - - private listFactory(): WorkbenchList { - const resourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility })); - - const container = $('.chat-used-context-list'); - this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); - - const list = this.instantiationService.createInstance( - WorkbenchList, - 'ChatListRenderer', - container, - new ContentReferencesListDelegate(), - [this.instantiationService.createInstance(ContentReferencesListRenderer, resourceLabels)], - { - alwaysConsumeMouseWheel: false, - accessibilityProvider: { - getAriaLabel: (element: IAideChatContentReference | IAideChatWarningMessage) => { - if (element.kind === 'warning') { - return element.content.value; - } - const reference = element.reference; - if ('variableName' in reference) { - return reference.variableName; - } else if (URI.isUri(reference)) { - return basename(reference.path); - } else { - return basename(reference.uri.path); - } - }, - - getWidgetAriaLabel: () => localize('usedReferences', "Used References") - }, - dnd: { - getDragURI: (element: IAideChatContentReference | IAideChatWarningMessage) => { - if (element.kind === 'warning') { - return null; - } - const { reference } = element; - if ('variableName' in reference) { - return null; - } else if (URI.isUri(reference)) { - return reference.toString(); - } else { - return reference.uri.toString(); - } - }, - dispose: () => { }, - onDragOver: () => false, - drop: () => { }, - }, - }); - - return list; - } - - get(): IDisposableReference> { - const object = this._pool.get(); - let stale = false; - return { - object, - isStale: () => stale, - dispose: () => { - stale = true; - this._pool.release(object); - } - }; - } -} - -class ContentReferencesListDelegate implements IListVirtualDelegate { - getHeight(element: IAideChatContentReference): number { - return 22; - } - - getTemplateId(element: IAideChatContentReference): string { - return ContentReferencesListRenderer.TEMPLATE_ID; - } -} - -interface IChatContentReferenceListTemplate { - label: IResourceLabel; - templateDisposables: IDisposable; -} - -class ContentReferencesListRenderer implements IListRenderer { - static TEMPLATE_ID = 'contentReferencesListRenderer'; - readonly templateId: string = ContentReferencesListRenderer.TEMPLATE_ID; - - constructor( - private labels: ResourceLabels, - @IThemeService private readonly themeService: IThemeService, - @IAideChatVariablesService private readonly chatVariablesService: IAideChatVariablesService, - ) { } - - renderTemplate(container: HTMLElement): IChatContentReferenceListTemplate { - const templateDisposables = new DisposableStore(); - const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true })); - return { templateDisposables, label }; - } - - - private getReferenceIcon(data: IAideChatContentReference): URI | ThemeIcon | undefined { - if (ThemeIcon.isThemeIcon(data.iconPath)) { - return data.iconPath; - } else { - return this.themeService.getColorTheme().type === ColorScheme.DARK && data.iconPath?.dark - ? data.iconPath?.dark - : data.iconPath?.light; - } - } - - renderElement(data: IAideChatContentReference | IAideChatWarningMessage, index: number, templateData: IChatContentReferenceListTemplate, height: number | undefined): void { - if (data.kind === 'warning') { - templateData.label.setResource({ name: data.content.value }, { icon: Codicon.warning }); - return; - } - - const reference = data.reference; - const icon = this.getReferenceIcon(data); - templateData.label.element.style.display = 'flex'; - if ('variableName' in reference) { - if (reference.value) { - const uri = URI.isUri(reference.value) ? reference.value : reference.value.uri; - templateData.label.setResource( - { - resource: uri, - name: basenameOrAuthority(uri), - description: `#${reference.variableName}`, - range: 'range' in reference.value ? reference.value.range : undefined, - }, { icon }); - } else { - const variable = this.chatVariablesService.getVariable(reference.variableName); - templateData.label.setLabel(`#${reference.variableName}`, undefined, { title: variable?.description }); - } - } else { - const uri = 'uri' in reference ? reference.uri : reference; - if (matchesSomeScheme(uri, Schemas.mailto, Schemas.http, Schemas.https)) { - templateData.label.setResource({ resource: uri, name: uri.toString() }, { icon: icon ?? Codicon.globe }); - } else { - templateData.label.setFile(uri, { - fileKind: FileKind.FILE, - // Should not have this live-updating data on a historical reference - fileDecorations: { badges: false, colors: false }, - range: 'range' in reference ? reference.range : undefined - }); - } - } - } - - disposeTemplate(templateData: IChatContentReferenceListTemplate): void { - templateData.templateDisposables.dispose(); - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatTaskContentPart.ts b/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatTaskContentPart.ts deleted file mode 100644 index 42fe7e5355f..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatTaskContentPart.ts +++ /dev/null @@ -1,52 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from '../../../../../base/browser/dom.js'; -import { Event } from '../../../../../base/common/event.js'; -import { Disposable, IDisposable } from '../../../../../base/common/lifecycle.js'; -import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IChatContentPart, IChatContentPartRenderContext } from '../../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatContentParts.js'; -import { ChatProgressContentPart } from '../../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatProgressContentPart.js'; -import { ChatReferencesContentPart, ContentReferencesListPool } from '../../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatReferencesContentPart.js'; -import { IChatProgressRenderableResponseContent } from '../../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { IAideChatTask } from '../../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { IChatResponseViewModel } from '../../../../../workbench/contrib/aideChat/common/aideChatViewModel.js'; - -export class ChatTaskContentPart extends Disposable implements IChatContentPart { - public readonly domNode: HTMLElement; - public readonly onDidChangeHeight: Event; - - constructor( - private readonly task: IAideChatTask, - contentReferencesListPool: ContentReferencesListPool, - renderer: MarkdownRenderer, - context: IChatContentPartRenderContext, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super(); - - if (task.progress.length) { - const refsPart = this._register(instantiationService.createInstance(ChatReferencesContentPart, task.progress, task.content.value, context.element as IChatResponseViewModel, contentReferencesListPool)); - this.domNode = dom.$('.chat-progress-task'); - this.domNode.appendChild(refsPart.domNode); - this.onDidChangeHeight = refsPart.onDidChangeHeight; - } else { - const progressPart = this._register(instantiationService.createInstance(ChatProgressContentPart, task, renderer, context, !task.isSettled(), true)); - this.domNode = progressPart.domNode; - this.onDidChangeHeight = Event.None; - } - } - - hasSameContent(other: IChatProgressRenderableResponseContent): boolean { - return other.kind === 'progressTask' - && other.progress.length === this.task.progress.length - && other.isSettled() === this.task.isSettled(); - } - - addDisposable(disposable: IDisposable): void { - this._register(disposable); - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatTextEditContentPart.ts b/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatTextEditContentPart.ts deleted file mode 100644 index 8bfee6993ab..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatTextEditContentPart.ts +++ /dev/null @@ -1,207 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from '../../../../../base/browser/dom.js'; -import { CancellationTokenSource } from '../../../../../base/common/cancellation.js'; -import { Emitter, Event } from '../../../../../base/common/event.js'; -import { Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js'; -import { Schemas } from '../../../../../base/common/network.js'; -import { isEqual } from '../../../../../base/common/resources.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { generateUuid } from '../../../../../base/common/uuid.js'; -import { ISingleEditOperation } from '../../../../../editor/common/core/editOperation.js'; -import { TextEdit } from '../../../../../editor/common/languages.js'; -import { createTextBufferFactoryFromSnapshot } from '../../../../../editor/common/model/textModel.js'; -import { IModelService } from '../../../../../editor/common/services/model.js'; -import { DefaultModelSHA1Computer } from '../../../../../editor/common/services/modelService.js'; -import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; -import { localize } from '../../../../../nls.js'; -import { MenuId } from '../../../../../platform/actions/common/actions.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IChatListItemRendererOptions } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { IDisposableReference, ResourcePool } from '../../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatCollections.js'; -import { IChatContentPart, IChatContentPartRenderContext } from '../../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatContentParts.js'; -import { IChatRendererDelegate } from '../../../../../workbench/contrib/aideChat/browser/aideChatListRenderer.js'; -import { ChatEditorOptions } from '../../../../../workbench/contrib/aideChat/browser/aideChatOptions.js'; -import { CodeCompareBlockPart, ICodeCompareBlockData, ICodeCompareBlockDiffData } from '../../../../../workbench/contrib/aideChat/browser/codeBlockPart.js'; -import { IChatProgressRenderableResponseContent, IChatTextEditGroup } from '../../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { IAideChatService } from '../../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { isResponseVM } from '../../../../../workbench/contrib/aideChat/common/aideChatViewModel.js'; - -const $ = dom.$; - -export class ChatTextEditContentPart extends Disposable implements IChatContentPart { - public readonly domNode: HTMLElement; - private readonly ref: IDisposableReference | undefined; - - private readonly _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; - - constructor( - chatTextEdit: IChatTextEditGroup, - context: IChatContentPartRenderContext, - rendererOptions: IChatListItemRendererOptions, - diffEditorPool: DiffEditorPool, - currentWidth: number, - @ITextModelService private readonly textModelService: ITextModelService, - @IModelService private readonly modelService: IModelService, - @IAideChatService private readonly chatService: IAideChatService, - ) { - super(); - const element = context.element; - - // TODO@jrieken move this into the CompareCodeBlock and properly say what kind of changes happen - if (rendererOptions.renderTextEditsAsSummary?.(chatTextEdit.uri)) { - if (isResponseVM(element) && element.response.value.every(item => item.kind === 'textEditGroup')) { - this.domNode = $('.interactive-edits-summary', undefined, !element.isComplete ? localize('editsSummary1', "Making changes...") : localize('editsSummary', "Made changes.")); - } else { - this.domNode = $('div'); - } - - // TODO@roblourens this case is now handled outside this Part in ChatListRenderer, but can it be cleaned up? - // return; - } else { - - - const cts = new CancellationTokenSource(); - - let isDisposed = false; - this._register(toDisposable(() => { - isDisposed = true; - cts.dispose(true); - })); - - this.ref = this._register(diffEditorPool.get()); - - // Attach this after updating text/layout of the editor, so it should only be fired when the size updates later (horizontal scrollbar, wrapping) - // not during a renderElement OR a progressive render (when we will be firing this event anyway at the end of the render) - this._register(this.ref.object.onDidChangeContentHeight(() => { - this._onDidChangeHeight.fire(); - })); - - const data: ICodeCompareBlockData = { - element, - edit: chatTextEdit, - diffData: (async () => { - - const ref = await this.textModelService.createModelReference(chatTextEdit.uri); - - if (isDisposed) { - ref.dispose(); - return; - } - - this._register(ref); - - const original = ref.object.textEditorModel; - let originalSha1: string = ''; - - if (chatTextEdit.state) { - originalSha1 = chatTextEdit.state.sha1; - } else { - const sha1 = new DefaultModelSHA1Computer(); - if (sha1.canComputeSHA1(original)) { - originalSha1 = sha1.computeSHA1(original); - chatTextEdit.state = { sha1: originalSha1, applied: 0 }; - } - } - - const modified = this.modelService.createModel( - createTextBufferFactoryFromSnapshot(original.createSnapshot()), - { languageId: original.getLanguageId(), onDidChange: Event.None }, - URI.from({ scheme: Schemas.vscodeChatCodeBlock, path: original.uri.path, query: generateUuid() }), - false - ); - const modRef = await this.textModelService.createModelReference(modified.uri); - this._register(modRef); - - const editGroups: ISingleEditOperation[][] = []; - if (isResponseVM(element)) { - const chatModel = this.chatService.getSession(element.sessionId)!; - - for (const request of chatModel.getRequests()) { - if (!request.response) { - continue; - } - for (const item of request.response.response.value) { - if (item.kind !== 'textEditGroup' || item.state?.applied || !isEqual(item.uri, chatTextEdit.uri)) { - continue; - } - for (const group of item.edits) { - const edits = group.map(TextEdit.asEditOperation); - editGroups.push(edits); - } - } - if (request.response === element.model) { - break; - } - } - } - - for (const edits of editGroups) { - modified.pushEditOperations(null, edits, () => null); - } - - return { - modified, - original, - originalSha1 - } satisfies ICodeCompareBlockDiffData; - })() - }; - this.ref.object.render(data, currentWidth, cts.token); - - this.domNode = this.ref.object.element; - } - } - - layout(width: number): void { - this.ref?.object.layout(width); - } - - hasSameContent(other: IChatProgressRenderableResponseContent): boolean { - // No other change allowed for this content type - return other.kind === 'textEditGroup'; - } - - addDisposable(disposable: IDisposable): void { - this._register(disposable); - } -} - -export class DiffEditorPool extends Disposable { - - private readonly _pool: ResourcePool; - - public inUse(): Iterable { - return this._pool.inUse; - } - - constructor( - options: ChatEditorOptions, - delegate: IChatRendererDelegate, - overflowWidgetsDomNode: HTMLElement | undefined, - @IInstantiationService instantiationService: IInstantiationService, - ) { - super(); - this._pool = this._register(new ResourcePool(() => { - return instantiationService.createInstance(CodeCompareBlockPart, options, MenuId.AideChatCompareBlock, delegate, overflowWidgetsDomNode); - })); - } - - get(): IDisposableReference { - const codeBlock = this._pool.get(); - let stale = false; - return { - object: codeBlock, - isStale: () => stale, - dispose: () => { - codeBlock.reset(); - stale = true; - this._pool.release(codeBlock); - } - }; - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatTreeContentPart.ts b/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatTreeContentPart.ts deleted file mode 100644 index de056c08e1e..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatTreeContentPart.ts +++ /dev/null @@ -1,225 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from '../../../../../base/browser/dom.js'; -import { IListVirtualDelegate } from '../../../../../base/browser/ui/list/list.js'; -import { ITreeCompressionDelegate } from '../../../../../base/browser/ui/tree/asyncDataTree.js'; -import { ICompressedTreeNode } from '../../../../../base/browser/ui/tree/compressedObjectTreeModel.js'; -import { ICompressibleTreeRenderer } from '../../../../../base/browser/ui/tree/objectTree.js'; -import { IAsyncDataSource, ITreeNode } from '../../../../../base/browser/ui/tree/tree.js'; -import { Emitter, Event } from '../../../../../base/common/event.js'; -import { Disposable, DisposableStore, IDisposable } from '../../../../../base/common/lifecycle.js'; -import { localize } from '../../../../../nls.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { FileKind, FileType } from '../../../../../platform/files/common/files.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { WorkbenchCompressibleAsyncDataTree } from '../../../../../platform/list/browser/listService.js'; -import { IOpenerService } from '../../../../../platform/opener/common/opener.js'; -import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; -import { IResourceLabel, ResourceLabels } from '../../../../../workbench/browser/labels.js'; -import { ChatTreeItem } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { IDisposableReference, ResourcePool } from '../../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatCollections.js'; -import { IChatContentPart } from '../../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatContentParts.js'; -import { IChatProgressRenderableResponseContent } from '../../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { IChatResponseProgressFileTreeData } from '../../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { createFileIconThemableTreeContainerScope } from '../../../../../workbench/contrib/files/browser/views/explorerView.js'; -import { IFilesConfiguration } from '../../../../../workbench/contrib/files/common/files.js'; - -const $ = dom.$; - -export class ChatTreeContentPart extends Disposable implements IChatContentPart { - public readonly domNode: HTMLElement; - - private readonly _onDidChangeHeight = this._register(new Emitter()); - public readonly onDidChangeHeight = this._onDidChangeHeight.event; - - public readonly onDidFocus: Event; - - private tree: WorkbenchCompressibleAsyncDataTree; - - constructor( - data: IChatResponseProgressFileTreeData, - element: ChatTreeItem, - treePool: TreePool, - treeDataIndex: number, - @IOpenerService private readonly openerService: IOpenerService - ) { - super(); - - const ref = this._register(treePool.get()); - this.tree = ref.object; - this.onDidFocus = this.tree.onDidFocus; - - this._register(this.tree.onDidOpen((e) => { - if (e.element && !('children' in e.element)) { - this.openerService.open(e.element.uri); - } - })); - this._register(this.tree.onDidChangeCollapseState(() => { - this._onDidChangeHeight.fire(); - })); - this._register(this.tree.onContextMenu((e) => { - e.browserEvent.preventDefault(); - e.browserEvent.stopPropagation(); - })); - - this.tree.setInput(data).then(() => { - if (!ref.isStale()) { - this.tree.layout(); - this._onDidChangeHeight.fire(); - } - }); - - this.domNode = this.tree.getHTMLElement().parentElement!; - } - - domFocus() { - this.tree.domFocus(); - } - - hasSameContent(other: IChatProgressRenderableResponseContent): boolean { - // No other change allowed for this content type - return other.kind === 'treeData'; - } - - addDisposable(disposable: IDisposable): void { - this._register(disposable); - } -} - -export class TreePool extends Disposable { - private _pool: ResourcePool>; - - public get inUse(): ReadonlySet> { - return this._pool.inUse; - } - - constructor( - private _onDidChangeVisibility: Event, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IConfigurationService private readonly configService: IConfigurationService, - @IThemeService private readonly themeService: IThemeService, - ) { - super(); - this._pool = this._register(new ResourcePool(() => this.treeFactory())); - } - - private treeFactory(): WorkbenchCompressibleAsyncDataTree { - const resourceLabels = this._register(this.instantiationService.createInstance(ResourceLabels, { onDidChangeVisibility: this._onDidChangeVisibility })); - - const container = $('.interactive-response-progress-tree'); - this._register(createFileIconThemableTreeContainerScope(container, this.themeService)); - - const tree = this.instantiationService.createInstance( - WorkbenchCompressibleAsyncDataTree, - 'ChatListRenderer', - container, - new ChatListTreeDelegate(), - new ChatListTreeCompressionDelegate(), - [new ChatListTreeRenderer(resourceLabels, this.configService.getValue('explorer.decorations'))], - new ChatListTreeDataSource(), - { - collapseByDefault: () => false, - expandOnlyOnTwistieClick: () => false, - identityProvider: { - getId: (e: IChatResponseProgressFileTreeData) => e.uri.toString() - }, - accessibilityProvider: { - getAriaLabel: (element: IChatResponseProgressFileTreeData) => element.label, - getWidgetAriaLabel: () => localize('treeAriaLabel', "File Tree") - }, - alwaysConsumeMouseWheel: false - }); - - return tree; - } - - get(): IDisposableReference> { - const object = this._pool.get(); - let stale = false; - return { - object, - isStale: () => stale, - dispose: () => { - stale = true; - this._pool.release(object); - } - }; - } -} - -class ChatListTreeDelegate implements IListVirtualDelegate { - static readonly ITEM_HEIGHT = 22; - - getHeight(element: IChatResponseProgressFileTreeData): number { - return ChatListTreeDelegate.ITEM_HEIGHT; - } - - getTemplateId(element: IChatResponseProgressFileTreeData): string { - return 'chatListTreeTemplate'; - } -} - -class ChatListTreeCompressionDelegate implements ITreeCompressionDelegate { - isIncompressible(element: IChatResponseProgressFileTreeData): boolean { - return !element.children; - } -} - -interface IChatListTreeRendererTemplate { - templateDisposables: DisposableStore; - label: IResourceLabel; -} - -class ChatListTreeRenderer implements ICompressibleTreeRenderer { - templateId: string = 'chatListTreeTemplate'; - - constructor(private labels: ResourceLabels, private decorations: IFilesConfiguration['explorer']['decorations']) { } - - renderCompressedElements(element: ITreeNode, void>, index: number, templateData: IChatListTreeRendererTemplate, height: number | undefined): void { - templateData.label.element.style.display = 'flex'; - const label = element.element.elements.map((e) => e.label); - templateData.label.setResource({ resource: element.element.elements[0].uri, name: label }, { - title: element.element.elements[0].label, - fileKind: element.children ? FileKind.FOLDER : FileKind.FILE, - extraClasses: ['explorer-item'], - fileDecorations: this.decorations - }); - } - renderTemplate(container: HTMLElement): IChatListTreeRendererTemplate { - const templateDisposables = new DisposableStore(); - const label = templateDisposables.add(this.labels.create(container, { supportHighlights: true })); - return { templateDisposables, label }; - } - renderElement(element: ITreeNode, index: number, templateData: IChatListTreeRendererTemplate, height: number | undefined): void { - templateData.label.element.style.display = 'flex'; - if (!element.children.length && element.element.type !== FileType.Directory) { - templateData.label.setFile(element.element.uri, { - fileKind: FileKind.FILE, - hidePath: true, - fileDecorations: this.decorations, - }); - } else { - templateData.label.setResource({ resource: element.element.uri, name: element.element.label }, { - title: element.element.label, - fileKind: FileKind.FOLDER, - fileDecorations: this.decorations - }); - } - } - disposeTemplate(templateData: IChatListTreeRendererTemplate): void { - templateData.templateDisposables.dispose(); - } -} - -class ChatListTreeDataSource implements IAsyncDataSource { - hasChildren(element: IChatResponseProgressFileTreeData): boolean { - return !!element.children; - } - - async getChildren(element: IChatResponseProgressFileTreeData): Promise> { - return element.children ?? []; - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatWarningContentPart.ts b/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatWarningContentPart.ts deleted file mode 100644 index b201b3d843a..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/aideChatWarningContentPart.ts +++ /dev/null @@ -1,53 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as dom from '../../../../../base/browser/dom.js'; -import { renderIcon } from '../../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { Codicon } from '../../../../../base/common/codicons.js'; -import { IMarkdownString } from '../../../../../base/common/htmlContent.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { MarkdownRenderer } from '../../../../../editor/browser/widget/markdownRenderer/browser/markdownRenderer.js'; -import { IChatContentPart } from '../../../../../workbench/contrib/aideChat/browser/chatContentParts/aideChatContentParts.js'; -import { IChatProgressRenderableResponseContent } from '../../../../../workbench/contrib/aideChat/common/aideChatModel.js'; - -const $ = dom.$; - -export class ChatWarningContentPart extends Disposable implements IChatContentPart { - public readonly domNode: HTMLElement; - - constructor( - kind: 'info' | 'warning' | 'error', - content: IMarkdownString, - renderer: MarkdownRenderer, - ) { - super(); - - this.domNode = $('.chat-notification-widget'); - let icon; - let iconClass; - switch (kind) { - case 'warning': - icon = Codicon.warning; - iconClass = '.chat-warning-codicon'; - break; - case 'error': - icon = Codicon.error; - iconClass = '.chat-error-codicon'; - break; - case 'info': - icon = Codicon.info; - iconClass = '.chat-info-codicon'; - break; - } - this.domNode.appendChild($(iconClass, undefined, renderIcon(icon))); - const markdownContent = renderer.render(content); - this.domNode.appendChild(markdownContent.element); - } - - hasSameContent(other: IChatProgressRenderableResponseContent): boolean { - // No other change allowed for this content type - return other.kind === 'warning'; - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/media/chatConfirmationWidget.css b/src/vs/workbench/contrib/aideChat/browser/chatContentParts/media/chatConfirmationWidget.css deleted file mode 100644 index e244f077dd6..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/chatContentParts/media/chatConfirmationWidget.css +++ /dev/null @@ -1,37 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.chat-confirmation-widget { - border: 1px solid var(--vscode-chat-requestBorder); - border-radius: 4px; - margin-bottom: 16px; - padding: 8px 12px 12px; -} - -.chat-confirmation-widget .chat-confirmation-widget-title { - font-weight: 600; -} - -.chat-confirmation-widget .chat-confirmation-widget-title p { - margin: 0 0 4px 0; -} - -.chat-confirmation-widget .chat-confirmation-widget-message .rendered-markdown p { - margin-top: 0; -} - -.chat-confirmation-widget .chat-confirmation-widget-message .rendered-markdown > :last-child { - margin-bottom: 0px; -} - -.chat-confirmation-widget .chat-confirmation-buttons-container { - display: flex; - gap: 8px; - margin-top: 13px; -} - -.chat-confirmation-widget.hideButtons .chat-confirmation-buttons-container { - display: none; -} diff --git a/src/vs/workbench/contrib/aideChat/browser/codeBlockContextProviderService.ts b/src/vs/workbench/contrib/aideChat/browser/codeBlockContextProviderService.ts deleted file mode 100644 index b604c8e5b60..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/codeBlockContextProviderService.ts +++ /dev/null @@ -1,20 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { ICodeBlockActionContextProvider, IAideChatCodeBlockContextProviderService } from '../../../../workbench/contrib/aideChat/browser/aideChat.js'; - -export class ChatCodeBlockContextProviderService implements IAideChatCodeBlockContextProviderService { - declare _serviceBrand: undefined; - private readonly _providers = new Map(); - - get providers(): ICodeBlockActionContextProvider[] { - return [...this._providers.values()]; - } - registerProvider(provider: ICodeBlockActionContextProvider, id: string): IDisposable { - this._providers.set(id, provider); - return toDisposable(() => this._providers.delete(id)); - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/codeBlockPart.css b/src/vs/workbench/contrib/aideChat/browser/codeBlockPart.css deleted file mode 100644 index 3c8843cd244..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/codeBlockPart.css +++ /dev/null @@ -1,163 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - - -.interactive-result-code-block { - position: relative; -} - -.interactive-result-code-block .interactive-result-code-block-toolbar { - display: none; -} - -.interactive-result-code-block .interactive-result-code-block-toolbar > .monaco-action-bar, -.interactive-result-code-block .interactive-result-code-block-toolbar > .monaco-toolbar { - position: absolute; - top: -13px; - height: 26px; - line-height: 26px; - background-color: var(--vscode-interactive-result-editor-background-color, var(--vscode-editor-background)); - border: 1px solid var(--vscode-chat-requestBorder); - z-index: 100; - max-width: 70%; - text-overflow: ellipsis; - overflow: hidden; -} - -.interactive-result-code-block .interactive-result-code-block-toolbar > .monaco-action-bar { - left: 0px -} - -.interactive-result-code-block .interactive-result-code-block-toolbar > .monaco-toolbar { - right: 10px; -} - -.interactive-result-code-block .monaco-toolbar .action-item { - height: 24px; - width: 24px; - margin: 1px 2px; -} - -.interactive-result-code-block .monaco-toolbar .action-item .codicon { - margin: 1px; -} - -.interactive-result-code-block:hover .interactive-result-code-block-toolbar, -.interactive-result-code-block .interactive-result-code-block-toolbar:focus-within, -.interactive-result-code-block.focused .interactive-result-code-block-toolbar { - display: initial; - border-radius: 2px; -} - -.interactive-result-code-block .interactive-result-code-block-toolbar.force-visibility .monaco-toolbar { - display: initial !important; -} - -.cschat-item-container .value .rendered-markdown [data-code] { - margin: 16px 0; -} - -.interactive-result-code-block { - border: 1px solid var(--vscode-input-border, transparent); - background-color: var(--vscode-interactive-result-editor-background-color); -} - -.interactive-result-code-block:has(.monaco-editor.focused) { - border-color: var(--vscode-focusBorder, transparent); -} - -.interactive-result-code-block, -.interactive-result-code-block .monaco-editor, -.interactive-result-code-block .monaco-editor .overflow-guard { - border-radius: 4px; -} - -.interactive-result-code-block .interactive-result-vulns { - font-size: 0.9em; - padding: 0px 8px 2px 8px; -} - -.interactive-result-code-block .interactive-result-vulns-header { - display: flex; - height: 22px; -} - -.interactive-result-code-block .interactive-result-vulns-header, -.interactive-result-code-block .interactive-result-vulns-list { - opacity: 0.8; -} - -.interactive-result-code-block .interactive-result-vulns-list { - margin: 0px; - padding-bottom: 3px; - padding-left: 16px !important; /* Override markdown styles */ -} - -.interactive-result-code-block.chat-vulnerabilities-collapsed .interactive-result-vulns-list { - display: none; -} - -.interactive-result-code-block .interactive-result-vulns-list .chat-vuln-title { - font-weight: bold; -} - -.interactive-result-code-block.no-vulns .interactive-result-vulns { - display: none; -} - -.interactive-result-code-block .interactive-result-vulns-header .monaco-button { - /* unset Button styles */ - display: inline-flex; - width: 100%; - border: none; - padding: 0; - text-align: initial; - justify-content: initial; - color: var(--vscode-foreground) !important; /* This is inside .rendered-markdown */ - user-select: none; -} - -.interactive-result-code-block .interactive-result-vulns-header .monaco-text-button:focus { - outline: none; -} - -.interactive-result-code-block .interactive-result-vulns-header .monaco-text-button:focus-visible { - outline: 1px solid var(--vscode-focusBorder); -} - -/* compare code block */ - -.interactive-result-code-block.compare.no-diff .message { - display: inherit; -} - -.interactive-result-code-block.compare .message { - display: none; - padding: 6px; -} - - -.interactive-result-code-block.compare .message A { - color: var(--vscode-textLink-foreground); - cursor: pointer; -} - -.interactive-result-code-block.compare .message A > CODE { - color: var(--vscode-textLink-foreground); -} - -.interactive-result-code-block.compare .interactive-result-header { - display: flex; - justify-content: space-between; - align-items: center; - padding: 0 3px; - box-sizing: border-box; - border-bottom: solid 1px var(--vscode-chat-requestBorder); -} - -.interactive-result-code-block.compare.no-diff .interactive-result-header, -.interactive-result-code-block.compare.no-diff .interactive-result-editor { - display: none; -} diff --git a/src/vs/workbench/contrib/aideChat/browser/codeBlockPart.ts b/src/vs/workbench/contrib/aideChat/browser/codeBlockPart.ts deleted file mode 100644 index 485597f559f..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/codeBlockPart.ts +++ /dev/null @@ -1,892 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import './codeBlockPart.css'; - -import * as dom from '../../../../base/browser/dom.js'; -import { renderFormattedText } from '../../../../base/browser/formattedTextRenderer.js'; -import { Button } from '../../../../base/browser/ui/button/button.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Codicon } from '../../../../base/common/codicons.js'; -import { Emitter } from '../../../../base/common/event.js'; -import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { isEqual } from '../../../../base/common/resources.js'; -import { URI, UriComponents } from '../../../../base/common/uri.js'; -import { IEditorConstructionOptions } from '../../../../editor/browser/config/editorConfiguration.js'; -import { TabFocus } from '../../../../editor/browser/config/tabFocus.js'; -import { IDiffEditor } from '../../../../editor/browser/editorBrowser.js'; -import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js'; -import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; -import { CodeEditorWidget, ICodeEditorWidgetOptions } from '../../../../editor/browser/widget/codeEditor/codeEditorWidget.js'; -import { DiffEditorWidget } from '../../../../editor/browser/widget/diffEditor/diffEditorWidget.js'; -import { EDITOR_FONT_DEFAULTS, EditorOption, IEditorOptions } from '../../../../editor/common/config/editorOptions.js'; -import { IRange, Range } from '../../../../editor/common/core/range.js'; -import { IDiffEditorViewModel, ScrollType } from '../../../../editor/common/editorCommon.js'; -import { TextEdit } from '../../../../editor/common/languages.js'; -import { EndOfLinePreference, ITextModel } from '../../../../editor/common/model.js'; -import { TextModelText } from '../../../../editor/common/model/textModelText.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; -import { DefaultModelSHA1Computer } from '../../../../editor/common/services/modelService.js'; -import { IResolvedTextEditorModel, ITextModelContentProvider, ITextModelService } from '../../../../editor/common/services/resolverService.js'; -import { BracketMatchingController } from '../../../../editor/contrib/bracketMatching/browser/bracketMatching.js'; -import { ColorDetector } from '../../../../editor/contrib/colorPicker/browser/colorDetector.js'; -import { ContextMenuController } from '../../../../editor/contrib/contextmenu/browser/contextmenu.js'; -import { GotoDefinitionAtPositionEditorContribution } from '../../../../editor/contrib/gotoSymbol/browser/link/goToDefinitionAtPosition.js'; -import { ContentHoverController } from '../../../../editor/contrib/hover/browser/contentHoverController.js'; -import { MessageController } from '../../../../editor/contrib/message/browser/messageController.js'; -import { ViewportSemanticTokensContribution } from '../../../../editor/contrib/semanticTokens/browser/viewportSemanticTokens.js'; -import { SmartSelectController } from '../../../../editor/contrib/smartSelect/browser/smartSelect.js'; -import { WordHighlighterContribution } from '../../../../editor/contrib/wordHighlighter/browser/wordHighlighter.js'; -import { localize } from '../../../../nls.js'; -import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; -import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; -import { MenuId } from '../../../../platform/actions/common/actions.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; -import { FileKind } from '../../../../platform/files/common/files.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; -import { ILabelService } from '../../../../platform/label/common/label.js'; -import { IOpenerService } from '../../../../platform/opener/common/opener.js'; -import { ResourceLabel } from '../../../../workbench/browser/labels.js'; -import { AccessibilityVerbositySettingId } from '../../../../workbench/contrib/accessibility/browser/accessibilityConfiguration.js'; -import { ChatTreeItem } from '../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { IChatRendererDelegate } from '../../../../workbench/contrib/aideChat/browser/aideChatListRenderer.js'; -import { ChatEditorOptions } from '../../../../workbench/contrib/aideChat/browser/aideChatOptions.js'; -import { CONTEXT_CHAT_EDIT_APPLIED } from '../../../../workbench/contrib/aideChat/common/aideChatContextKeys.js'; -import { IChatResponseModel, IChatTextEditGroup } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { IChatResponseViewModel, isResponseVM } from '../../../../workbench/contrib/aideChat/common/aideChatViewModel.js'; -import { MenuPreventer } from '../../../../workbench/contrib/codeEditor/browser/menuPreventer.js'; -import { SelectionClipboardContributionID } from '../../../../workbench/contrib/codeEditor/browser/selectionClipboard.js'; -import { getSimpleEditorOptions } from '../../../../workbench/contrib/codeEditor/browser/simpleEditorOptions.js'; -import { IMarkdownVulnerability } from '../common/annotations.js'; - -const $ = dom.$; - -export interface ICodeBlockData { - readonly codeBlockIndex: number; - readonly element: unknown; - - readonly textModel: Promise; - readonly languageId: string; - - readonly vulns?: readonly IMarkdownVulnerability[]; - readonly range?: Range; - - readonly parentContextKeyService?: IContextKeyService; - readonly hideToolbar?: boolean; -} - -/** - * Special markdown code block language id used to render a local file. - * - * The text of the code path should be a {@link LocalFileCodeBlockData} json object. - */ -export const localFileLanguageId = 'vscode-local-file'; - - -export function parseLocalFileData(text: string) { - - interface RawLocalFileCodeBlockData { - readonly uri: UriComponents; - readonly range?: IRange; - } - - let data: RawLocalFileCodeBlockData; - try { - data = JSON.parse(text); - } catch (e) { - throw new Error('Could not parse code block local file data'); - } - - let uri: URI; - try { - uri = URI.revive(data?.uri); - } catch (e) { - throw new Error('Invalid code block local file data URI'); - } - - let range: IRange | undefined; - if (data.range) { - // Note that since this is coming from extensions, position are actually zero based and must be converted. - range = new Range(data.range.startLineNumber + 1, data.range.startColumn + 1, data.range.endLineNumber + 1, data.range.endColumn + 1); - } - - return { uri, range }; -} - -export interface ICodeBlockActionContext { - code: string; - languageId?: string; - codeBlockIndex: number; - element: unknown; -} - -const defaultCodeblockPadding = 10; -export class CodeBlockPart extends Disposable { - protected readonly _onDidChangeContentHeight = this._register(new Emitter()); - public readonly onDidChangeContentHeight = this._onDidChangeContentHeight.event; - - public readonly editor: CodeEditorWidget; - protected readonly toolbar: MenuWorkbenchToolBar; - private readonly contextKeyService: IContextKeyService; - - public readonly element: HTMLElement; - - private readonly vulnsButton: Button; - private readonly vulnsListElement: HTMLElement; - - private currentCodeBlockData: ICodeBlockData | undefined; - private currentScrollWidth = 0; - - private readonly disposableStore = this._register(new DisposableStore()); - - constructor( - private readonly options: ChatEditorOptions, - readonly menuId: MenuId, - delegate: IChatRendererDelegate, - overflowWidgetsDomNode: HTMLElement | undefined, - @IInstantiationService instantiationService: IInstantiationService, - @IContextKeyService contextKeyService: IContextKeyService, - @IModelService protected readonly modelService: IModelService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IAccessibilityService private readonly accessibilityService: IAccessibilityService, - ) { - super(); - this.element = $('.interactive-result-code-block'); - - this.contextKeyService = this._register(contextKeyService.createScoped(this.element)); - const scopedInstantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService])); - const editorElement = dom.append(this.element, $('.interactive-result-editor')); - this.editor = this.createEditor(scopedInstantiationService, editorElement, { - ...getSimpleEditorOptions(this.configurationService), - readOnly: true, - lineNumbers: 'off', - selectOnLineNumbers: true, - scrollBeyondLastLine: false, - lineDecorationsWidth: 8, - dragAndDrop: false, - padding: { top: defaultCodeblockPadding, bottom: defaultCodeblockPadding }, - mouseWheelZoom: false, - scrollbar: { - vertical: 'hidden', - alwaysConsumeMouseWheel: false - }, - definitionLinkOpensInPeek: false, - gotoLocation: { - multiple: 'goto', - multipleDeclarations: 'goto', - multipleDefinitions: 'goto', - multipleImplementations: 'goto', - }, - ariaLabel: localize('aideChat.codeBlockHelp', 'Code block'), - overflowWidgetsDomNode, - ...this.getEditorOptionsFromConfig(), - }); - - const toolbarElement = dom.append(this.element, $('.interactive-result-code-block-toolbar')); - const editorScopedService = this.editor.contextKeyService.createScoped(toolbarElement); - const editorScopedInstantiationService = scopedInstantiationService.createChild(new ServiceCollection([IContextKeyService, editorScopedService])); - this.toolbar = this._register(editorScopedInstantiationService.createInstance(MenuWorkbenchToolBar, toolbarElement, menuId, { - menuOptions: { - shouldForwardArgs: true - } - })); - - const vulnsContainer = dom.append(this.element, $('.interactive-result-vulns')); - const vulnsHeaderElement = dom.append(vulnsContainer, $('.interactive-result-vulns-header', undefined)); - this.vulnsButton = this._register(new Button(vulnsHeaderElement, { - buttonBackground: undefined, - buttonBorder: undefined, - buttonForeground: undefined, - buttonHoverBackground: undefined, - buttonSecondaryBackground: undefined, - buttonSecondaryForeground: undefined, - buttonSecondaryHoverBackground: undefined, - buttonSeparator: undefined, - supportIcons: true - })); - - this.vulnsListElement = dom.append(vulnsContainer, $('ul.interactive-result-vulns-list')); - - this._register(this.vulnsButton.onDidClick(() => { - const element = this.currentCodeBlockData!.element as IChatResponseViewModel; - element.vulnerabilitiesListExpanded = !element.vulnerabilitiesListExpanded; - this.vulnsButton.label = this.getVulnerabilitiesLabel(); - this.element.classList.toggle('chat-vulnerabilities-collapsed', !element.vulnerabilitiesListExpanded); - this._onDidChangeContentHeight.fire(); - // this.updateAriaLabel(collapseButton.element, referencesLabel, element.usedReferencesExpanded); - })); - - this._register(this.toolbar.onDidChangeDropdownVisibility(e => { - toolbarElement.classList.toggle('force-visibility', e); - })); - - this._configureForScreenReader(); - this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(() => this._configureForScreenReader())); - this._register(this.configurationService.onDidChangeConfiguration((e) => { - if (e.affectedKeys.has(AccessibilityVerbositySettingId.Chat)) { - this._configureForScreenReader(); - } - })); - - this._register(this.options.onDidChange(() => { - this.editor.updateOptions(this.getEditorOptionsFromConfig()); - })); - - this._register(this.editor.onDidScrollChange(e => { - this.currentScrollWidth = e.scrollWidth; - })); - this._register(this.editor.onDidContentSizeChange(e => { - if (e.contentHeightChanged) { - this._onDidChangeContentHeight.fire(); - } - })); - this._register(this.editor.onDidBlurEditorWidget(() => { - this.element.classList.remove('focused'); - WordHighlighterContribution.get(this.editor)?.stopHighlighting(); - this.clearWidgets(); - })); - this._register(this.editor.onDidFocusEditorWidget(() => { - this.element.classList.add('focused'); - WordHighlighterContribution.get(this.editor)?.restoreViewState(true); - })); - - // Parent list scrolled - if (delegate.onDidScroll) { - this._register(delegate.onDidScroll(e => { - this.clearWidgets(); - })); - } - } - - get uri(): URI | undefined { - return this.editor.getModel()?.uri; - } - - private createEditor(instantiationService: IInstantiationService, parent: HTMLElement, options: Readonly): CodeEditorWidget { - return this._register(instantiationService.createInstance(CodeEditorWidget, parent, options, { - isSimpleWidget: false, - contributions: EditorExtensionsRegistry.getSomeEditorContributions([ - MenuPreventer.ID, - SelectionClipboardContributionID, - ContextMenuController.ID, - - WordHighlighterContribution.ID, - ViewportSemanticTokensContribution.ID, - BracketMatchingController.ID, - SmartSelectController.ID, - ContentHoverController.ID, - MessageController.ID, - GotoDefinitionAtPositionEditorContribution.ID, - ColorDetector.ID - ]) - })); - } - - focus(): void { - this.editor.focus(); - } - - private updatePaddingForLayout() { - // scrollWidth = "the width of the content that needs to be scrolled" - // contentWidth = "the width of the area where content is displayed" - const horizontalScrollbarVisible = this.currentScrollWidth > this.editor.getLayoutInfo().contentWidth; - const scrollbarHeight = this.editor.getLayoutInfo().horizontalScrollbarHeight; - const bottomPadding = horizontalScrollbarVisible ? - Math.max(defaultCodeblockPadding - scrollbarHeight, 2) : - defaultCodeblockPadding; - this.editor.updateOptions({ padding: { top: defaultCodeblockPadding, bottom: bottomPadding } }); - } - - private _configureForScreenReader(): void { - const toolbarElt = this.toolbar.getElement(); - if (this.accessibilityService.isScreenReaderOptimized()) { - toolbarElt.style.display = 'block'; - toolbarElt.ariaLabel = this.configurationService.getValue(AccessibilityVerbositySettingId.Chat) ? localize('aideChat.codeBlock.toolbarVerbose', 'Toolbar for code block which can be reached via tab') : localize('aideChat.codeBlock.toolbar', 'Code block toolbar'); - } else { - toolbarElt.style.display = ''; - } - } - - private getEditorOptionsFromConfig(): IEditorOptions { - return { - wordWrap: this.options.configuration.resultEditor.wordWrap, - fontLigatures: this.options.configuration.resultEditor.fontLigatures, - bracketPairColorization: this.options.configuration.resultEditor.bracketPairColorization, - fontFamily: this.options.configuration.resultEditor.fontFamily === 'default' ? - EDITOR_FONT_DEFAULTS.fontFamily : - this.options.configuration.resultEditor.fontFamily, - fontSize: this.options.configuration.resultEditor.fontSize, - fontWeight: this.options.configuration.resultEditor.fontWeight, - lineHeight: this.options.configuration.resultEditor.lineHeight, - }; - } - - layout(width: number): void { - const contentHeight = this.getContentHeight(); - const editorBorder = 2; - this.editor.layout({ width: width - editorBorder, height: contentHeight }); - this.updatePaddingForLayout(); - } - - private getContentHeight() { - if (this.currentCodeBlockData?.range) { - const lineCount = this.currentCodeBlockData.range.endLineNumber - this.currentCodeBlockData.range.startLineNumber + 1; - const lineHeight = this.editor.getOption(EditorOption.lineHeight); - return lineCount * lineHeight; - } - return this.editor.getContentHeight(); - } - - async render(data: ICodeBlockData, width: number, editable: boolean | undefined) { - this.currentCodeBlockData = data; - if (data.parentContextKeyService) { - this.contextKeyService.updateParent(data.parentContextKeyService); - } - - if (this.options.configuration.resultEditor.wordWrap === 'on') { - // Initialize the editor with the new proper width so that getContentHeight - // will be computed correctly in the next call to layout() - this.layout(width); - } - - await this.updateEditor(data); - - this.layout(width); - if (editable) { - this.disposableStore.clear(); - this.disposableStore.add(this.editor.onDidFocusEditorWidget(() => TabFocus.setTabFocusMode(true))); - this.disposableStore.add(this.editor.onDidBlurEditorWidget(() => TabFocus.setTabFocusMode(false))); - } - this.editor.updateOptions({ ariaLabel: localize('aideChat.codeBlockLabel', "Code block {0}", data.codeBlockIndex + 1), readOnly: !editable }); - - if (data.hideToolbar) { - dom.hide(this.toolbar.getElement()); - } else { - dom.show(this.toolbar.getElement()); - } - - if (data.vulns?.length && isResponseVM(data.element)) { - dom.clearNode(this.vulnsListElement); - this.element.classList.remove('no-vulns'); - this.element.classList.toggle('chat-vulnerabilities-collapsed', !data.element.vulnerabilitiesListExpanded); - dom.append(this.vulnsListElement, ...data.vulns.map(v => $('li', undefined, $('span.chat-vuln-title', undefined, v.title), ' ' + v.description))); - this.vulnsButton.label = this.getVulnerabilitiesLabel(); - } else { - this.element.classList.add('no-vulns'); - } - } - - reset() { - this.clearWidgets(); - } - - private clearWidgets() { - ContentHoverController.get(this.editor)?.hideContentHover(); - } - - private async updateEditor(data: ICodeBlockData): Promise { - const textModel = (await data.textModel).textEditorModel; - this.editor.setModel(textModel); - if (data.range) { - this.editor.setSelection(data.range); - this.editor.revealRangeInCenter(data.range, ScrollType.Immediate); - } - - this.toolbar.context = { - code: textModel.getTextBuffer().getValueInRange(data.range ?? textModel.getFullModelRange(), EndOfLinePreference.TextDefined), - codeBlockIndex: data.codeBlockIndex, - element: data.element, - languageId: textModel.getLanguageId() - } satisfies ICodeBlockActionContext; - } - - private getVulnerabilitiesLabel(): string { - if (!this.currentCodeBlockData || !this.currentCodeBlockData.vulns) { - return ''; - } - - const referencesLabel = this.currentCodeBlockData.vulns.length > 1 ? - localize('vulnerabilitiesPlural', "{0} vulnerabilities", this.currentCodeBlockData.vulns.length) : - localize('vulnerabilitiesSingular', "{0} vulnerability", 1); - const icon = (element: IChatResponseViewModel) => element.vulnerabilitiesListExpanded ? Codicon.chevronDown : Codicon.chevronRight; - return `${referencesLabel} $(${icon(this.currentCodeBlockData.element as IChatResponseViewModel).id})`; - } -} - -export class ChatCodeBlockContentProvider extends Disposable implements ITextModelContentProvider { - - constructor( - @ITextModelService textModelService: ITextModelService, - @IModelService private readonly _modelService: IModelService, - ) { - super(); - this._register(textModelService.registerTextModelContentProvider(Schemas.vscodeChatCodeBlock, this)); - } - - async provideTextContent(resource: URI): Promise { - const existing = this._modelService.getModel(resource); - if (existing) { - return existing; - } - return this._modelService.createModel('', null, resource); - } -} - -// - -export interface ICodeCompareBlockActionContext { - readonly element: IChatResponseViewModel; - readonly diffEditor: IDiffEditor; - readonly edit: IChatTextEditGroup; -} - -export interface ICodeCompareBlockDiffData { - modified: ITextModel; - original: ITextModel; - originalSha1: string; -} - -export interface ICodeCompareBlockData { - readonly element: ChatTreeItem; - - readonly edit: IChatTextEditGroup; - - readonly diffData: Promise; - - readonly parentContextKeyService?: IContextKeyService; - // readonly hideToolbar?: boolean; -} - - -export class CodeCompareBlockPart extends Disposable { - protected readonly _onDidChangeContentHeight = this._register(new Emitter()); - public readonly onDidChangeContentHeight = this._onDidChangeContentHeight.event; - - private readonly contextKeyService: IContextKeyService; - private readonly diffEditor: DiffEditorWidget; - private readonly resourceLabel: ResourceLabel; - private readonly toolbar: MenuWorkbenchToolBar; - readonly element: HTMLElement; - private readonly messageElement: HTMLElement; - - private readonly _lastDiffEditorViewModel = this._store.add(new MutableDisposable()); - private currentScrollWidth = 0; - - constructor( - private readonly options: ChatEditorOptions, - readonly menuId: MenuId, - delegate: IChatRendererDelegate, - overflowWidgetsDomNode: HTMLElement | undefined, - @IInstantiationService instantiationService: IInstantiationService, - @IContextKeyService contextKeyService: IContextKeyService, - @IModelService protected readonly modelService: IModelService, - @IConfigurationService private readonly configurationService: IConfigurationService, - @IAccessibilityService private readonly accessibilityService: IAccessibilityService, - @ILabelService private readonly labelService: ILabelService, - @IOpenerService private readonly openerService: IOpenerService, - ) { - super(); - this.element = $('.interactive-result-code-block'); - this.element.classList.add('compare'); - - this.messageElement = dom.append(this.element, $('.message')); - this.messageElement.setAttribute('role', 'status'); - this.messageElement.tabIndex = 0; - - this.contextKeyService = this._register(contextKeyService.createScoped(this.element)); - const scopedInstantiationService = instantiationService.createChild(new ServiceCollection([IContextKeyService, this.contextKeyService])); - const editorHeader = dom.append(this.element, $('.interactive-result-header.show-file-icons')); - const editorElement = dom.append(this.element, $('.interactive-result-editor')); - this.diffEditor = this.createDiffEditor(scopedInstantiationService, editorElement, { - ...getSimpleEditorOptions(this.configurationService), - lineNumbers: 'on', - selectOnLineNumbers: true, - scrollBeyondLastLine: false, - lineDecorationsWidth: 12, - dragAndDrop: false, - padding: { top: defaultCodeblockPadding, bottom: defaultCodeblockPadding }, - mouseWheelZoom: false, - scrollbar: { - vertical: 'hidden', - alwaysConsumeMouseWheel: false - }, - definitionLinkOpensInPeek: false, - gotoLocation: { - multiple: 'goto', - multipleDeclarations: 'goto', - multipleDefinitions: 'goto', - multipleImplementations: 'goto', - }, - ariaLabel: localize('aideChat.codeBlockHelp', 'Code block'), - overflowWidgetsDomNode, - ...this.getEditorOptionsFromConfig(), - }); - - this.resourceLabel = this._register(scopedInstantiationService.createInstance(ResourceLabel, editorHeader, { supportIcons: true })); - - const editorScopedService = this.diffEditor.getModifiedEditor().contextKeyService.createScoped(editorHeader); - const editorScopedInstantiationService = scopedInstantiationService.createChild(new ServiceCollection([IContextKeyService, editorScopedService])); - this.toolbar = this._register(editorScopedInstantiationService.createInstance(MenuWorkbenchToolBar, editorHeader, menuId, { - menuOptions: { - shouldForwardArgs: true - } - })); - - this._configureForScreenReader(); - this._register(this.accessibilityService.onDidChangeScreenReaderOptimized(() => this._configureForScreenReader())); - this._register(this.configurationService.onDidChangeConfiguration((e) => { - if (e.affectedKeys.has(AccessibilityVerbositySettingId.Chat)) { - this._configureForScreenReader(); - } - })); - - this._register(this.options.onDidChange(() => { - this.diffEditor.updateOptions(this.getEditorOptionsFromConfig()); - })); - - this._register(this.diffEditor.getModifiedEditor().onDidScrollChange(e => { - this.currentScrollWidth = e.scrollWidth; - })); - this._register(this.diffEditor.onDidContentSizeChange(e => { - if (e.contentHeightChanged) { - this._onDidChangeContentHeight.fire(); - } - })); - this._register(this.diffEditor.getModifiedEditor().onDidBlurEditorWidget(() => { - this.element.classList.remove('focused'); - WordHighlighterContribution.get(this.diffEditor.getModifiedEditor())?.stopHighlighting(); - this.clearWidgets(); - })); - this._register(this.diffEditor.getModifiedEditor().onDidFocusEditorWidget(() => { - this.element.classList.add('focused'); - WordHighlighterContribution.get(this.diffEditor.getModifiedEditor())?.restoreViewState(true); - })); - - - // Parent list scrolled - if (delegate.onDidScroll) { - this._register(delegate.onDidScroll(e => { - this.clearWidgets(); - })); - } - } - - get uri(): URI | undefined { - return this.diffEditor.getModifiedEditor().getModel()?.uri; - } - - private createDiffEditor(instantiationService: IInstantiationService, parent: HTMLElement, options: Readonly): DiffEditorWidget { - const widgetOptions: ICodeEditorWidgetOptions = { - isSimpleWidget: false, - contributions: EditorExtensionsRegistry.getSomeEditorContributions([ - MenuPreventer.ID, - SelectionClipboardContributionID, - ContextMenuController.ID, - - WordHighlighterContribution.ID, - ViewportSemanticTokensContribution.ID, - BracketMatchingController.ID, - SmartSelectController.ID, - ContentHoverController.ID, - GotoDefinitionAtPositionEditorContribution.ID, - ]) - }; - - return this._register(instantiationService.createInstance(DiffEditorWidget, parent, { - scrollbar: { useShadows: false, alwaysConsumeMouseWheel: false, ignoreHorizontalScrollbarInContentHeight: true, }, - renderMarginRevertIcon: false, - diffCodeLens: false, - scrollBeyondLastLine: false, - stickyScroll: { enabled: false }, - originalAriaLabel: localize('original', 'Original'), - modifiedAriaLabel: localize('modified', 'Modified'), - diffAlgorithm: 'advanced', - readOnly: false, - isInEmbeddedEditor: true, - useInlineViewWhenSpaceIsLimited: false, - hideUnchangedRegions: { enabled: true, contextLineCount: 1 }, - renderGutterMenu: false, - ...options - }, { originalEditor: widgetOptions, modifiedEditor: widgetOptions })); - } - - focus(): void { - this.diffEditor.focus(); - } - - private updatePaddingForLayout() { - // scrollWidth = "the width of the content that needs to be scrolled" - // contentWidth = "the width of the area where content is displayed" - const horizontalScrollbarVisible = this.currentScrollWidth > this.diffEditor.getModifiedEditor().getLayoutInfo().contentWidth; - const scrollbarHeight = this.diffEditor.getModifiedEditor().getLayoutInfo().horizontalScrollbarHeight; - const bottomPadding = horizontalScrollbarVisible ? - Math.max(defaultCodeblockPadding - scrollbarHeight, 2) : - defaultCodeblockPadding; - this.diffEditor.updateOptions({ padding: { top: defaultCodeblockPadding, bottom: bottomPadding } }); - } - - private _configureForScreenReader(): void { - const toolbarElt = this.toolbar.getElement(); - if (this.accessibilityService.isScreenReaderOptimized()) { - toolbarElt.style.display = 'block'; - toolbarElt.ariaLabel = this.configurationService.getValue(AccessibilityVerbositySettingId.Chat) ? localize('aideChat.codeBlock.toolbarVerbose', 'Toolbar for code block which can be reached via tab') : localize('aideChat.codeBlock.toolbar', 'Code block toolbar'); - } else { - toolbarElt.style.display = ''; - } - } - - private getEditorOptionsFromConfig(): IEditorOptions { - return { - wordWrap: this.options.configuration.resultEditor.wordWrap, - fontLigatures: this.options.configuration.resultEditor.fontLigatures, - bracketPairColorization: this.options.configuration.resultEditor.bracketPairColorization, - fontFamily: this.options.configuration.resultEditor.fontFamily === 'default' ? - EDITOR_FONT_DEFAULTS.fontFamily : - this.options.configuration.resultEditor.fontFamily, - fontSize: this.options.configuration.resultEditor.fontSize, - fontWeight: this.options.configuration.resultEditor.fontWeight, - lineHeight: this.options.configuration.resultEditor.lineHeight, - }; - } - - layout(width: number): void { - const contentHeight = this.getContentHeight(); - const editorBorder = 2; - const dimension = { width: width - editorBorder, height: contentHeight }; - this.element.style.height = `${dimension.height}px`; - this.element.style.width = `${dimension.width}px`; - this.diffEditor.layout(dimension); - this.updatePaddingForLayout(); - } - - private getContentHeight() { - return this.diffEditor.getContentHeight(); - } - - async render(data: ICodeCompareBlockData, width: number, token: CancellationToken) { - if (data.parentContextKeyService) { - this.contextKeyService.updateParent(data.parentContextKeyService); - } - - if (this.options.configuration.resultEditor.wordWrap === 'on') { - // Initialize the editor with the new proper width so that getContentHeight - // will be computed correctly in the next call to layout() - this.layout(width); - } - - await this.updateEditor(data, token); - - this.layout(width); - this.diffEditor.updateOptions({ ariaLabel: localize('aideChat.compareCodeBlockLabel', "Code Edits") }); - - this.resourceLabel.element.setFile(data.edit.uri, { - fileKind: FileKind.FILE, - fileDecorations: { colors: true, badges: false } - }); - } - - reset() { - this.clearWidgets(); - } - - private clearWidgets() { - ContentHoverController.get(this.diffEditor.getOriginalEditor())?.hideContentHover(); - } - - private async updateEditor(data: ICodeCompareBlockData, token: CancellationToken): Promise { - - if (!isResponseVM(data.element)) { - return; - } - - const isEditApplied = Boolean(data.edit.state?.applied ?? 0); - - CONTEXT_CHAT_EDIT_APPLIED.bindTo(this.contextKeyService).set(isEditApplied); - - this.element.classList.toggle('no-diff', isEditApplied); - - if (data.edit.state?.applied) { - - const uriLabel = this.labelService.getUriLabel(data.edit.uri, { relative: true, noPrefix: true }); - - let template: string; - if (data.edit.state.applied === 1) { - template = localize('aideChat.edits.1', "Made 1 change in [[``{0}``]]", uriLabel); - } else if (data.edit.state.applied < 0) { - template = localize('aideChat.edits.rejected', "Edits in [[``{0}``]] have been rejected", uriLabel); - } else { - template = localize('aideChat.edits.N', "Made {0} changes in [[``{1}``]]", data.edit.state.applied, uriLabel); - } - - const message = renderFormattedText(template, { - renderCodeSegments: true, - actionHandler: { - callback: () => { - this.openerService.open(data.edit.uri, { fromUserGesture: true, allowCommands: false }); - }, - disposables: this._store, - } - }); - - dom.reset(this.messageElement, message); - - } - - const diffData = await data.diffData; - if (!diffData) { - return; - } - - if (!isEditApplied) { - const viewModel = this.diffEditor.createViewModel({ - original: diffData.original, - modified: diffData.modified - }); - - await viewModel.waitForDiff(); - - if (token.isCancellationRequested) { - return; - } - - this.diffEditor.setModel(viewModel); - this._lastDiffEditorViewModel.value = viewModel; - - } else { - this.diffEditor.setModel(null); - this._lastDiffEditorViewModel.value = undefined; - } - - this.toolbar.context = { - edit: data.edit, - element: data.element, - diffEditor: this.diffEditor, - } satisfies ICodeCompareBlockActionContext; - } -} - -export class DefaultChatTextEditor { - - private readonly _sha1 = new DefaultModelSHA1Computer(); - - constructor( - @ITextModelService private readonly modelService: ITextModelService, - @ICodeEditorService private readonly editorService: ICodeEditorService, - @IDialogService private readonly dialogService: IDialogService, - ) { } - - async apply(response: IChatResponseModel | IChatResponseViewModel, item: IChatTextEditGroup, diffEditor: IDiffEditor | undefined): Promise { - - if (!response.response.value.includes(item)) { - // bogous item - return; - } - - if (item.state?.applied) { - // already applied - return; - } - - if (!diffEditor) { - for (const candidate of this.editorService.listDiffEditors()) { - if (!candidate.getContainerDomNode().isConnected) { - continue; - } - const model = candidate.getModel(); - if (!model || !isEqual(model.original.uri, item.uri) || model.modified.uri.scheme !== Schemas.vscodeChatCodeCompareBlock) { - diffEditor = candidate; - break; - } - } - } - - const edits = diffEditor - ? await this._applyWithDiffEditor(diffEditor, item) - : await this._apply(item); - - response.setEditApplied(item, edits); - } - - private async _applyWithDiffEditor(diffEditor: IDiffEditor, item: IChatTextEditGroup) { - const model = diffEditor.getModel(); - if (!model) { - return 0; - } - - const diff = diffEditor.getDiffComputationResult(); - if (!diff || diff.identical) { - return 0; - } - - - if (!await this._checkSha1(model.original, item)) { - return 0; - } - - const modified = new TextModelText(model.modified); - const edits = diff.changes2.map(i => i.toRangeMapping().toTextEdit(modified).toSingleEditOperation()); - - model.original.pushStackElement(); - model.original.pushEditOperations(null, edits, () => null); - model.original.pushStackElement(); - - return edits.length; - } - - private async _apply(item: IChatTextEditGroup) { - const ref = await this.modelService.createModelReference(item.uri); - try { - - if (!await this._checkSha1(ref.object.textEditorModel, item)) { - return 0; - } - - ref.object.textEditorModel.pushStackElement(); - let total = 0; - for (const group of item.edits) { - const edits = group.map(TextEdit.asEditOperation); - ref.object.textEditorModel.pushEditOperations(null, edits, () => null); - total += edits.length; - } - ref.object.textEditorModel.pushStackElement(); - return total; - - } finally { - ref.dispose(); - } - } - - private async _checkSha1(model: ITextModel, item: IChatTextEditGroup) { - if (item.state?.sha1 && this._sha1.computeSHA1(model) && this._sha1.computeSHA1(model) !== item.state.sha1) { - const result = await this.dialogService.confirm({ - message: localize('aideChat.compare.apply.confirm', "The original file has been modified."), - detail: localize('aideChat.compare.apply.confirm.detail', "Do you want to apply the changes anyway?"), - }); - - if (!result.confirmed) { - return false; - } - } - return true; - } - - discard(response: IChatResponseModel | IChatResponseViewModel, item: IChatTextEditGroup) { - if (!response.response.value.includes(item)) { - // bogous item - return; - } - - if (item.state?.applied) { - // already applied - return; - } - - response.setEditApplied(item, -1); - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/contrib/aideChatContextAttachments.ts b/src/vs/workbench/contrib/aideChat/browser/contrib/aideChatContextAttachments.ts deleted file mode 100644 index ceb7632be43..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/contrib/aideChatContextAttachments.ts +++ /dev/null @@ -1,80 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Emitter } from '../../../../../base/common/event.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { IChatWidget } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { ChatWidget, IChatWidgetContrib } from '../../../../../workbench/contrib/aideChat/browser/aideChatWidget.js'; -import { IAideChatRequestVariableEntry } from '../../../../../workbench/contrib/aideChat/common/aideChatModel.js'; - -export class ChatContextAttachments extends Disposable implements IChatWidgetContrib { - - private _attachedContext = new Set(); - - private readonly _onDidChangeInputState = this._register(new Emitter()); - readonly onDidChangeInputState = this._onDidChangeInputState.event; - - public static readonly ID = 'chatContextAttachments'; - - get id() { - return ChatContextAttachments.ID; - } - - constructor(readonly widget: IChatWidget) { - super(); - - this._register(this.widget.onDidDeleteContext((e) => { - this._removeContext(e); - })); - - this._register(this.widget.onDidSubmitAgent(() => { - this._clearAttachedContext(); - })); - } - - getInputState(): IAideChatRequestVariableEntry[] { - return [...this._attachedContext.values()]; - } - - setInputState(s: any): void { - if (!Array.isArray(s)) { - s = []; - } - - this._attachedContext.clear(); - for (const attachment of s) { - this._attachedContext.add(attachment); - } - - this.widget.setContext(true, ...s); - } - - getContext() { - return new Set([...this._attachedContext.values()].map((v) => v.id)); - } - - setContext(overwrite: boolean, ...attachments: IAideChatRequestVariableEntry[]) { - if (overwrite) { - this._attachedContext.clear(); - } - for (const attachment of attachments) { - this._attachedContext.add(attachment); - } - - this.widget.setContext(overwrite, ...attachments); - this._onDidChangeInputState.fire(); - } - - private _removeContext(attachment: IAideChatRequestVariableEntry) { - this._attachedContext.delete(attachment); - this._onDidChangeInputState.fire(); - } - - private _clearAttachedContext() { - this._attachedContext.clear(); - } -} - -ChatWidget.CONTRIBS.push(ChatContextAttachments); diff --git a/src/vs/workbench/contrib/aideChat/browser/contrib/aideChatDynamicVariables.ts b/src/vs/workbench/contrib/aideChat/browser/contrib/aideChatDynamicVariables.ts deleted file mode 100644 index f2f4c27f272..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/contrib/aideChatDynamicVariables.ts +++ /dev/null @@ -1,550 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { coalesce } from '../../../../../base/common/arrays.js'; -import { Emitter } from '../../../../../base/common/event.js'; -import { IMarkdownString, MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js'; -import { Disposable } from '../../../../../base/common/lifecycle.js'; -import { basename } from '../../../../../base/common/resources.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; -import { EditorAction2 } from '../../../../../editor/browser/editorExtensions.js'; -import { IRange, Range } from '../../../../../editor/common/core/range.js'; -import { IDecorationOptions } from '../../../../../editor/common/editorCommon.js'; -import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js'; -import { Command } from '../../../../../editor/common/languages.js'; -import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; -import { ITextModelService } from '../../../../../editor/common/services/resolverService.js'; -import { SuggestController } from '../../../../../editor/contrib/suggest/browser/suggestController.js'; -import { localize2 } from '../../../../../nls.js'; -import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; -import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { ILabelService } from '../../../../../platform/label/common/label.js'; -import { ILogService } from '../../../../../platform/log/common/log.js'; -import { IChatWidget, showChatView } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { ChatWidget, IChatWidgetContrib } from '../../../../../workbench/contrib/aideChat/browser/aideChatWidget.js'; -import { CONTEXT_CHAT_ENABLED } from '../../../../../workbench/contrib/aideChat/common/aideChatContextKeys.js'; -import { chatVariableLeader } from '../../../../../workbench/contrib/aideChat/common/aideChatParserTypes.js'; -import { IAideChatRequestVariableValue, IDynamicVariable } from '../../../../../workbench/contrib/aideChat/common/aideChatVariables.js'; -import { ISymbolQuickPickItem } from '../../../../../workbench/contrib/search/browser/symbolsQuickAccess.js'; -import { IViewsService } from '../../../../../workbench/services/views/common/viewsService.js'; - -export const dynamicVariableDecorationType = 'chat-dynamic-variable'; - -export const FolderReferenceCompletionProviderName = 'chatInplaceFolderReferenceCompletionProvider'; -export const FileReferenceCompletionProviderName = 'chatInplaceFileReferenceCompletionProvider'; -export const CodeSymbolCompletionProviderName = 'chatInplaceCodeCompletionProvider'; - -export interface IWidgetWithInputEditor { - inputEditor: ICodeEditor; - getContrib(id: string): T | undefined; -} - -export class ChatDynamicVariableModel extends Disposable implements IChatWidgetContrib { - public static readonly ID = 'chatDynamicVariableModel'; - - private _variables: IDynamicVariable[] = []; - get variables(): ReadonlyArray { - return [...this._variables]; - } - - get id() { - return ChatDynamicVariableModel.ID; - } - - private _onDidChangeInputState = this._register(new Emitter()); - readonly onDidChangeInputState = this._onDidChangeInputState.event; - - constructor( - private readonly widget: IWidgetWithInputEditor, - @ILabelService private readonly labelService: ILabelService, - ) { - super(); - this._register(widget.inputEditor.onDidChangeModelContent(e => { - e.changes.forEach(c => { - // Don't mutate entries in _variables, since they will be returned from the getter - const originalNumVariables = this._variables.length; - this._variables = coalesce(this._variables.map(ref => { - const intersection = Range.intersectRanges(ref.range, c.range); - if (intersection && !intersection.isEmpty()) { - // The reference text was changed, it's broken. - // But if the whole reference range was deleted (eg history navigation) then don't try to change the editor. - if (!Range.containsRange(c.range, ref.range)) { - const rangeToDelete = new Range(ref.range.startLineNumber, ref.range.startColumn, ref.range.endLineNumber, ref.range.endColumn - 1); - this.widget.inputEditor.executeEdits(this.id, [{ - range: rangeToDelete, - text: '', - }]); - } - return null; - } else if (Range.compareRangesUsingStarts(ref.range, c.range) > 0) { - const delta = c.text.length - c.rangeLength; - return { - ...ref, - range: { - startLineNumber: ref.range.startLineNumber, - startColumn: ref.range.startColumn + delta, - endLineNumber: ref.range.endLineNumber, - endColumn: ref.range.endColumn + delta - } - }; - } - - return ref; - })); - - if (this._variables.length !== originalNumVariables) { - this._onDidChangeInputState.fire(); - } - }); - - this.updateDecorations(); - })); - } - - getInputState(): any { - return this.variables; - } - - setInputState(s: any): void { - if (!Array.isArray(s)) { - s = []; - } - - this._variables = s; - this.updateDecorations(); - } - - addReference(ref: IDynamicVariable): void { - this._variables.push(ref); - this.updateDecorations(); - this._onDidChangeInputState.fire(); - } - - private updateDecorations(): void { - this.widget.inputEditor.setDecorationsByType('aideChat', dynamicVariableDecorationType, this._variables.map(r => ({ - range: r.range, - hoverMessage: this.getHoverForReference(r) - }))); - } - - private getHoverForReference(ref: IDynamicVariable): string | IMarkdownString { - const value = ref.data; - if (URI.isUri(value)) { - return new MarkdownString(this.labelService.getUriLabel(value, { relative: true })); - } else { - return (value as any).toString(); - } - } -} - -ChatWidget.CONTRIBS.push(ChatDynamicVariableModel); -//AideControls.INPUT_CONTRIBS.push(ChatDynamicVariableModel); - -interface MultiLevelCodeTriggerActionContext { - inputEditor: ICodeEditor; - range: IRange; - pick: 'file' | 'code' | 'folder'; -} - -function isMultiLevelCodeTriggerActionContext(context: any): context is MultiLevelCodeTriggerActionContext { - return 'inputEditor' in context && 'range' in context && 'pick' in context; -} - -export class MultiLevelCodeTriggerAction extends Action2 { - static readonly ID = 'workbench.action.aideChat.multiLevelCodeTrigger'; - - constructor() { - super({ - id: MultiLevelCodeTriggerAction.ID, - title: '' // not displayed - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - const languageFeaturesService = accessor.get(ILanguageFeaturesService); - - const context = args[0]; - if (!isMultiLevelCodeTriggerActionContext(context)) { - return; - } - - const inputEditor = context.inputEditor; - const doCleanup = () => { - // Failed, remove the dangling prefix - inputEditor.executeEdits('chatMultiLevelCodeTrigger', [{ range: context.range, text: `` }]); - }; - - const suggestController = SuggestController.get(inputEditor); - if (!suggestController) { - doCleanup(); - return; - } - - const completionProviders = languageFeaturesService.completionProvider.getForAllLanguages(); - const completionProvider = completionProviders.find( - provider => provider._debugDisplayName === ( - context.pick === 'code' ? CodeSymbolCompletionProviderName : context.pick === 'file' ? FileReferenceCompletionProviderName : FolderReferenceCompletionProviderName - )); - - if (!completionProvider) { - doCleanup(); - return; - } - - suggestController.triggerSuggest(new Set([completionProvider])); - } -} -registerAction2(MultiLevelCodeTriggerAction); - -interface SelectAndInsertFileActionContext { - widget: IWidgetWithInputEditor; - range: IRange; - uri: URI; -} - -function isSelectAndInsertFileActionContext(context: any): context is SelectAndInsertFileActionContext { - return 'widget' in context && 'range' in context && 'uri' in context; -} - -export class SelectAndInsertFolderAction extends Action2 { - static readonly ID = 'workbench.action.aideChat.selectAndInsertFolder'; - - constructor() { - super({ - id: SelectAndInsertFolderAction.ID, - title: '' // not displayed - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - const logService = accessor.get(ILogService); - - const context = args[0]; - if (!isSelectAndInsertFileActionContext(context)) { - return; - } - - const doCleanup = () => { - // Failed, remove the dangling `folder` - context.widget.inputEditor.executeEdits('chatInsertFolder', [{ range: context.range, text: `` }]); - }; - - const resource = context.uri; - if (!resource) { - logService.trace('SelectAndInsertFolderAction: no resource selected'); - doCleanup(); - return; - } - - const fileName = basename(resource); - const editor = context.widget.inputEditor; - const text = `${chatVariableLeader}folder:${fileName}`; - const range = context.range; - const success = editor.executeEdits('chatInsertFolder', [{ range, text: text + ' ' }]); - if (!success) { - logService.trace(`SelectAndInsertFolderAction: failed to insert "${text}"`); - doCleanup(); - return; - } - - context.widget.getContrib(ChatDynamicVariableModel.ID)?.addReference({ - id: 'vscode.folder', - range: { startLineNumber: range.startLineNumber, startColumn: range.startColumn, endLineNumber: range.endLineNumber, endColumn: range.startColumn + text.length }, - data: resource - }); - } -} -registerAction2(SelectAndInsertFolderAction); - -export class SelectAndInsertFileAction extends Action2 { - static readonly ID = 'workbench.action.aideChat.selectAndInsertFile'; - - constructor() { - super({ - id: SelectAndInsertFileAction.ID, - title: '' // not displayed - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - const textModelService = accessor.get(ITextModelService); - const logService = accessor.get(ILogService); - - const context = args[0]; - if (!isSelectAndInsertFileActionContext(context)) { - return; - } - - const doCleanup = () => { - // Failed, remove the dangling `file` - context.widget.inputEditor.executeEdits('chatInsertFile', [{ range: context.range, text: `` }]); - }; - - const resource = context.uri; - if (!resource) { - logService.trace('SelectAndInsertFileAction: no resource selected'); - doCleanup(); - return; - } - - const model = await textModelService.createModelReference(resource); - const fileRange = model.object.textEditorModel.getFullModelRange(); - model.dispose(); - - const fileName = basename(resource); - const editor = context.widget.inputEditor; - const text = `${chatVariableLeader}file:${fileName}`; - const range = context.range; - const success = editor.executeEdits('chatInsertFile', [{ range, text: text + ' ' }]); - if (!success) { - logService.trace(`SelectAndInsertFileAction: failed to insert "${text}"`); - doCleanup(); - return; - } - - const valueObj = { uri: resource, range: fileRange }; - const value = JSON.stringify(valueObj); - context.widget.getContrib(ChatDynamicVariableModel.ID)?.addReference({ - id: 'vscode.file', - range: { startLineNumber: range.startLineNumber, startColumn: range.startColumn, endLineNumber: range.endLineNumber, endColumn: range.startColumn + text.length }, - data: value - }); - } -} -registerAction2(SelectAndInsertFileAction); - -interface SelectAndInsertCodeActionContext { - widget: IWidgetWithInputEditor; - range: IRange; - pick: ISymbolQuickPickItem; -} - -function isSelectAndInsertCodeActionContext(context: any): context is SelectAndInsertCodeActionContext { - return 'widget' in context && 'range' in context && 'pick' in context; -} - -export class SelectAndInsertCodeAction extends Action2 { - static readonly ID = 'workbench.action.aideChat.selectAndInsertCode'; - - constructor() { - super({ - id: SelectAndInsertCodeAction.ID, - title: '' // not displayed - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - const logService = accessor.get(ILogService); - - const context = args[0]; - if (!isSelectAndInsertCodeActionContext(context)) { - return; - } - - const doCleanup = () => { - // Failed, remove the dangling `code` - context.widget.inputEditor.executeEdits('chatInsertCode', [{ range: context.range, text: `` }]); - }; - - const pick = context.pick; - if (!pick || !pick.resource) { - logService.trace('SelectAndInsertCodeAction: no resource selected'); - doCleanup(); - return; - } - - const selectionRange = pick.symbol?.location.range; - const result = parseVariableInfo(pick.label); - if (!result || !selectionRange) { - logService.trace('SelectAndInsertCodeAction: failed to parse code symbol'); - doCleanup(); - return; - } - - const [symbolName, symbolType] = result; - const editor = context.widget.inputEditor; - const text = `${chatVariableLeader}${symbolType}:${symbolName}`; - const range = context.range; - const success = editor.executeEdits('chatInsertCode', [{ range, text: text + ' ' }]); - if (!success) { - logService.trace(`SelectAndInsertCodeAction: failed to insert "${text}"`); - doCleanup(); - return; - } - - const valueObj = { uri: pick.resource, range: selectionRange }; - const value = JSON.stringify(valueObj); - context.widget.getContrib(ChatDynamicVariableModel.ID)?.addReference({ - id: 'vscode.codeSymbol', - range: { startLineNumber: range.startLineNumber, startColumn: range.startColumn, endLineNumber: range.endLineNumber, endColumn: range.startColumn + text.length }, - data: value - }); - } -} -registerAction2(SelectAndInsertCodeAction); - -export const parseVariableInfo = (input: string): [string, string] | null => { - // Define a regular expression pattern to match the variable declaration. - const pattern = /\$\(([^)]+)\)\s*(\w+)/; - - // Use the regular expression to match and capture the variable type and name. - const match = input.match(pattern); - - if (match) { - // The first captured group (match[1]) is the variable type. - // The second captured group (match[2]) is the variable name. - let variableType = match[1]; - const variableName = match[2]; - - // Remove the "symbol-" part from the variable type. - variableType = variableType.replace(/^symbol-/, ''); - - return [variableName, variableType]; - } - - // Return null if no match is found. - return null; -}; - -class ChatAddContext extends EditorAction2 { - static readonly ID = 'workbench.action.aideChat.addContext'; - - constructor() { - super({ - id: ChatAddContext.ID, - title: localize2({ key: 'actions.chat.addContext', comment: ['Add context to the chat input box'] }, "Add Context"), - precondition: CONTEXT_CHAT_ENABLED, - keybinding: { - when: EditorContextKeys.textInputFocus, - primary: KeyMod.CtrlCmd | KeyCode.KeyL, - weight: KeybindingWeight.EditorContrib - } - }); - } - - async runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor): Promise { - const viewsService = accessor.get(IViewsService); - - const chatWidget = await showChatView(viewsService); - const editorModel = editor.getModel(); - if (!editorModel || !chatWidget) { - return; - } - - // get the current position from chatWidget and insert the context - const position = chatWidget.inputEditor.getPosition(); - if (!position) { - return; - } - const range = { - startLineNumber: position.lineNumber, - startColumn: position.column, - endLineNumber: position.lineNumber, - endColumn: position.column - }; - - const editorUri = editorModel.uri; - const selectedRange = editor.getSelection(); - if (editorUri && !selectedRange?.isEmpty() && selectedRange) { - const fileName = basename(editorUri); - let text = `${chatVariableLeader}file:${fileName}`; - - if (selectedRange.startLineNumber === selectedRange.endLineNumber) { - text += `:${selectedRange.startLineNumber}`; - } else { - text += `:${selectedRange.startLineNumber}-${selectedRange.endLineNumber}`; - } - - const success = chatWidget.inputEditor.executeEdits('chatAddContext', [{ range, text: text + ' ' }]); - if (!success) { - return; - } - - const valueObj = { uri: editorUri, range: selectedRange }; - const value = JSON.stringify(valueObj); - chatWidget.getContrib(ChatDynamicVariableModel.ID)?.addReference({ - id: 'vscode.chatContext', - range: { ...range, endColumn: range.endColumn + text.length }, - data: value - }); - - chatWidget.focusInput(); - } - } -} -registerAction2(ChatAddContext); - -export interface IAddDynamicVariableContext { - id: string; - widget: IChatWidget; - range: IRange; - variableData: IAideChatRequestVariableValue; - command?: Command; -} - -function isAddDynamicVariableContext(context: any): context is IAddDynamicVariableContext { - return 'widget' in context && - 'range' in context && - 'variableData' in context; -} - -export class AddDynamicVariableAction extends Action2 { - static readonly ID = 'workbench.action.aideChat.addDynamicVariable'; - - constructor() { - super({ - id: AddDynamicVariableAction.ID, - title: '' // not displayed - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - const context = args[0]; - if (!isAddDynamicVariableContext(context)) { - return; - } - - let range = context.range; - const variableData = context.variableData; - - const doCleanup = () => { - // Failed, remove the dangling variable prefix - context.widget.inputEditor.executeEdits('chatInsertDynamicVariableWithArguments', [{ range: context.range, text: `` }]); - }; - - // If this completion item has no command, return it directly - if (context.command) { - // Invoke the command on this completion item along with its args and return the result - const commandService = accessor.get(ICommandService); - const selection: string | undefined = await commandService.executeCommand(context.command.id, ...(context.command.arguments ?? [])); - if (!selection) { - doCleanup(); - return; - } - - // Compute new range and variableData - const insertText = ':' + selection; - const insertRange = new Range(range.startLineNumber, range.endColumn, range.endLineNumber, range.endColumn + insertText.length); - range = new Range(range.startLineNumber, range.startColumn, range.endLineNumber, range.endColumn + insertText.length); - const editor = context.widget.inputEditor; - const success = editor.executeEdits('chatInsertDynamicVariableWithArguments', [{ range: insertRange, text: insertText + ' ' }]); - if (!success) { - doCleanup(); - return; - } - } - - context.widget.getContrib(ChatDynamicVariableModel.ID)?.addReference({ - id: context.id, - range: range, - data: variableData - }); - } -} -registerAction2(AddDynamicVariableAction); diff --git a/src/vs/workbench/contrib/aideChat/browser/contrib/aideChatInputCompletions.ts b/src/vs/workbench/contrib/aideChat/browser/contrib/aideChatInputCompletions.ts deleted file mode 100644 index 77e0ba40e12..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/contrib/aideChatInputCompletions.ts +++ /dev/null @@ -1,439 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { Position } from '../../../../../editor/common/core/position.js'; -import { Range } from '../../../../../editor/common/core/range.js'; -import { IWordAtPosition, getWordAtText } from '../../../../../editor/common/core/wordHelper.js'; -import { CompletionContext, CompletionItem, CompletionItemKind, CompletionList } from '../../../../../editor/common/languages.js'; -import { ITextModel } from '../../../../../editor/common/model.js'; -import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; -import { localize } from '../../../../../nls.js'; -import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js'; -import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js'; -import { ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js'; -import { Registry } from '../../../../../platform/registry/common/platform.js'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../../workbench/common/contributions.js'; -import { SubmitAction } from '../../../../../workbench/contrib/aideChat/browser/actions/aideChatExecuteActions.js'; -import { IChatWidget, IAideChatWidgetService } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { ChatInputPart } from '../../../../../workbench/contrib/aideChat/browser/aideChatInputPart.js'; -import { MultiLevelCodeTriggerAction } from '../../../../../workbench/contrib/aideChat/browser/contrib/aideChatDynamicVariables.js'; -import { AideChatAgentLocation, getFullyQualifiedId, IChatAgentData, IAideChatAgentNameService, IAideChatAgentService } from '../../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestTextPart, ChatRequestVariablePart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from '../../../../../workbench/contrib/aideChat/common/aideChatParserTypes.js'; -import { IAideChatSlashCommandService } from '../../../../../workbench/contrib/aideChat/common/aideChatSlashCommands.js'; -import { IAideChatVariablesService } from '../../../../../workbench/contrib/aideChat/common/aideChatVariables.js'; -import { LifecyclePhase } from '../../../../../workbench/services/lifecycle/common/lifecycle.js'; - -class SlashCommandCompletions extends Disposable { - constructor( - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IAideChatWidgetService private readonly chatWidgetService: IAideChatWidgetService, - @IAideChatSlashCommandService private readonly chatSlashCommandService: IAideChatSlashCommandService - ) { - super(); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'globalSlashCommands', - triggerCharacters: ['/'], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.viewModel || (widget.location !== AideChatAgentLocation.Panel && widget.location !== AideChatAgentLocation.Notebook) /* TODO@jrieken - enable when agents are adopted*/) { - return null; - } - - const range = computeCompletionRanges(model, position, /\/\w*/g); - if (!range) { - return null; - } - - const parsedRequest = widget.parsedInput.parts; - const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart); - if (usedAgent) { - // No (classic) global slash commands when an agent is used - return; - } - - const slashCommands = this.chatSlashCommandService.getCommands(); - if (!slashCommands) { - return null; - } - - return { - suggestions: slashCommands.map((c, i): CompletionItem => { - const withSlash = `/${c.command}`; - return { - label: withSlash, - insertText: c.executeImmediately ? '' : `${withSlash} `, - detail: c.detail, - range: new Range(1, 1, 1, 1), - sortText: c.sortText ?? 'a'.repeat(i + 1), - kind: CompletionItemKind.Text, // The icons are disabled here anyway, - command: c.executeImmediately ? { id: SubmitAction.ID, title: withSlash, arguments: [{ widget, inputValue: `${withSlash} ` }] } : undefined, - }; - }) - }; - } - })); - } -} - -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(SlashCommandCompletions, LifecyclePhase.Eventually); - -class AgentCompletions extends Disposable { - constructor( - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IAideChatWidgetService private readonly chatWidgetService: IAideChatWidgetService, - @IAideChatAgentService private readonly chatAgentService: IAideChatAgentService, - @IAideChatAgentNameService private readonly chatAgentNameService: IAideChatAgentNameService, - ) { - super(); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatAgent', - triggerCharacters: ['@'], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.viewModel || (widget.location !== AideChatAgentLocation.Panel && widget.location !== AideChatAgentLocation.Notebook) /* TODO@jrieken - enable when agents are adopted*/) { - return null; - } - - const parsedRequest = widget.parsedInput.parts; - const usedAgent = parsedRequest.find(p => p instanceof ChatRequestAgentPart); - if (usedAgent && !Range.containsPosition(usedAgent.editorRange, position)) { - // Only one agent allowed - return; - } - - const range = computeCompletionRanges(model, position, /@\w*/g); - if (!range) { - return null; - } - - const agents = this.chatAgentService.getAgents() - .filter(a => !a.isDefault) - .filter(a => a.locations.includes(widget.location)); - - return { - suggestions: agents.map((agent, i): CompletionItem => { - const { label: agentLabel, isDupe } = this.getAgentCompletionDetails(agent); - return { - // Leading space is important because detail has no space at the start by design - label: isDupe ? - { label: agentLabel, description: agent.description, detail: ` (${agent.publisherDisplayName})` } : - agentLabel, - insertText: `${agentLabel} `, - detail: agent.description, - range: new Range(1, 1, 1, 1), - command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent: agent, widget } satisfies AssignSelectedAgentActionArgs] }, - kind: CompletionItemKind.Text, // The icons are disabled here anyway - }; - }) - }; - } - })); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatAgentSubcommand', - triggerCharacters: ['/'], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.viewModel || widget.location !== AideChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { - return; - } - - const range = computeCompletionRanges(model, position, /\/\w*/g); - if (!range) { - return null; - } - - const parsedRequest = widget.parsedInput.parts; - const usedAgentIdx = parsedRequest.findIndex((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); - if (usedAgentIdx < 0) { - return; - } - - const usedSubcommand = parsedRequest.find(p => p instanceof ChatRequestAgentSubcommandPart); - if (usedSubcommand) { - // Only one allowed - return; - } - - for (const partAfterAgent of parsedRequest.slice(usedAgentIdx + 1)) { - // Could allow text after 'position' - if (!(partAfterAgent instanceof ChatRequestTextPart) || !partAfterAgent.text.trim().match(/^(\/\w*)?$/)) { - // No text allowed between agent and subcommand - return; - } - } - - const usedAgent = parsedRequest[usedAgentIdx] as ChatRequestAgentPart; - return { - suggestions: usedAgent.agent.slashCommands.map((c, i): CompletionItem => { - const withSlash = `/${c.name}`; - return { - label: withSlash, - insertText: `${withSlash} `, - detail: c.description, - range, - kind: CompletionItemKind.Text, // The icons are disabled here anyway - }; - }) - }; - } - })); - - // list subcommands when the query is empty, insert agent+subcommand - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatAgentAndSubcommand', - triggerCharacters: ['/'], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, token: CancellationToken) => { - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - const viewModel = widget?.viewModel; - if (!widget || !viewModel || widget.location !== AideChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { - return; - } - - const range = computeCompletionRanges(model, position, /\/\w*/g); - if (!range) { - return null; - } - - const agents = this.chatAgentService.getAgents() - .filter(a => a.locations.includes(widget.location)); - - // When the input is only `/`, items are sorted by sortText. - // When typing, filterText is used to score and sort. - // The same list is refiltered/ranked while typing. - const getFilterText = (agent: IChatAgentData, command: string) => { - // This is hacking the filter algorithm to make @terminal /explain match worse than @workspace /explain by making its match index later in the string. - // When I type `/exp`, the workspace one should be sorted over the terminal one. - const dummyPrefix = agent.id === 'github.copilot.terminalPanel' ? `0000` : ``; - return `${chatSubcommandLeader}${dummyPrefix}${agent.name}.${command}`; - }; - - const justAgents: CompletionItem[] = agents - .filter(a => !a.isDefault) - .map(agent => { - const { label: agentLabel, isDupe } = this.getAgentCompletionDetails(agent); - const detail = agent.description; - - return { - label: isDupe ? - { label: agentLabel, description: agent.description, detail: ` (${agent.publisherDisplayName})` } : - agentLabel, - detail, - filterText: `${chatSubcommandLeader}${agent.name}`, - insertText: `${agentLabel} `, - range: new Range(1, 1, 1, 1), - kind: CompletionItemKind.Text, - sortText: `${chatSubcommandLeader}${agent.name}`, - command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent, widget } satisfies AssignSelectedAgentActionArgs] }, - }; - }); - - return { - suggestions: justAgents.concat( - agents.flatMap(agent => agent.slashCommands.map((c, i) => { - const { label: agentLabel, isDupe } = this.getAgentCompletionDetails(agent); - const withSlash = `${chatSubcommandLeader}${c.name}`; - return { - label: { label: withSlash, description: agentLabel, detail: isDupe ? ` (${agent.publisherDisplayName})` : undefined }, - filterText: getFilterText(agent, c.name), - commitCharacters: [' '], - insertText: `${agentLabel} ${withSlash} `, - detail: `(${agentLabel}) ${c.description ?? ''}`, - range: new Range(1, 1, 1, 1), - kind: CompletionItemKind.Text, // The icons are disabled here anyway - sortText: `${chatSubcommandLeader}${agent.name}${c.name}`, - command: { id: AssignSelectedAgentAction.ID, title: AssignSelectedAgentAction.ID, arguments: [{ agent, widget } satisfies AssignSelectedAgentActionArgs] }, - } satisfies CompletionItem; - }))) - }; - } - })); - } - - private getAgentCompletionDetails(agent: IChatAgentData): { label: string; isDupe: boolean } { - const isAllowed = this.chatAgentNameService.getAgentNameRestriction(agent); - const agentLabel = `${chatAgentLeader}${isAllowed ? agent.name : getFullyQualifiedId(agent)}`; - const isDupe = isAllowed && this.chatAgentService.agentHasDupeName(agent.id); - return { label: agentLabel, isDupe }; - } -} -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(AgentCompletions, LifecyclePhase.Eventually); - -interface AssignSelectedAgentActionArgs { - agent: IChatAgentData; - widget: IChatWidget; -} - -class AssignSelectedAgentAction extends Action2 { - static readonly ID = 'workbench.action.aideChat.assignSelectedAgent'; - - constructor() { - super({ - id: AssignSelectedAgentAction.ID, - title: '' // not displayed - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]) { - const arg: AssignSelectedAgentActionArgs = args[0]; - if (!arg || !arg.widget || !arg.agent) { - return; - } - - arg.widget.lastSelectedAgent = arg.agent; - } -} -registerAction2(AssignSelectedAgentAction); - -class BuiltinDynamicCompletions extends Disposable { - private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag - - constructor( - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IAideChatWidgetService private readonly chatWidgetService: IAideChatWidgetService, - ) { - super(); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatDynamicCompletions', - triggerCharacters: [chatVariableLeader], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !widget.supportsFileReferences || widget.location !== AideChatAgentLocation.Panel /* TODO@jrieken - enable when agents are adopted*/) { - return null; - } - - const range = computeCompletionRanges(model, position, BuiltinDynamicCompletions.VariableNameDef); - if (!range) { - return null; - } - - const afterRange = new Range(position.lineNumber, range.replace.startColumn, position.lineNumber, range.replace.startColumn + '#file:'.length); - return { - suggestions: [ - { - label: `${chatVariableLeader}file`, - insertText: `${chatVariableLeader}file:`, - detail: localize('pickFileReferenceLabel', "Pick a file"), - range, - kind: CompletionItemKind.Text, - command: { id: MultiLevelCodeTriggerAction.ID, title: MultiLevelCodeTriggerAction.ID, arguments: [{ inputEditor: widget.inputEditor, range: afterRange, pick: 'file' }] }, - sortText: 'z' - }, - { - label: `${chatVariableLeader}code`, - insertText: `${chatVariableLeader}code:`, - detail: localize('pickCodeSymbolLabel', "Pick a code symbol"), - range, - kind: CompletionItemKind.Text, - command: { id: MultiLevelCodeTriggerAction.ID, title: MultiLevelCodeTriggerAction.ID, arguments: [{ inputEditor: widget.inputEditor, range: afterRange, pick: 'code' }] }, - sortText: 'z' - }, - { - label: `${chatVariableLeader}folder`, - insertText: `${chatVariableLeader}folder:`, - detail: localize('pickFolderReferenceLabel', "Pick a folder"), - range, - kind: CompletionItemKind.Text, - command: { id: MultiLevelCodeTriggerAction.ID, title: MultiLevelCodeTriggerAction.ID, arguments: [{ inputEditor: widget.inputEditor, range: afterRange, pick: 'folder' }] }, - sortText: 'z' - } - ] - }; - } - })); - } -} - -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(BuiltinDynamicCompletions, LifecyclePhase.Eventually); - -export function computeCompletionRanges(model: ITextModel, position: Position, reg: RegExp): { insert: Range; replace: Range; varWord: IWordAtPosition | null } | undefined { - const varWord = getWordAtText(position.column, reg, model.getLineContent(position.lineNumber), 0); - if (!varWord && model.getWordUntilPosition(position).word) { - // inside a "normal" word - return; - } - - let insert: Range; - let replace: Range; - if (!varWord) { - insert = replace = Range.fromPositions(position); - } else { - insert = new Range(position.lineNumber, varWord.startColumn, position.lineNumber, position.column); - replace = new Range(position.lineNumber, varWord.startColumn, position.lineNumber, varWord.endColumn); - } - - return { insert, replace, varWord }; -} - -class VariableCompletions extends Disposable { - - private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}\\w*`, 'g'); // MUST be using `g`-flag - - constructor( - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IAideChatWidgetService private readonly chatWidgetService: IAideChatWidgetService, - @IAideChatVariablesService private readonly chatVariablesService: IAideChatVariablesService, - @IConfigurationService configService: IConfigurationService, - ) { - super(); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'chatVariables', - triggerCharacters: [chatVariableLeader], - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { - - const locations = new Set(); - locations.add(AideChatAgentLocation.Panel); - - for (const value of Object.values(AideChatAgentLocation)) { - if (typeof value === 'string' && configService.getValue(`aideChat.experimental.variables.${value}`)) { - locations.add(value); - } - } - - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget || !locations.has(widget.location)) { - return null; - } - - const range = computeCompletionRanges(model, position, VariableCompletions.VariableNameDef); - if (!range) { - return null; - } - - const usedAgent = widget.parsedInput.parts.find(p => p instanceof ChatRequestAgentPart); - const slowSupported = usedAgent ? usedAgent.agent.metadata.supportsSlowVariables : true; - - const usedVariables = widget.parsedInput.parts.filter((p): p is ChatRequestVariablePart => p instanceof ChatRequestVariablePart); - const variableItems = Array.from(this.chatVariablesService.getVariables()) - // This doesn't look at dynamic variables like `file`, where multiple makes sense. - .filter(v => !usedVariables.some(usedVar => usedVar.variableName === v.name)) - .filter(v => !v.isSlow || slowSupported) - .map((v): CompletionItem => { - const withLeader = `${chatVariableLeader}${v.name}`; - return { - label: withLeader, - range, - insertText: withLeader + ' ', - detail: v.description, - kind: CompletionItemKind.Text, // The icons are disabled here anyway - sortText: 'z' - }; - }); - - return { - suggestions: variableItems - }; - } - })); - } -} - -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(VariableCompletions, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/aideChat/browser/contrib/aideChatInputEditorContrib.ts b/src/vs/workbench/contrib/aideChat/browser/contrib/aideChatInputEditorContrib.ts deleted file mode 100644 index 218ba3f2216..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/contrib/aideChatInputEditorContrib.ts +++ /dev/null @@ -1,582 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { MarkdownString } from '../../../../../base/common/htmlContent.js'; -import { Disposable, MutableDisposable } from '../../../../../base/common/lifecycle.js'; -import { basenameOrAuthority, dirname } from '../../../../../base/common/resources.js'; -import { URI } from '../../../../../base/common/uri.js'; -import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js'; -import { Position } from '../../../../../editor/common/core/position.js'; -import { IRange, Range } from '../../../../../editor/common/core/range.js'; -import { getWordAtText } from '../../../../../editor/common/core/wordHelper.js'; -import { IDecorationOptions } from '../../../../../editor/common/editorCommon.js'; -import { CompletionContext, CompletionItem, CompletionItemKind } from '../../../../../editor/common/languages.js'; -import { ITextModel } from '../../../../../editor/common/model.js'; -import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { ILabelService } from '../../../../../platform/label/common/label.js'; -import { Registry } from '../../../../../platform/registry/common/platform.js'; -import { inputPlaceholderForeground } from '../../../../../platform/theme/common/colorRegistry.js'; -import { IThemeService } from '../../../../../platform/theme/common/themeService.js'; -import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions } from '../../../../../workbench/common/contributions.js'; -import { IAideChatWidgetService, IChatWidget } from '../../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { ChatInputPart } from '../../../../../workbench/contrib/aideChat/browser/aideChatInputPart.js'; -import { ChatWidget } from '../../../../../workbench/contrib/aideChat/browser/aideChatWidget.js'; -import { CodeSymbolCompletionProviderName, dynamicVariableDecorationType, FileReferenceCompletionProviderName, FolderReferenceCompletionProviderName, IWidgetWithInputEditor, SelectAndInsertCodeAction, SelectAndInsertFileAction, SelectAndInsertFolderAction } from '../../../../../workbench/contrib/aideChat/browser/contrib/aideChatDynamicVariables.js'; -import { AideChatAgentLocation, IAideChatAgentService, IChatAgentCommand, IChatAgentData } from '../../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { chatSlashCommandBackground, chatSlashCommandForeground } from '../../../../../workbench/contrib/aideChat/common/aideChatColors.js'; -import { chatAgentLeader, ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, chatSubcommandLeader, chatVariableLeader, IParsedChatRequestPart } from '../../../../../workbench/contrib/aideChat/common/aideChatParserTypes.js'; -import { ChatRequestParser } from '../../../../../workbench/contrib/aideChat/common/aideChatRequestParser.js'; -import { SymbolsQuickAccessProvider } from '../../../../../workbench/contrib/search/browser/symbolsQuickAccess.js'; -import { getOutOfWorkspaceEditorResources } from '../../../../../workbench/contrib/search/common/search.js'; -import { LifecyclePhase } from '../../../../../workbench/services/lifecycle/common/lifecycle.js'; -import { QueryBuilder } from '../../../../../workbench/services/search/common/queryBuilder.js'; -import { ISearchComplete, ISearchService } from '../../../../../workbench/services/search/common/search.js'; - -const decorationDescription = 'chat'; -const placeholderDecorationType = 'chat-session-detail'; -const slashCommandTextDecorationType = 'chat-session-text'; -const variableTextDecorationType = 'chat-variable-text'; - -function agentAndCommandToKey(agent: IChatAgentData, subcommand: string | undefined): string { - return subcommand ? `${agent.id}__${subcommand}` : agent.id; -} - -class InputEditorDecorations extends Disposable { - - public readonly id = 'inputEditorDecorations'; - - private readonly previouslyUsedAgents = new Set(); - - private readonly viewModelDisposables = this._register(new MutableDisposable()); - - constructor( - private readonly widget: IChatWidget, - @ICodeEditorService private readonly codeEditorService: ICodeEditorService, - @IThemeService private readonly themeService: IThemeService, - @IAideChatAgentService private readonly chatAgentService: IAideChatAgentService, - ) { - super(); - - this.codeEditorService.registerDecorationType(decorationDescription, placeholderDecorationType, {}); - - this._register(this.themeService.onDidColorThemeChange(() => this.updateRegisteredDecorationTypes())); - this.updateRegisteredDecorationTypes(); - - this.updateInputEditorDecorations(); - this._register(this.widget.inputEditor.onDidChangeModelContent(() => this.updateInputEditorDecorations())); - this._register(this.widget.onDidChangeParsedInput(() => this.updateInputEditorDecorations())); - this._register(this.widget.onDidChangeViewModel(() => { - this.registerViewModelListeners(); - this.previouslyUsedAgents.clear(); - this.updateInputEditorDecorations(); - })); - this._register(this.widget.onDidSubmitAgent((e) => { - this.previouslyUsedAgents.add(agentAndCommandToKey(e.agent, e.slashCommand?.name)); - })); - this._register(this.chatAgentService.onDidChangeAgents(() => this.updateInputEditorDecorations())); - - this.registerViewModelListeners(); - } - - private registerViewModelListeners(): void { - this.viewModelDisposables.value = this.widget.viewModel?.onDidChange(e => { - if (e?.kind === 'changePlaceholder' || e?.kind === 'initialize') { - this.updateInputEditorDecorations(); - } - }); - } - - private updateRegisteredDecorationTypes() { - this.codeEditorService.removeDecorationType(variableTextDecorationType); - this.codeEditorService.removeDecorationType(dynamicVariableDecorationType); - this.codeEditorService.removeDecorationType(slashCommandTextDecorationType); - - const theme = this.themeService.getColorTheme(); - this.codeEditorService.registerDecorationType(decorationDescription, slashCommandTextDecorationType, { - color: theme.getColor(chatSlashCommandForeground)?.toString(), - backgroundColor: theme.getColor(chatSlashCommandBackground)?.toString(), - borderRadius: '3px' - }); - this.codeEditorService.registerDecorationType(decorationDescription, variableTextDecorationType, { - color: theme.getColor(chatSlashCommandForeground)?.toString(), - backgroundColor: theme.getColor(chatSlashCommandBackground)?.toString(), - borderRadius: '3px' - }); - this.codeEditorService.registerDecorationType(decorationDescription, dynamicVariableDecorationType, { - color: theme.getColor(chatSlashCommandForeground)?.toString(), - backgroundColor: theme.getColor(chatSlashCommandBackground)?.toString(), - borderRadius: '3px' - }); - this.updateInputEditorDecorations(); - } - - private getPlaceholderColor(): string | undefined { - const theme = this.themeService.getColorTheme(); - const transparentForeground = theme.getColor(inputPlaceholderForeground); - return transparentForeground?.toString(); - } - - private async updateInputEditorDecorations() { - const inputValue = this.widget.inputEditor.getValue(); - - const viewModel = this.widget.viewModel; - if (!viewModel) { - return; - } - - if (!inputValue) { - const defaultAgent = this.chatAgentService.getDefaultAgent(this.widget.location); - const decoration: IDecorationOptions[] = [ - { - range: { - startLineNumber: 1, - endLineNumber: 1, - startColumn: 1, - endColumn: 1000 - }, - renderOptions: { - after: { - contentText: viewModel.inputPlaceholder || (defaultAgent?.description ?? ''), - color: this.getPlaceholderColor() - } - } - } - ]; - this.widget.inputEditor.setDecorationsByType(decorationDescription, placeholderDecorationType, decoration); - return; - } - - const parsedRequest = this.widget.parsedInput.parts; - - let placeholderDecoration: IDecorationOptions[] | undefined; - const agentPart = parsedRequest.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); - const agentSubcommandPart = parsedRequest.find((p): p is ChatRequestAgentSubcommandPart => p instanceof ChatRequestAgentSubcommandPart); - const slashCommandPart = parsedRequest.find((p): p is ChatRequestSlashCommandPart => p instanceof ChatRequestSlashCommandPart); - - const exactlyOneSpaceAfterPart = (part: IParsedChatRequestPart): boolean => { - const partIdx = parsedRequest.indexOf(part); - if (parsedRequest.length > partIdx + 2) { - return false; - } - - const nextPart = parsedRequest[partIdx + 1]; - return nextPart && nextPart instanceof ChatRequestTextPart && nextPart.text === ' '; - }; - - const getRangeForPlaceholder = (part: IParsedChatRequestPart) => ({ - startLineNumber: part.editorRange.startLineNumber, - endLineNumber: part.editorRange.endLineNumber, - startColumn: part.editorRange.endColumn + 1, - endColumn: 1000 - }); - - const onlyAgentAndWhitespace = agentPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentPart); - if (onlyAgentAndWhitespace) { - // Agent reference with no other text - show the placeholder - const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent, undefined)); - const shouldRenderFollowupPlaceholder = isFollowupSlashCommand && agentPart.agent.metadata.followupPlaceholder; - if (agentPart.agent.description && exactlyOneSpaceAfterPart(agentPart)) { - placeholderDecoration = [{ - range: getRangeForPlaceholder(agentPart), - renderOptions: { - after: { - contentText: shouldRenderFollowupPlaceholder ? agentPart.agent.metadata.followupPlaceholder : agentPart.agent.description, - color: this.getPlaceholderColor(), - } - } - }]; - } - } - - const onlyAgentCommandAndWhitespace = agentPart && agentSubcommandPart && parsedRequest.every(p => p instanceof ChatRequestTextPart && !p.text.trim().length || p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart); - if (onlyAgentCommandAndWhitespace) { - // Agent reference and subcommand with no other text - show the placeholder - const isFollowupSlashCommand = this.previouslyUsedAgents.has(agentAndCommandToKey(agentPart.agent, agentSubcommandPart.command.name)); - const shouldRenderFollowupPlaceholder = isFollowupSlashCommand && agentSubcommandPart.command.followupPlaceholder; - if (agentSubcommandPart?.command.description && exactlyOneSpaceAfterPart(agentSubcommandPart)) { - placeholderDecoration = [{ - range: getRangeForPlaceholder(agentSubcommandPart), - renderOptions: { - after: { - contentText: shouldRenderFollowupPlaceholder ? agentSubcommandPart.command.followupPlaceholder : agentSubcommandPart.command.description, - color: this.getPlaceholderColor(), - } - } - }]; - } - } - - this.widget.inputEditor.setDecorationsByType(decorationDescription, placeholderDecorationType, placeholderDecoration ?? []); - - const textDecorations: IDecorationOptions[] | undefined = []; - if (agentPart) { - textDecorations.push({ range: agentPart.editorRange }); - if (agentSubcommandPart) { - textDecorations.push({ range: agentSubcommandPart.editorRange, hoverMessage: new MarkdownString(agentSubcommandPart.command.description) }); - } - } - - if (slashCommandPart) { - textDecorations.push({ range: slashCommandPart.editorRange }); - } - - this.widget.inputEditor.setDecorationsByType(decorationDescription, slashCommandTextDecorationType, textDecorations); - - const varDecorations: IDecorationOptions[] = []; - const variableParts = parsedRequest.filter((p): p is ChatRequestVariablePart => p instanceof ChatRequestVariablePart); - for (const variable of variableParts) { - varDecorations.push({ range: variable.editorRange }); - } - - this.widget.inputEditor.setDecorationsByType(decorationDescription, variableTextDecorationType, varDecorations); - } -} - -class InputEditorSlashCommandMode extends Disposable { - public readonly id = 'InputEditorSlashCommandMode'; - - constructor( - private readonly widget: IChatWidget - ) { - super(); - this._register(this.widget.onDidSubmitAgent(e => { - this.repopulateAgentCommand(e.agent, e.slashCommand); - })); - } - - private async repopulateAgentCommand(agent: IChatAgentData, slashCommand: IChatAgentCommand | undefined) { - let value: string | undefined; - if (slashCommand && slashCommand.isSticky) { - value = `${chatAgentLeader}${agent.name} ${chatSubcommandLeader}${slashCommand.name} `; - } else if (agent.metadata.isSticky) { - value = `${chatAgentLeader}${agent.name} `; - } - - if (value) { - this.widget.inputEditor.setValue(value); - this.widget.inputEditor.setPosition({ lineNumber: 1, column: value.length + 1 }); - } - } -} - -ChatWidget.CONTRIBS.push(InputEditorDecorations, InputEditorSlashCommandMode); - -class ChatTokenDeleter extends Disposable { - - public readonly id = 'chatTokenDeleter'; - - constructor( - private readonly widget: IChatWidget, - @IInstantiationService private readonly instantiationService: IInstantiationService, - ) { - super(); - const parser = this.instantiationService.createInstance(ChatRequestParser); - const inputValue = this.widget.inputEditor.getValue(); - let previousInputValue: string | undefined; - let previousSelectedAgent: IChatAgentData | undefined; - - // A simple heuristic to delete the previous token when the user presses backspace. - // The sophisticated way to do this would be to have a parse tree that can be updated incrementally. - this._register(this.widget.inputEditor.onDidChangeModelContent(e => { - if (!previousInputValue) { - previousInputValue = inputValue; - previousSelectedAgent = this.widget.lastSelectedAgent; - } - - // Don't try to handle multicursor edits right now - const change = e.changes[0]; - - // If this was a simple delete, try to find out whether it was inside a token - if (!change.text && this.widget.viewModel) { - const previousParsedValue = parser.parseChatRequest(this.widget.viewModel.sessionId, previousInputValue, AideChatAgentLocation.Panel, { selectedAgent: previousSelectedAgent }); - - // For dynamic variables, this has to happen in ChatDynamicVariableModel with the other bookkeeping - const deletableTokens = previousParsedValue.parts.filter(p => p instanceof ChatRequestAgentPart || p instanceof ChatRequestAgentSubcommandPart || p instanceof ChatRequestSlashCommandPart || p instanceof ChatRequestVariablePart); - deletableTokens.forEach(token => { - const deletedRangeOfToken = Range.intersectRanges(token.editorRange, change.range); - // Part of this token was deleted, or the space after it was deleted, and the deletion range doesn't go off the front of the token, for simpler math - if (deletedRangeOfToken && Range.compareRangesUsingStarts(token.editorRange, change.range) < 0) { - // Assume single line tokens - const length = deletedRangeOfToken.endColumn - deletedRangeOfToken.startColumn; - const rangeToDelete = new Range(token.editorRange.startLineNumber, token.editorRange.startColumn, token.editorRange.endLineNumber, token.editorRange.endColumn - length); - this.widget.inputEditor.executeEdits(this.id, [{ - range: rangeToDelete, - text: '', - }]); - } - }); - } - - previousInputValue = this.widget.inputEditor.getValue(); - previousSelectedAgent = this.widget.lastSelectedAgent; - })); - } -} -ChatWidget.CONTRIBS.push(ChatTokenDeleter); - -async function getWidget( - model: ITextModel, - chatWidgetService: IAideChatWidgetService, -): Promise { - let widget: IWidgetWithInputEditor | undefined | null; - const scheme = model.uri.scheme; - if (scheme === ChatInputPart.INPUT_SCHEME) { - widget = chatWidgetService.getWidgetByInputUri(model.uri); - } - - return widget; -} - -export class FileReferenceCompletionsProvider extends Disposable { - private readonly fileQueryBuilder = this.instantiationService.createInstance(QueryBuilder); - - constructor( - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IAideChatWidgetService private readonly chatWidgetService: IAideChatWidgetService, - @ISearchService private readonly searchService: ISearchService, - @ILabelService private readonly labelService: ILabelService, - ) { - super(); - } - - async provideCompletionItems(model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) { - const widget = await getWidget(model, this.chatWidgetService); - if (!widget) { - return null; - } - - const varWord = getWordAtText(position.column, FileReferenceCompletions.VariableNameDef, model.getLineContent(position.lineNumber), 0); - if (!varWord && model.getWordUntilPosition(position).word) { - return null; - } - - const range: IRange = { - startLineNumber: position.lineNumber, - startColumn: varWord ? varWord.endColumn : position.column, - endLineNumber: position.lineNumber, - endColumn: varWord ? varWord.endColumn : position.column - }; - - const files = await this.doGetFileSearchResults(_token); - const completionURIs = files.results.map(result => result.resource); - - const editRange: IRange = { - startLineNumber: position.lineNumber, - startColumn: varWord ? varWord.startColumn : position.column, - endLineNumber: position.lineNumber, - endColumn: varWord ? varWord.endColumn : position.column - }; - - const completionItems = completionURIs.map(uri => { - const detail = this.labelService.getUriLabel(dirname(uri), { relative: true }); - return { - label: basenameOrAuthority(uri), - insertText: '', - detail, - kind: CompletionItemKind.File, - range, - command: { id: SelectAndInsertFileAction.ID, title: SelectAndInsertFileAction.ID, arguments: [{ widget, range: editRange, uri }] }, - sortText: 'z' - }; - }); - - return { - suggestions: completionItems - }; - } - - private doGetFileSearchResults(token: CancellationToken): Promise { - return this.searchService.fileSearch( - this.fileQueryBuilder.file( - this.contextService.getWorkspace().folders, - { - extraFileResources: this.instantiationService.invokeFunction(getOutOfWorkspaceEditorResources), - sortByScore: true, - } - ), token); - } -} - -class FileReferenceCompletions extends Disposable { - static readonly VariableNameDef = new RegExp(`${chatVariableLeader}file:\\w*`, 'g'); // MUST be using `g`-flag - - constructor( - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - ) { - super(); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: FileReferenceCompletionProviderName, - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { - const fileReferenceCompletionsProvider = this._register(this.instantiationService.createInstance(FileReferenceCompletionsProvider)); - return fileReferenceCompletionsProvider.provideCompletionItems(model, position, _context, _token); - } - })); - } -} -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(FileReferenceCompletions, LifecyclePhase.Eventually); - -export class CodeSymbolCompletionProvider extends Disposable { - private readonly workspaceSymbolsQuickAccess = this.instantiationService.createInstance(SymbolsQuickAccessProvider); - - constructor( - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IAideChatWidgetService private readonly chatWidgetService: IAideChatWidgetService, - ) { - super(); - } - - async provideCompletionItems(model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) { - const widget = await getWidget(model, this.chatWidgetService); - if (!widget) { - return null; - } - - const varWord = getWordAtText(position.column, CodeSymbolCompletions.VariableNameDef, model.getLineContent(position.lineNumber), 0); - if (!varWord && model.getWordUntilPosition(position).word) { - return null; - } - - const range: IRange = { - startLineNumber: position.lineNumber, - startColumn: varWord ? varWord.endColumn : position.column, - endLineNumber: position.lineNumber, - endColumn: varWord ? varWord.endColumn : position.column - }; - - const prefixWord = `${chatVariableLeader}code:`; - const query = varWord ? varWord.word.substring(prefixWord.length) : ''; - const editorSymbolPicks = await this.workspaceSymbolsQuickAccess.getSymbolPicks(query, undefined, CancellationToken.None); - if (!editorSymbolPicks.length) { - return null; - } - - const editRange: IRange = { - startLineNumber: position.lineNumber, - startColumn: varWord ? varWord.startColumn : position.column, - endLineNumber: position.lineNumber, - endColumn: varWord ? varWord.endColumn : position.column - }; - return { - incomplete: true, - suggestions: editorSymbolPicks.map(pick => ({ - label: pick.label, - insertText: '', - detail: pick.resource ? basenameOrAuthority(pick.resource) : '', - kind: CompletionItemKind.Text, - range, - command: { id: SelectAndInsertCodeAction.ID, title: SelectAndInsertCodeAction.ID, arguments: [{ widget, range: editRange, pick }] }, - sortText: 'z' - } satisfies CompletionItem)), - }; - } -} - -class CodeSymbolCompletions extends Disposable { - static readonly VariableNameDef = new RegExp(`${chatVariableLeader}code:\\w*`, 'g'); // MUST be using `g`-flag - - constructor( - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - ) { - super(); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: CodeSymbolCompletionProviderName, - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { - const codeSymbolCompletionsProvider = this._register(this.instantiationService.createInstance(CodeSymbolCompletionProvider)); - return codeSymbolCompletionsProvider.provideCompletionItems(model, position, _context, _token); - } - })); - } -} -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(CodeSymbolCompletions, LifecyclePhase.Eventually); - - -class FolderReferenceCompletions extends Disposable { - private static readonly VariableNameDef = new RegExp(`${chatVariableLeader}folder:\\w*`, 'g'); // MUST be using `g`-flag - private readonly fileQueryBuilder = this.instantiationService.createInstance(QueryBuilder); - - constructor( - @IWorkspaceContextService private readonly contextService: IWorkspaceContextService, - @ILanguageFeaturesService private readonly languageFeaturesService: ILanguageFeaturesService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IAideChatWidgetService private readonly chatWidgetService: IAideChatWidgetService, - @ISearchService private readonly searchService: ISearchService, - @ILabelService private readonly labelService: ILabelService, - ) { - super(); - - this._register(this.languageFeaturesService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: FolderReferenceCompletionProviderName, - provideCompletionItems: async (model: ITextModel, position: Position, _context: CompletionContext, _token: CancellationToken) => { - const widget = this.chatWidgetService.getWidgetByInputUri(model.uri); - if (!widget) { - return null; - } - - const varWord = getWordAtText(position.column, FolderReferenceCompletions.VariableNameDef, model.getLineContent(position.lineNumber), 0); - if (!varWord && model.getWordUntilPosition(position).word) { - return null; - } - - const range: IRange = { - startLineNumber: position.lineNumber, - startColumn: varWord ? varWord.endColumn : position.column, - endLineNumber: position.lineNumber, - endColumn: varWord ? varWord.endColumn : position.column - }; - - const completionURIs = await this.doGetFolderSearchResults(_token); - - const editRange: IRange = { - startLineNumber: position.lineNumber, - startColumn: varWord ? varWord.startColumn : position.column, - endLineNumber: position.lineNumber, - endColumn: varWord ? varWord.endColumn : position.column - }; - - const completionItems = completionURIs.map(uri => { - return { - label: this.labelService.getUriLabel(uri, { relative: true }), - insertText: '', - kind: CompletionItemKind.Folder, - range, - command: { id: SelectAndInsertFolderAction.ID, title: SelectAndInsertFolderAction.ID, arguments: [{ widget, range: editRange, uri }] }, - sortText: 'z' - }; - }); - - return { - suggestions: completionItems - }; - } - })); - } - - private async doGetFolderSearchResults(token: CancellationToken): Promise { - const response = await this.searchService.fileSearch( - this.fileQueryBuilder.file( - this.contextService.getWorkspace().folders, - { - extraFileResources: this.instantiationService.invokeFunction(getOutOfWorkspaceEditorResources), - sortByScore: true, - } - ), token); - - const dirUris = new Map(); - response.results.forEach(result => { - const dirUri = dirname(result.resource); - dirUris.set(dirUri.toString(), dirUri); - }); - return Array.from(dirUris.values()); - } -} -Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(FolderReferenceCompletions, LifecyclePhase.Eventually); diff --git a/src/vs/workbench/contrib/aideChat/browser/contrib/aideChatInputEditorHover.ts b/src/vs/workbench/contrib/aideChat/browser/contrib/aideChatInputEditorHover.ts deleted file mode 100644 index 3a0f93d8cbe..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/contrib/aideChatInputEditorHover.ts +++ /dev/null @@ -1,100 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; -import { Range } from '../../../../../editor/common/core/range.js'; -import { IModelDecoration } from '../../../../../editor/common/model.js'; -import { HoverAnchor, HoverAnchorType, HoverParticipantRegistry, IEditorHoverParticipant, IEditorHoverRenderContext, IHoverPart, IRenderedHoverPart, IRenderedHoverParts, RenderedHoverParts } from '../../../../../editor/contrib/hover/browser/hoverTypes.js'; -import * as nls from '../../../../../nls.js'; -import { ICommandService } from '../../../../../platform/commands/common/commands.js'; -import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js'; -import { IChatAgentData } from '../../common/aideChatAgents.js'; -import { extractAgentAndCommand } from '../../common/aideChatParserTypes.js'; -import { IAideChatWidgetService } from '../aideChat.js'; -import { ChatAgentHover, getChatAgentHoverOptions } from '../aideChatAgentHover.js'; -import { ChatEditorHoverWrapper } from './editorHoverWrapper.js'; - -export class ChatAgentHoverParticipant implements IEditorHoverParticipant { - - public readonly hoverOrdinal: number = 1; - - constructor( - private readonly editor: ICodeEditor, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @IAideChatWidgetService private readonly chatWidgetService: IAideChatWidgetService, - @ICommandService private readonly commandService: ICommandService, - ) { } - - public computeSync(anchor: HoverAnchor, _lineDecorations: IModelDecoration[]): ChatAgentHoverPart[] { - if (!this.editor.hasModel()) { - return []; - } - - const widget = this.chatWidgetService.getWidgetByInputUri(this.editor.getModel().uri); - if (!widget) { - return []; - } - - const { agentPart } = extractAgentAndCommand(widget.parsedInput); - if (!agentPart) { - return []; - } - - if (Range.containsPosition(agentPart.editorRange, anchor.range.getStartPosition())) { - return [new ChatAgentHoverPart(this, Range.lift(agentPart.editorRange), agentPart.agent)]; - } - - return []; - } - - public renderHoverParts(context: IEditorHoverRenderContext, hoverParts: ChatAgentHoverPart[]): IRenderedHoverParts { - if (!hoverParts.length) { - return new RenderedHoverParts([]); - } - - const disposables = new DisposableStore(); - const hover = disposables.add(this.instantiationService.createInstance(ChatAgentHover)); - disposables.add(hover.onDidChangeContents(() => context.onContentsChanged())); - const hoverPart = hoverParts[0]; - const agent = hoverPart.agent; - hover.setAgent(agent.id); - - const actions = getChatAgentHoverOptions(() => agent, this.commandService).actions; - const wrapper = this.instantiationService.createInstance(ChatEditorHoverWrapper, hover.domNode, actions); - const wrapperNode = wrapper.domNode; - context.fragment.appendChild(wrapperNode); - const renderedHoverPart: IRenderedHoverPart = { - hoverPart, - hoverElement: wrapperNode, - dispose() { disposables.dispose(); } - }; - return new RenderedHoverParts([renderedHoverPart]); - } - - public getAccessibleContent(hoverPart: ChatAgentHoverPart): string { - return nls.localize('hoverAccessibilityChatAgent', 'There is a chat agent hover part here.'); - - } -} - -export class ChatAgentHoverPart implements IHoverPart { - - constructor( - public readonly owner: IEditorHoverParticipant, - public readonly range: Range, - public readonly agent: IChatAgentData - ) { } - - public isValidForHoverAnchor(anchor: HoverAnchor): boolean { - return ( - anchor.type === HoverAnchorType.Range - && this.range.startColumn <= anchor.range.startColumn - && this.range.endColumn >= anchor.range.endColumn - ); - } -} - -HoverParticipantRegistry.register(ChatAgentHoverParticipant); diff --git a/src/vs/workbench/contrib/aideChat/browser/contrib/aideChatKeybindingPillContrib.ts b/src/vs/workbench/contrib/aideChat/browser/contrib/aideChatKeybindingPillContrib.ts deleted file mode 100644 index e62bd10c0d5..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/contrib/aideChatKeybindingPillContrib.ts +++ /dev/null @@ -1,39 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js'; -import { IEditorContribution } from '../../../../../editor/common/editorCommon.js'; -import { KeybindingPillWidget } from '../../../../../workbench/contrib/aideChat/browser/aideKeybindingPill.js'; - -export class KeybindingPillContribution implements IEditorContribution { - public static readonly ID = 'editor.contrib.keybindingPill'; - - private pillWidget: KeybindingPillWidget | null | undefined; - private editor: ICodeEditor; - - constructor(editor: ICodeEditor) { - this.editor = editor; - this.pillWidget = this.editor.getContribution(KeybindingPillWidget.ID); - - this.editor.onDidChangeCursorSelection(event => { - if (event.selection.isEmpty()) { - this.pillWidget?.hide(); - } else { - this.pillWidget?.showAt(event.selection.getPosition()); - } - }); - } - - dispose() { - if (this.pillWidget) { - this.pillWidget.dispose(); - this.pillWidget = null; - } - } - - getId(): string { - return KeybindingPillContribution.ID; - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/contrib/editorHoverWrapper.ts b/src/vs/workbench/contrib/aideChat/browser/contrib/editorHoverWrapper.ts deleted file mode 100644 index 01f5adf945d..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/contrib/editorHoverWrapper.ts +++ /dev/null @@ -1,52 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import './media/editorHoverWrapper.css'; -import * as dom from '../../../../../base/browser/dom.js'; -import { IHoverAction } from '../../../../../base/browser/ui/hover/hover.js'; -import { HoverAction } from '../../../../../base/browser/ui/hover/hoverWidget.js'; -import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js'; - -const $ = dom.$; -const h = dom.h; - -/** - * This borrows some of HoverWidget so that a chat editor hover can be rendered in the same way as a workbench hover. - * Maybe it can be reusable in a generic way. - */ -export class ChatEditorHoverWrapper { - public readonly domNode: HTMLElement; - - constructor( - hoverContentElement: HTMLElement, - actions: IHoverAction[] | undefined, - @IKeybindingService private readonly keybindingService: IKeybindingService, - ) { - const hoverElement = h( - '.chat-editor-hover-wrapper@root', - [h('.chat-editor-hover-wrapper-content@content')]); - this.domNode = hoverElement.root; - hoverElement.content.appendChild(hoverContentElement); - - if (actions && actions.length > 0) { - const statusBarElement = $('.hover-row.status-bar'); - const actionsElement = $('.actions'); - actions.forEach(action => { - const keybinding = this.keybindingService.lookupKeybinding(action.commandId); - const keybindingLabel = keybinding ? keybinding.getLabel() : null; - HoverAction.render(actionsElement, { - label: action.label, - commandId: action.commandId, - run: e => { - action.run(e); - }, - iconClass: action.iconClass - }, keybindingLabel); - }); - statusBarElement.appendChild(actionsElement); - this.domNode.appendChild(statusBarElement); - } - } -} diff --git a/src/vs/workbench/contrib/aideChat/browser/contrib/media/editorHoverWrapper.css b/src/vs/workbench/contrib/aideChat/browser/contrib/media/editorHoverWrapper.css deleted file mode 100644 index d95fd395255..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/contrib/media/editorHoverWrapper.css +++ /dev/null @@ -1,8 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.chat-editor-hover-wrapper-content { - padding: 2px 8px; -} diff --git a/src/vs/workbench/contrib/aideChat/browser/media/chat.css b/src/vs/workbench/contrib/aideChat/browser/media/chat.css deleted file mode 100644 index 718ff66b6f8..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/media/chat.css +++ /dev/null @@ -1,865 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.cschat-session { - max-width: 850px; - margin: auto; -} - -.cschat-list > .monaco-list > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row > .monaco-tl-row > .monaco-tl-twistie { - /* Hide twisties from chat tree rows, but not from nested trees within a chat response */ - display: none !important; -} - -.cschat-item-container { - padding-block: 16px; - padding-inline: 10px; - display: flex; - flex-direction: column; - gap: 8px; - color: var(--vscode-interactive-session-foreground); - - cursor: default; - user-select: text; - -webkit-user-select: text; -} - -.cschat-input-part .header, -.cschat-item-container .header { - display: flex; - align-items: center; - justify-content: space-between; - position: relative; -} - -.cschat-input-part .header.hidden, -.cschat-item-container .header.hidden { - display: none; -} - -.cschat-input-part .header .user, -.cschat-item-container .header .user { - display: flex; - align-items: center; - gap: 8px; - width: 100%; -} - -.cschat-input-part .header .username, -.cschat-item-container .header .username { - margin: 0; - font-size: 13px; - font-weight: 600; -} - -.cschat-item-container .detail-container { - font-size: 12px; - color: var(--vscode-descriptionForeground); - overflow: hidden; -} - -.cschat-item-container .detail-container .detail .agentOrSlashCommandDetected A { - cursor: pointer; - color: var(--vscode-textLink-foreground); -} - -.cschat-item-container .chat-animated-ellipsis { - display: inline-block; - width: 11px; -} - -.cschat-item-container:not(.show-detail-progress) .chat-animated-ellipsis { - display: none; -} - -@keyframes ellipsis { - 0% { - content: ""; - } - 25% { - content: "."; - } - 50% { - content: ".."; - } - 75% { - content: "..."; - } - 100% { - content: ""; - } -} - -.cschat-item-container .chat-animated-ellipsis::after { - content: ''; - white-space: nowrap; - overflow: hidden; - width: 3em; - animation: ellipsis steps(4, end) 1s infinite; -} - -.cschat-input-part .header .avatar-container, -.cschat-item-container .header .avatar-container { - display: flex; - pointer-events: none; - user-select: none; -} - -.cschat-input-part .header .avatar, -.cschat-item-container .header .avatar { - display: flex; - align-items: center; - justify-content: center; - width: 24px; - height: 24px; - border-radius: 50%; - outline: 1px solid var(--vscode-chat-requestBorder); -} - -.cschat-input-part .header .avatar.codicon-avatar, -.cschat-item-container .header .avatar.codicon-avatar { - background: var(--vscode-chat-avatarBackground); -} - -.cschat-input-part .header .avatar+.avatar, -.cschat-item-container .header .avatar+.avatar { - margin-left: -8px; -} - -.cschat-input-part .header .avatar .icon, -.cschat-item-container .header .avatar .icon { - width: 24px; - height: 24px; - border-radius: 50%; - background-color: var(--vscode-chat-list-background); -} - -.cschat-input-part .header .avatar .codicon, -.cschat-item-container .header .avatar .codicon { - color: var(--vscode-chat-avatarForeground) !important; - font-size: 14px; -} - -.monaco-list-row:not(.focused) .cschat-item-container:not(:hover) .header .monaco-toolbar, -.monaco-list:not(:focus-within) .monaco-list-row .cschat-item-container:not(:hover) .header .monaco-toolbar, -.monaco-list-row:not(.focused) .cschat-item-container:not(:hover) .header .monaco-toolbar .action-label, -.monaco-list:not(:focus-within) .monaco-list-row .cschat-item-container:not(:hover) .header .monaco-toolbar .action-label { - /* Also apply this rule to the .action-label directly to work around a strange issue- when the - toolbar is hidden without that second rule, tabbing from the list container into a list item doesn't work - and the tab key doesn't do anything. */ - display: none; -} - -.cschat-item-container .header .monaco-toolbar .monaco-action-bar .actions-container { - gap: 4px; -} - -.cschat-item-container .header .monaco-toolbar .action-label { - border: 1px solid transparent; - padding: 2px; -} - -.cschat-item-container .header .monaco-toolbar { - position: absolute; - right: 0px; - background-color: var(--vscode-chat-list-background); -} - -.cschat-item-container.interactive-request .header .monaco-toolbar { - /* Take the partially-transparent background color override for request rows */ - background-color: inherit; -} - -.cschat-item-container .header .monaco-toolbar .checked.action-label, -.cschat-item-container .header .monaco-toolbar .checked.action-label:hover { - color: var(--vscode-inputOption-activeForeground) !important; - border-color: var(--vscode-inputOption-activeBorder); - background-color: var(--vscode-inputOption-activeBackground); -} - -.cschat-item-container .value { - width: 100%; -} - -.cschat-item-container > .value .chat-used-context { - margin-bottom: 8px; -} - -.cschat-item-container .value .rendered-markdown table { - width: 100%; - text-align: left; - margin-bottom: 16px; -} - -.cschat-item-container .value .rendered-markdown table, -.cschat-item-container .value .rendered-markdown table td, -.cschat-item-container .value .rendered-markdown table th { - border: 1px solid var(--vscode-chat-requestBorder); - border-collapse: collapse; - padding: 4px 6px; -} - -.cschat-item-container .value .rendered-markdown a, -.cschat-item-container .value .cschat-session-followups, -.cschat-item-container .value .rendered-markdown a code { - color: var(--vscode-textLink-foreground); -} - -.cschat-item-container .value .rendered-markdown a:hover, -.cschat-item-container .value .rendered-markdown a:active { - color: var(--vscode-textLink-activeForeground); -} - -.hc-black .cschat-item-container .value .rendered-markdown a code, -.hc-light .cschat-item-container .value .rendered-markdown a code { - color: var(--vscode-textPreformat-foreground); -} - -.cschat-list { - overflow: hidden; -} - -.interactive-request { - border-bottom: 1px solid var(--vscode-chat-requestBorder); - border-top: 1px solid var(--vscode-chat-requestBorder); -} - -.hc-black .interactive-request, -.hc-light .interactive-request { - border-left: 3px solid var(--vscode-chat-requestBorder); - border-right: 3px solid var(--vscode-chat-requestBorder); -} - -.cschat-item-container .value { - white-space: normal; - overflow-wrap: anywhere; -} - -.cschat-item-container .value > :last-child.rendered-markdown > :last-child { - margin-bottom: 0px; -} - -.cschat-item-container .value .rendered-markdown h1 { - font-size: 20px; - font-weight: 600; - margin: 16px 0; - -} - -.cschat-item-container .value .rendered-markdown h2 { - font-size: 16px; - font-weight: 600; - margin: 16px 0; -} - -.cschat-item-container .value .rendered-markdown h3 { - font-size: 14px; - font-weight: 600; - margin: 16px 0; -} - -.cschat-item-container .value .rendered-markdown p { - line-height: 1.5em; -} - -.cschat-item-container .value > .rendered-markdown p { - margin: 0 0 16px 0; -} - -.cschat-item-container .value > .rendered-markdown li > p { - margin: 0; -} - -.cschat-item-container .value .rendered-markdown ul { - padding-inline-start: 24px; -} - -.cschat-item-container .value .rendered-markdown ol { - padding-inline-start: 28px; -} - -.cschat-item-container .value .rendered-markdown li { - line-height: 1.3rem; -} - -.cschat-item-container .value .rendered-markdown img { - max-width: 100%; -} - -.cschat-item-container .monaco-tokenized-source, -.cschat-item-container code { - font-family: var(--monaco-monospace-font); - font-size: 12px; - color: var(--vscode-textPreformat-foreground); - background-color: var(--vscode-textPreformat-background); - padding: 1px 3px; - border-radius: 4px; -} - -.cschat-item-container.interactive-item-compact { - padding: 8px 20px; -} - -.cschat-item-container.interactive-item-compact.no-padding { - padding: unset; - gap: unset; -} - -.cschat-item-container.interactive-item-compact .header { - height: 16px; -} - -.cschat-item-container.interactive-item-compact .header .avatar { - width: 18px; - height: 18px; -} - -.cschat-item-container.interactive-item-compact .header .avatar .icon { - width: 16px; - height: 16px; -} - -.cschat-item-container.interactive-item-compact .header .codicon-avatar .codicon { - font-size: 12px; -} - -.cschat-item-container.interactive-item-compact .header .avatar+.avatar { - margin-left: -4px; -} - -.cschat-item-container.interactive-item-compact .value { - min-height: 0; -} - -.cschat-item-container.interactive-item-compact .value > .rendered-markdown p { - margin: 0 0 8px 0; -} - -.cschat-item-container.interactive-item-compact .value > .rendered-markdown li > p { - margin: 0; -} - -.cschat-item-container.interactive-item-compact .value .rendered-markdown h1 { - margin: 8px 0; - -} - -.cschat-item-container.interactive-item-compact .value .rendered-markdown h2 { - margin: 8px 0; -} - -.cschat-item-container.interactive-item-compact .value .rendered-markdown h3 { - margin: 8px 0; -} - -.cschat-item-container.minimal { - flex-direction: row; -} - -.cschat-item-container.minimal .column.left { - width: 20px; - padding-top: 2px; -} - -.cschat-item-container.minimal .user > .username { - display: none; -} - -.cschat-item-container.minimal .detail-container { - font-size: unset; -} - -.cschat-item-container.minimal > .header { - position: absolute; - right: 0; -} - -.cschat-session .interactive-input-and-execute-toolbar { - display: flex; - flex-direction: column; - box-sizing: border-box; - cursor: text; - /* - background-color: var(--vscode-input-background); - border: 1px solid var(--vscode-input-border, transparent); - border-radius: 4px; - */ - position: relative; - padding: 6px 0px; - /* - margin-bottom: 4px; - */ - align-items: flex-end; - justify-content: space-between; -} - -.cschat-session .interactive-input-toolbars { - width: 100%; - display: flex; - justify-content: space-between; -} - -.cschat-session .interactive-input-part.compact .interactive-input-and-execute-toolbar { - margin-bottom: 0; -} - -.cschat-session .interactive-input-and-side-toolbar { - display: flex; - gap: 4px; - align-items: center; -} - -.cschat-session .interactive-input-and-execute-toolbar.focused { - border-color: var(--vscode-focusBorder); -} - -.cschat-session .interactive-input-and-execute-toolbar .monaco-editor .mtk1 { - color: var(--vscode-input-foreground); -} - -/* -.cschat-session .interactive-input-and-execute-toolbar .monaco-editor, -.cschat-session .interactive-input-and-execute-toolbar .monaco-editor .monaco-editor-background { - background-color: var(--vscode-input-background) !important; -} -*/ - -.cschat-session .interactive-input-and-execute-toolbar .monaco-editor .cursors-layer { - padding-left: 4px; -} - -.cschat-session .cschat-input-part .interactive-execute-toolbar { - height: 22px; - - /* It's bottom-aligned, make it appear centered within the container */ - margin-bottom: 7px; -} - -.cschat-session .cschat-input-part .interactive-execute-toolbar .monaco-action-bar .actions-container { - display: flex; - gap: 4px; -} - -.cschat-session .cschat-input-part .interactive-execute-toolbar .codicon-debug-stop { - color: var(--vscode-icon-foreground) !important; -} - -.cschat-session .cschat-input-part .interactive-aide-toolbar { - height: 22px; - - /* It's bottom-aligned, make it appear centered within the container */ - margin-bottom: 7px; -} - -.cschat-session .cschat-input-part .interactive-aide-toolbar .monaco-action-bar .actions-container { - display: flex; - gap: 4px; -} - -.interactive-response .interactive-result-code-block .interactive-result-editor .monaco-editor, -.interactive-response .interactive-result-code-block .interactive-result-editor .monaco-editor .margin, -.interactive-response .interactive-result-code-block .interactive-result-editor .monaco-editor .monaco-editor-background { - background-color: var(--vscode-interactive-result-editor-background-color) !important; -} - -.interactive-item-compact .interactive-result-code-block { - margin: 0 0 8px 0; -} - -.cschat-item-container .interactive-result-code-block .monaco-toolbar .monaco-action-bar .actions-container { - padding-inline-start: unset; -} - -.chat-notification-widget .chat-info-codicon, -.chat-notification-widget .chat-error-codicon, -.chat-notification-widget .chat-warning-codicon { - display: flex; - align-items: start; - gap: 8px; -} - -.cschat-item-container .value .chat-notification-widget .rendered-markdown p { - margin: 0; -} - -.interactive-response .interactive-response-error-details { - display: flex; - align-items: start; - gap: 6px; -} - -.interactive-response .interactive-response-error-details .rendered-markdown :last-child { - margin-bottom: 0px; -} - -.chat-notification-widget .chat-info-codicon .codicon, -.chat-notification-widget .chat-error-codicon .codicon, -.chat-notification-widget .chat-warning-codicon .codicon { - margin-top: 2px; -} - -.interactive-response .interactive-response-error-details .codicon { - margin-top: 1px; -} - -.chat-used-context-list .codicon-warning { - color: var(--vscode-notificationsWarningIcon-foreground); /* Have to override default styles which apply to all lists */ -} - -.chat-used-context-list .monaco-icon-label-container { - color: var(--vscode-interactive-session-foreground); -} - -.chat-notification-widget .chat-warning-codicon .codicon-warning { - color: var(--vscode-notificationsWarningIcon-foreground) !important; /* Have to override default styles which apply to all lists */ -} - -.chat-notification-widget .chat-error-codicon .codicon-error, -.interactive-response .interactive-response-error-details .codicon-error { - color: var(--vscode-errorForeground) !important; /* Have to override default styles which apply to all lists */ -} - -.chat-notification-widget .chat-info-codicon .codicon-info, -.interactive-response .interactive-response-error-details .codicon-info { - color: var(--vscode-notificationsInfoIcon-foreground) !important; /* Have to override default styles which apply to all lists */ -} - -.cschat-session .cschat-input-part { - padding: 12px 20px; - display: flex; - flex-direction: column; - border-top: 1px solid var(--vscode-chat-requestBorder); -} - -.cschat-session .cschat-input-part.compact { - margin: 0; - padding: 6px 0px; -} - -.cschat-session .chat-attached-context .chat-attached-context-attachment { - display: flex; - gap: 4px; -} - -.cschat-session .chat-attached-context .chat-attached-context-attachment .monaco-button:hover { - cursor: pointer; -} - -.cschat-session .chat-attached-context .chat-attached-context-attachment .monaco-button { - display: flex; - align-items: center; -} - -.cschat-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label-container { - display: flex; -} - -.cschat-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label-container .monaco-highlighted-label { - display: flex !important; - align-items: center !important; -} - -.cschat-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label .monaco-button.codicon.codicon-close, -.cschat-session .chat-attached-context .chat-attached-context-attachment .monaco-button.codicon.codicon-close { - color: var(--vscode-descriptionForeground); -} - -.cschat-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label .codicon { - padding-left: 4px; -} - -.cschat-session .chat-attached-context { - padding: 0 0 8px 0; - display: flex; - gap: 4px; - flex-wrap: wrap; -} - -.cschat-session .chat-attached-context .chat-attached-context-attachment { - padding: 2px; - border: 1px solid var(--vscode-chat-requestBorder, var(--vscode-input-background, transparent)); - border-radius: 4px; - height: 18px; - max-width: 100%; -} - -.cschat-session-followups { - display: flex; - flex-direction: column; - gap: 6px; - align-items: start; -} - -.cschat-session-followups .monaco-button { - text-align: left; - width: initial; -} - -.cschat-session-followups .monaco-button .codicon { - margin-left: 0; - margin-top: 1px; -} - -.cschat-item-container .interactive-response-followups .monaco-button { - padding: 4px 8px; -} - -.cschat-session .cschat-input-part .interactive-input-followups .cschat-session-followups { - margin-bottom: 8px; -} - -.cschat-session .cschat-input-part .interactive-input-followups .cschat-session-followups .monaco-button { - display: block; - color: var(--vscode-textLink-foreground); - font-size: 12px; - - /* clamp to max 3 lines */ - display: -webkit-box; - -webkit-line-clamp: 3; - -webkit-box-orient: vertical; - overflow: hidden; -} - -.cschat-session .cschat-input-part .interactive-input-followups .cschat-session-followups code { - font-family: var(--monaco-monospace-font); - font-size: 11px; -} - -.cschat-session .cschat-input-part .interactive-input-followups .cschat-session-followups .monaco-button .codicon-sparkle { - float: left; -} - -.cschat-session-followups .monaco-button.interactive-followup-reply { - padding: 0px; - border: none; -} - -.interactive-welcome .value .cschat-session-followups { - margin-bottom: 16px; -} - -.cschat-item-container .monaco-toolbar .codicon { - /* Very aggressive list styles try to apply focus colors to every codicon in a list row. */ - color: var(--vscode-icon-foreground) !important; -} - -.cschat-item-container.filtered-response .value > .rendered-markdown { - pointer-events: none; - -webkit-mask-image: linear-gradient(rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0.05) 60%, rgba(0, 0, 0, 0.00) 80%); - mask-image: linear-gradient(rgba(0, 0, 0, 0.75), rgba(0, 0, 0, 0.05) 60%, rgba(0, 0, 0, 0.00) 80%); -} - -/* #region Quick Chat */ - -.quick-input-widget .cschat-session .cschat-input-part { - padding: 8px 6px 6px 6px; - margin: 0 3px; -} - -.quick-input-widget .cschat-session .cschat-input-part .interactive-execute-toolbar { - margin-bottom: 1px; -} - -.quick-input-widget .cschat-session .interactive-input-and-execute-toolbar { - margin: 0; - border-radius: 2px; - padding: 0 4px 0 6px; -} - -.quick-input-widget .cschat-list { - border-bottom-right-radius: 6px; - border-bottom-left-radius: 6px; -} - -.quick-input-widget .interactive-response { - min-height: 86px; -} - -.quick-input-widget .cschat-session .interactive-input-and-execute-toolbar, -.quick-input-widget .cschat-session .interactive-input-and-execute-toolbar .monaco-editor, -.quick-input-widget .cschat-session .interactive-input-and-execute-toolbar .monaco-editor .monaco-editor-background { - background-color: unset !important; -} - -/* #endregion */ - -.interactive-response-progress-tree .monaco-list-row:not(.selected) .monaco-tl-row:hover { - background-color: var(--vscode-list-hoverBackground); -} - -.interactive-response-progress-tree { - margin: 16px 0px; -} - -.interactive-response-progress-tree.focused { - border-color: var(--vscode-focusBorder, transparent); -} - -.cschat-item-container .value .interactive-response-placeholder-codicon .codicon { - color: var(--vscode-editorGhostText-foreground); -} - -.cschat-item-container .value .interactive-response-placeholder-content { - color: var(--vscode-editorGhostText-foreground); - font-size: 12px; - margin-bottom: 16px; -} - -.cschat-item-container .value .interactive-response-placeholder-content p { - margin: 0; -} - -.interactive-response .interactive-response-codicon-details { - display: flex; - align-items: start; - gap: 6px; -} - -.chat-used-context-list .monaco-list { - border: none; - border-radius: 4px; - width: auto; -} - -.cschat-item-container .chat-resource-widget { - background-color: var(--vscode-chat-slashCommandBackground); - color: var(--vscode-chat-slashCommandForeground); -} - -.cschat-item-container .chat-resource-widget, -.cschat-item-container .chat-agent-widget .monaco-button { - border-radius: 4px; - padding: 1px 3px; -} - -.cschat-item-container .chat-agent-widget .monaco-text-button { - display: inline; - border: none; -} - -.cschat-session .chat-used-context.chat-used-context-collapsed .chat-used-context-list { - display: none; -} - -.cschat-session .chat-used-context { - display: flex; - flex-direction: column; - gap: 2px; -} - -.interactive-response-progress-tree, -.cschat-item-container .chat-notification-widget, -.cschat-session .chat-used-context-list { - border: 1px solid var(--vscode-chat-requestBorder); - border-radius: 4px; - margin-bottom: 8px; - padding: 6px 8px; -} - -.cschat-item-container .chat-notification-widget { - padding: 8px 12px; -} - -.cschat-session .chat-used-context-list .monaco-list .monaco-list-row { - border-radius: 2px; -} - -.cschat-session .chat-used-context-label { - font-size: 12px; - color: var(--vscode-descriptionForeground); - user-select: none; -} - -.cschat-session .chat-used-context-label:hover { - opacity: unset; -} - -.cschat-session .chat-used-context-label .monaco-button { - /* unset Button styles */ - display: inline-flex; - gap: 4px; - width: 100%; - border: none; - border-radius: 4px; - padding: 4px 8px 4px 0; - text-align: initial; - justify-content: initial; -} - -.cschat-session .chat-used-context-label .monaco-button:hover { - background-color: var(--vscode-list-hoverBackground); - color: var(--vscode-foreground); - -} - -.cschat-session .chat-used-context-label .monaco-text-button:focus { - outline: none; -} - -.cschat-session .chat-used-context-label .monaco-text-button:focus-visible { - outline: 1px solid var(--vscode-focusBorder); -} - -.cschat-session .chat-used-context .chat-used-context-label .monaco-button .codicon { - margin: 0 0 0 4px; -} - -.cschat-item-container .rendered-markdown.progress-step { - display: flex; - margin-left: 4px; - white-space: normal; -} - -.cschat-item-container .rendered-markdown.progress-step > p { - color: var(--vscode-descriptionForeground); - font-size: 12px; - display: flex; - gap: 8px; - align-items: center; - margin-bottom: 6px; -} - -.cschat-item-container .rendered-markdown.progress-step > p .codicon { - /* Very aggressive list styles try to apply focus colors to every codicon in a list row. */ - color: var(--vscode-icon-foreground) !important; -} - -.cschat-item-container .rendered-markdown.progress-step > p .codicon.codicon-check { - color: var(--vscode-debugIcon-startForeground) !important; -} - -.cschat-item-container .chat-command-button { - display: flex; - margin-bottom: 16px; -} - -.cschat-item-container .chat-notification-widget { - display: flex; - flex-direction: row; - gap: 6px; -} - -.cschat-item-container .chat-command-button .monaco-button, -.chat-confirmation-widget .chat-confirmation-buttons-container .monaco-button { - text-align: left; - width: initial; - padding: 4px 8px; -} - -.cschat-item-container .chat-command-button .monaco-button .codicon { - margin-left: 0; - margin-top: 1px; -} - -.keybindingPillWidget { - display: flex !important; - border-radius: 2px; - overflow: hidden; -} - -.keybindingPillWidget .keybinding-pill { - white-space: nowrap; - border-radius: 0; -} diff --git a/src/vs/workbench/contrib/aideChat/browser/media/chatAgentHover.css b/src/vs/workbench/contrib/aideChat/browser/media/chatAgentHover.css deleted file mode 100644 index 29e38f48cad..00000000000 --- a/src/vs/workbench/contrib/aideChat/browser/media/chatAgentHover.css +++ /dev/null @@ -1,85 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.chat-agent-hover { - line-height: unset; - padding: 6px 0px; -} - -.chat-agent-hover-header { - display: flex; - gap: 8px; - margin-bottom: 4px; -} - -.chat-agent-hover-icon img, -.chat-agent-hover-icon .codicon { - width: 32px; - height: 32px; - border-radius: 50%; - outline: 1px solid var(--vscode-chat-requestBorder); -} - -.chat-agent-hover .chat-agent-hover-icon .codicon { - font-size: 23px !important; /* Override workbench hover styles */ - display: flex; - justify-content: center; - align-items: center; -} - -.chat-agent-hover-publisher { - display: flex; - gap: 4px; -} - -.chat-agent-hover .chat-agent-hover-publisher .codicon.codicon-extensions-verified-publisher { - color: var(--vscode-extensionIcon-verifiedForeground); -} - -.chat-agent-hover .extension-verified-publisher { - display: none; -} - -.chat-agent-hover.verifiedPublisher .extension-verified-publisher { - display: flex; -} - -.chat-agent-hover .chat-agent-hover-warning .codicon { - color: var(--vscode-notificationsWarningIcon-foreground) !important; - margin-right: 3px; -} - -.chat-agent-hover.allowedName .chat-agent-hover-warning { - display: none; -} - -.chat-agent-hover-header .chat-agent-hover-name { - font-size: 14px; - font-weight: 600; -} - -.chat-agent-hover-header .chat-agent-hover-details { - font-size: 12px; -} - -.chat-agent-hover-extension { - display: flex; - gap: 6px; - color: var(--vscode-descriptionForeground); -} - -.chat-agent-hover.noExtensionName .chat-agent-hover-separator, -.chat-agent-hover.noExtensionName .chat-agent-hover-extension-name { - display: none; -} - -.chat-agent-hover-separator { - opacity: 0.7; -} - -.chat-agent-hover-description, -.chat-agent-hover-warning { - font-size: 13px; -} diff --git a/src/vs/workbench/contrib/aideChat/common/aideChatAgents.ts b/src/vs/workbench/contrib/aideChat/common/aideChatAgents.ts deleted file mode 100644 index 14c6a19b534..00000000000 --- a/src/vs/workbench/contrib/aideChat/common/aideChatAgents.ts +++ /dev/null @@ -1,561 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { findLast } from '../../../../base/common/arraysFind.js'; -import { timeout } from '../../../../base/common/async.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { IMarkdownString } from '../../../../base/common/htmlContent.js'; -import { Iterable } from '../../../../base/common/iterator.js'; -import { IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { revive } from '../../../../base/common/marshalling.js'; -import { IObservable } from '../../../../base/common/observable.js'; -import { observableValue } from '../../../../base/common/observableInternal/base.js'; -import { equalsIgnoreCase } from '../../../../base/common/strings.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { URI } from '../../../../base/common/uri.js'; -import { Command, ProviderResult } from '../../../../editor/common/languages.js'; -import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { IProductService } from '../../../../platform/product/common/productService.js'; -import { asJson, IRequestService } from '../../../../platform/request/common/request.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { CONTEXT_CHAT_ENABLED } from '../../../../workbench/contrib/aideChat/common/aideChatContextKeys.js'; -import { IAideChatProgressResponseContent, IChatRequestVariableData, ISerializableChatAgentData } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { IRawChatCommandContribution, RawChatParticipantLocation } from '../../../../workbench/contrib/aideChat/common/aideChatParticipantContribTypes.js'; -import { IAideChatFollowup, IAideChatProgress, IAideChatResponseErrorDetails, IAideChatTaskDto } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; - -//#region agent service, commands etc - -export interface IChatAgentHistoryEntry { - request: IAideChatAgentRequest; - response: ReadonlyArray; - result: IAideChatAgentResult; -} - -export enum AideChatAgentLocation { - Panel = 'panel', - Terminal = 'terminal', - Notebook = 'notebook', - Editor = 'editor' -} - -export namespace AideChatAgentLocation { - export function fromRaw(value: RawChatParticipantLocation | string): AideChatAgentLocation { - switch (value) { - case 'panel': return AideChatAgentLocation.Panel; - case 'terminal': return AideChatAgentLocation.Terminal; - case 'notebook': return AideChatAgentLocation.Notebook; - case 'editor': return AideChatAgentLocation.Editor; - } - return AideChatAgentLocation.Panel; - } -} - -export interface IChatAgentData { - id: string; - name: string; - fullName?: string; - description?: string; - when?: string; - extensionId: ExtensionIdentifier; - extensionPublisherId: string; - /** This is the extension publisher id, or, in the case of a dynamically registered participant (remote agent), whatever publisher name we have for it */ - publisherDisplayName?: string; - extensionDisplayName: string; - /** The agent invoked when no agent is specified */ - isDefault?: boolean; - /** This agent is not contributed in package.json, but is registered dynamically */ - isDynamic?: boolean; - metadata: IAideChatAgentMetadata; - slashCommands: IChatAgentCommand[]; - defaultImplicitVariables?: string[]; - locations: AideChatAgentLocation[]; -} - -export interface IChatAgentImplementation { - invoke(request: IAideChatAgentRequest, progress: (part: IAideChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; - provideFollowups?(request: IAideChatAgentRequest, result: IAideChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; - provideWelcomeMessage?(location: AideChatAgentLocation, token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined>; - provideSampleQuestions?(location: AideChatAgentLocation, token: CancellationToken): ProviderResult; -} - -export type IChatAgent = IChatAgentData & IChatAgentImplementation; - -export interface IChatAgentCommand extends IRawChatCommandContribution { - followupPlaceholder?: string; -} - -export interface IChatRequesterInformation { - name: string; - - /** - * A full URI for the icon of the requester. - */ - icon?: URI; -} - -export interface IAideChatAgentMetadata { - helpTextPrefix?: string | IMarkdownString; - helpTextVariablesPrefix?: string | IMarkdownString; - helpTextPostfix?: string | IMarkdownString; - isSecondary?: boolean; // Invoked by ctrl/cmd+enter - icon?: URI; - iconDark?: URI; - themeIcon?: ThemeIcon | URI; - sampleRequest?: string; - supportIssueReporting?: boolean; - followupPlaceholder?: string; - isSticky?: boolean; - requester?: IChatRequesterInformation; - supportsSlowVariables?: boolean; -} - - -export interface IAideChatAgentRequest { - sessionId: string; - requestId: string; - agentId: string; - command?: string; - message: string; - attempt?: number; - enableCommandDetection?: boolean; - variables: IChatRequestVariableData; - location: AideChatAgentLocation; - acceptedConfirmationData?: any[]; - rejectedConfirmationData?: any[]; -} - -export interface IAideChatAgentResult { - errorDetails?: IAideChatResponseErrorDetails; - timings?: { - firstProgress?: number; - totalElapsed: number; - }; - /** Extra properties that the agent can use to identify a result */ - readonly metadata?: { readonly [key: string]: any }; -} - -export const IAideChatAgentService = createDecorator('aideChatAgentService'); - -interface IChatAgentEntry { - data: IChatAgentData; - impl?: IChatAgentImplementation; -} - -export interface IChatAgentCompletionItem { - id: string; - name?: string; - fullName?: string; - icon?: ThemeIcon; - value: unknown; - command?: Command; -} - -export interface IAideChatAgentService { - _serviceBrand: undefined; - /** - * undefined when an agent was removed IChatAgent - */ - readonly onDidChangeAgents: Event; - registerAgent(id: string, data: IChatAgentData): IDisposable; - registerAgentImplementation(id: string, agent: IChatAgentImplementation): IDisposable; - registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable; - registerAgentCompletionProvider(id: string, provider: (query: string, token: CancellationToken) => Promise): IDisposable; - getAgentCompletionItems(id: string, query: string, token: CancellationToken): Promise; - invokeAgent(agent: string, request: IAideChatAgentRequest, progress: (part: IAideChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; - getFollowups(id: string, request: IAideChatAgentRequest, result: IAideChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise; - getAgent(id: string): IChatAgentData | undefined; - getAgentByFullyQualifiedId(id: string): IChatAgentData | undefined; - getAgents(): IChatAgentData[]; - getActivatedAgents(): Array; - getAgentsByName(name: string): IChatAgentData[]; - agentHasDupeName(id: string): boolean; - - /** - * Get the default agent (only if activated) - */ - getDefaultAgent(location: AideChatAgentLocation): IChatAgent | undefined; - - /** - * Get the default agent data that has been contributed (may not be activated yet) - */ - getContributedDefaultAgent(location: AideChatAgentLocation): IChatAgentData | undefined; - getSecondaryAgent(): IChatAgentData | undefined; - updateAgent(id: string, updateMetadata: IAideChatAgentMetadata): void; -} - -export class ChatAgentService implements IAideChatAgentService { - - public static readonly AGENT_LEADER = '@'; - - declare _serviceBrand: undefined; - - private _agents = new Map(); - - private readonly _onDidChangeAgents = new Emitter(); - readonly onDidChangeAgents: Event = this._onDidChangeAgents.event; - - private readonly _hasDefaultAgent: IContextKey; - - constructor( - @IContextKeyService private readonly contextKeyService: IContextKeyService, - ) { - this._hasDefaultAgent = CONTEXT_CHAT_ENABLED.bindTo(this.contextKeyService); - } - - registerAgent(id: string, data: IChatAgentData): IDisposable { - const existingAgent = this.getAgent(id); - if (existingAgent) { - throw new Error(`Agent already registered: ${JSON.stringify(id)}`); - } - - const that = this; - const commands = data.slashCommands; - data = { - ...data, - get slashCommands() { - return commands.filter(c => !c.when || that.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(c.when))); - } - }; - const entry = { data }; - this._agents.set(id, entry); - return toDisposable(() => { - this._agents.delete(id); - this._onDidChangeAgents.fire(undefined); - }); - } - - registerAgentImplementation(id: string, agentImpl: IChatAgentImplementation): IDisposable { - const entry = this._agents.get(id); - if (!entry) { - throw new Error(`Unknown agent: ${JSON.stringify(id)}`); - } - - if (entry.impl) { - throw new Error(`Agent already has implementation: ${JSON.stringify(id)}`); - } - - if (entry.data.isDefault) { - this._hasDefaultAgent.set(true); - } - - entry.impl = agentImpl; - this._onDidChangeAgents.fire(new MergedChatAgent(entry.data, agentImpl)); - - return toDisposable(() => { - entry.impl = undefined; - this._onDidChangeAgents.fire(undefined); - - if (entry.data.isDefault) { - this._hasDefaultAgent.set(false); - } - }); - } - - registerDynamicAgent(data: IChatAgentData, agentImpl: IChatAgentImplementation): IDisposable { - data.isDynamic = true; - const agent = { data, impl: agentImpl }; - this._agents.set(data.id, agent); - this._onDidChangeAgents.fire(new MergedChatAgent(data, agentImpl)); - - return toDisposable(() => { - this._agents.delete(data.id); - this._onDidChangeAgents.fire(undefined); - }); - } - - private _agentCompletionProviders = new Map Promise>(); - - registerAgentCompletionProvider(id: string, provider: (query: string, token: CancellationToken) => Promise) { - this._agentCompletionProviders.set(id, provider); - return { - dispose: () => { this._agentCompletionProviders.delete(id); } - }; - } - - async getAgentCompletionItems(id: string, query: string, token: CancellationToken) { - return await this._agentCompletionProviders.get(id)?.(query, token) ?? []; - } - - updateAgent(id: string, updateMetadata: IAideChatAgentMetadata): void { - const agent = this._agents.get(id); - if (!agent?.impl) { - throw new Error(`No activated agent with id ${JSON.stringify(id)} registered`); - } - agent.data.metadata = { ...agent.data.metadata, ...updateMetadata }; - this._onDidChangeAgents.fire(new MergedChatAgent(agent.data, agent.impl)); - } - - getDefaultAgent(location: AideChatAgentLocation): IChatAgent | undefined { - return findLast(this.getActivatedAgents(), a => !!a.isDefault && a.locations.includes(location)); - } - - getContributedDefaultAgent(location: AideChatAgentLocation): IChatAgentData | undefined { - return this.getAgents().find(a => !!a.isDefault && a.locations.includes(location)); - } - - getSecondaryAgent(): IChatAgentData | undefined { - // TODO also static - return Iterable.find(this._agents.values(), a => !!a.data.metadata.isSecondary)?.data; - } - - getAgent(id: string): IChatAgentData | undefined { - if (!this._agentIsEnabled(id)) { - return; - } - - return this._agents.get(id)?.data; - } - - private _agentIsEnabled(id: string): boolean { - const entry = this._agents.get(id); - return !entry?.data.when || this.contextKeyService.contextMatchesRules(ContextKeyExpr.deserialize(entry.data.when)); - } - - getAgentByFullyQualifiedId(id: string): IChatAgentData | undefined { - const agent = Iterable.find(this._agents.values(), a => getFullyQualifiedId(a.data) === id)?.data; - if (agent && !this._agentIsEnabled(agent.id)) { - return; - } - - return agent; - } - - /** - * Returns all agent datas that exist- static registered and dynamic ones. - */ - getAgents(): IChatAgentData[] { - return Array.from(this._agents.values()) - .map(entry => entry.data) - .filter(a => this._agentIsEnabled(a.id)); - } - - getActivatedAgents(): IChatAgent[] { - return Array.from(this._agents.values()) - .filter(a => !!a.impl) - .filter(a => this._agentIsEnabled(a.data.id)) - .map(a => new MergedChatAgent(a.data, a.impl!)); - } - - getAgentsByName(name: string): IChatAgentData[] { - return this.getAgents().filter(a => a.name === name); - } - - agentHasDupeName(id: string): boolean { - const agent = this.getAgent(id); - if (!agent) { - return false; - } - - return this.getAgentsByName(agent.name) - .filter(a => a.extensionId.value !== agent.extensionId.value).length > 0; - } - - async invokeAgent(id: string, request: IAideChatAgentRequest, progress: (part: IAideChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { - const data = this._agents.get(id); - if (!data?.impl) { - throw new Error(`No activated agent with id "${id}"`); - } - - return await data.impl.invoke(request, progress, history, token); - } - - async getFollowups(id: string, request: IAideChatAgentRequest, result: IAideChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { - const data = this._agents.get(id); - if (!data?.impl) { - throw new Error(`No activated agent with id "${id}"`); - } - - if (!data.impl?.provideFollowups) { - return []; - } - - return data.impl.provideFollowups(request, result, history, token); - } -} - -export class MergedChatAgent implements IChatAgent { - constructor( - private readonly data: IChatAgentData, - private readonly impl: IChatAgentImplementation - ) { } - - get id(): string { return this.data.id; } - get name(): string { return this.data.name ?? ''; } - get fullName(): string { return this.data.fullName ?? ''; } - get description(): string { return this.data.description ?? ''; } - get extensionId(): ExtensionIdentifier { return this.data.extensionId; } - get extensionPublisherId(): string { return this.data.extensionPublisherId; } - get extensionPublisherDisplayName() { return this.data.publisherDisplayName; } - get extensionDisplayName(): string { return this.data.extensionDisplayName; } - get isDefault(): boolean | undefined { return this.data.isDefault; } - get metadata(): IAideChatAgentMetadata { return this.data.metadata; } - get slashCommands(): IChatAgentCommand[] { return this.data.slashCommands; } - get defaultImplicitVariables(): string[] | undefined { return this.data.defaultImplicitVariables; } - get locations(): AideChatAgentLocation[] { return this.data.locations; } - - async invoke(request: IAideChatAgentRequest, progress: (part: IAideChatProgress) => void, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { - return this.impl.invoke(request, progress, history, token); - } - - async provideFollowups(request: IAideChatAgentRequest, result: IAideChatAgentResult, history: IChatAgentHistoryEntry[], token: CancellationToken): Promise { - if (this.impl.provideFollowups) { - return this.impl.provideFollowups(request, result, history, token); - } - - return []; - } - - provideWelcomeMessage(location: AideChatAgentLocation, token: CancellationToken): ProviderResult<(string | IMarkdownString)[] | undefined> { - if (this.impl.provideWelcomeMessage) { - return this.impl.provideWelcomeMessage(location, token); - } - - return undefined; - } - - provideSampleQuestions(location: AideChatAgentLocation, token: CancellationToken): ProviderResult { - if (this.impl.provideSampleQuestions) { - return this.impl.provideSampleQuestions(location, token); - } - - return undefined; - } -} - -export const IAideChatAgentNameService = createDecorator('aideChatAgentNameService'); - -type IChatParticipantRegistry = { [name: string]: string[] }; - -interface IChatParticipantRegistryResponse { - readonly version: number; - readonly restrictedChatParticipants: IChatParticipantRegistry; -} - -export interface IAideChatAgentNameService { - _serviceBrand: undefined; - getAgentNameRestriction(chatAgentData: IChatAgentData): boolean; -} - -export class ChatAgentNameService implements IAideChatAgentNameService { - - private static readonly StorageKey = 'aideChat.participantNameRegistry'; - - declare _serviceBrand: undefined; - - private readonly url!: string; - private registry = observableValue(this, Object.create(null)); - private disposed = false; - - constructor( - @IProductService productService: IProductService, - @IRequestService private readonly requestService: IRequestService, - @ILogService private readonly logService: ILogService, - @IStorageService private readonly storageService: IStorageService - ) { - if (!productService.chatParticipantRegistry) { - return; - } - - this.url = productService.chatParticipantRegistry; - - const raw = storageService.get(ChatAgentNameService.StorageKey, StorageScope.APPLICATION); - - try { - this.registry.set(JSON.parse(raw ?? '{}'), undefined); - } catch (err) { - storageService.remove(ChatAgentNameService.StorageKey, StorageScope.APPLICATION); - } - - this.refresh(); - } - - private refresh(): void { - if (this.disposed) { - return; - } - - this.update() - .catch(err => this.logService.warn('Failed to fetch chat participant registry', err)) - .then(() => timeout(5 * 60 * 1000)) // every 5 minutes - .then(() => this.refresh()); - } - - private async update(): Promise { - const context = await this.requestService.request({ type: 'GET', url: this.url }, CancellationToken.None); - - if (context.res.statusCode !== 200) { - throw new Error('Could not get extensions report.'); - } - - const result = await asJson(context); - - if (!result || result.version !== 1) { - throw new Error('Unexpected chat participant registry response.'); - } - - const registry = result.restrictedChatParticipants; - this.registry.set(registry, undefined); - this.storageService.store(ChatAgentNameService.StorageKey, JSON.stringify(registry), StorageScope.APPLICATION, StorageTarget.MACHINE); - } - - /** - * Returns true if the agent is allowed to use this name - */ - getAgentNameRestriction(chatAgentData: IChatAgentData): boolean { - // TODO would like to use observables here but nothing uses it downstream and I'm not sure how to combine these two - const nameAllowed = this.checkAgentNameRestriction(chatAgentData.name, chatAgentData).get(); - const fullNameAllowed = !chatAgentData.fullName || this.checkAgentNameRestriction(chatAgentData.fullName.replace(/\s/g, ''), chatAgentData).get(); - return nameAllowed && fullNameAllowed; - } - - private checkAgentNameRestriction(name: string, chatAgentData: IChatAgentData): IObservable { - // Registry is a map of name to an array of extension publisher IDs or extension IDs that are allowed to use it. - // Look up the list of extensions that are allowed to use this name - const allowList = this.registry.map(registry => registry[name.toLowerCase()]); - return allowList.map(allowList => { - if (!allowList) { - return true; - } - - return allowList.some(id => equalsIgnoreCase(id, id.includes('.') ? chatAgentData.extensionId.value : chatAgentData.extensionPublisherId)); - }); - } - - dispose() { - this.disposed = true; - } -} - -export function getFullyQualifiedId(chatAgentData: IChatAgentData): string { - return `${chatAgentData.extensionId.value}.${chatAgentData.id}`; -} - -export function reviveSerializedAgent(raw: ISerializableChatAgentData): IChatAgentData { - const agent = 'name' in raw ? - raw : - { - ...(raw as any), - name: (raw as any).id, - }; - - // Fill in required fields that may be missing from old data - if (!('extensionPublisherId' in agent)) { - agent.extensionPublisherId = agent.extensionPublisher ?? ''; - } - - if (!('extensionDisplayName' in agent)) { - agent.extensionDisplayName = ''; - } - - if (!('extensionId' in agent)) { - agent.extensionId = new ExtensionIdentifier(''); - } - - return revive(agent); -} diff --git a/src/vs/workbench/contrib/aideChat/common/aideChatColors.ts b/src/vs/workbench/contrib/aideChat/common/aideChatColors.ts deleted file mode 100644 index 549af78b4cb..00000000000 --- a/src/vs/workbench/contrib/aideChat/common/aideChatColors.ts +++ /dev/null @@ -1,44 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Color, RGBA } from '../../../../base/common/color.js'; -import { localize } from '../../../../nls.js'; -import { badgeBackground, badgeForeground, contrastBorder, editorBackground, editorWidgetBackground, foreground, registerColor, transparent } from '../../../../platform/theme/common/colorRegistry.js'; - -export const chatRequestBorder = registerColor( - 'aideChat.requestBorder', - { dark: new Color(new RGBA(255, 255, 255, 0.10)), light: new Color(new RGBA(0, 0, 0, 0.10)), hcDark: contrastBorder, hcLight: contrastBorder, }, - localize('aideChat.requestBorder', 'The border color of a chat request.') -); - -export const chatRequestBackground = registerColor( - 'aideChat.requestBackground', - { dark: transparent(editorBackground, 0.62), light: transparent(editorBackground, 0.62), hcDark: editorWidgetBackground, hcLight: null }, - localize('aideChat.requestBackground', 'The background color of a chat request.') -); - -export const chatSlashCommandBackground = registerColor( - 'aideChat.slashCommandBackground', - { dark: '#34414b8f', light: '#d2ecff99', hcDark: Color.white, hcLight: badgeBackground }, - localize('aideChat.slashCommandBackground', 'The background color of a chat slash command.') -); - -export const chatSlashCommandForeground = registerColor( - 'aideChat.slashCommandForeground', - { dark: '#40A6FF', light: '#306CA2', hcDark: Color.black, hcLight: badgeForeground }, - localize('aideChat.slashCommandForeground', 'The foreground color of a chat slash command.') -); - -export const chatAvatarBackground = registerColor( - 'aideChat.avatarBackground', - { dark: '#1f1f1f', light: '#f2f2f2', hcDark: Color.black, hcLight: Color.white, }, - localize('aideChat.avatarBackground', 'The background color of a chat avatar.') -); - -export const chatAvatarForeground = registerColor( - 'aideChat.avatarForeground', - { dark: foreground, light: foreground, hcDark: foreground, hcLight: foreground, }, - localize('aideChat.avatarForeground', 'The foreground color of a chat avatar.') -); diff --git a/src/vs/workbench/contrib/aideChat/common/aideChatContextKeys.ts b/src/vs/workbench/contrib/aideChat/common/aideChatContextKeys.ts deleted file mode 100644 index 9b17a1a9457..00000000000 --- a/src/vs/workbench/contrib/aideChat/common/aideChatContextKeys.ts +++ /dev/null @@ -1,31 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { localize } from '../../../../nls.js'; -import { RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { AideChatAgentLocation } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; - -export const CONTEXT_RESPONSE_VOTE = new RawContextKey('aideChatSessionResponseVote', '', { type: 'string', description: localize('aideChatResponseVote', "When the response has been voted up, is set to 'up'. When voted down, is set to 'down'. Otherwise an empty string.") }); -export const CONTEXT_RESPONSE_DETECTED_AGENT_COMMAND = new RawContextKey('aideChatSessionResponseDetectedAgentOrCommand', false, { type: 'boolean', description: localize('aideChatSessionResponseDetectedAgentOrCommand', "When the agent or command was automatically detected") }); -export const CONTEXT_CHAT_RESPONSE_SUPPORT_ISSUE_REPORTING = new RawContextKey('aideChatResponseSupportsIssueReporting', false, { type: 'boolean', description: localize('aideChatResponseSupportsIssueReporting', "True when the current chat response supports issue reporting.") }); -export const CONTEXT_RESPONSE_FILTERED = new RawContextKey('aideChatSessionResponseFiltered', false, { type: 'boolean', description: localize('aideChatResponseFiltered', "True when the chat response was filtered out by the server.") }); -export const CONTEXT_CHAT_REQUEST_IN_PROGRESS = new RawContextKey('aideChatSessionRequestInProgress', false, { type: 'boolean', description: localize('aideChatRequestInProgress', "True when the current request is still in progress.") }); -export const CONTEXT_CHAT_HAS_REQUESTS = new RawContextKey('aideChatSessionHasRequests', false, { type: 'boolean', description: localize('aideChatHasRequests', "True when the current chat session has requests.") }); - -export const CONTEXT_RESPONSE = new RawContextKey('aideChatResponse', false, { type: 'boolean', description: localize('aideChatResponse', "The chat item is a response.") }); -export const CONTEXT_REQUEST = new RawContextKey('aideChatRequest', false, { type: 'boolean', description: localize('aideChatRequest', "The chat item is a request") }); - -export const CONTEXT_CHAT_EDIT_APPLIED = new RawContextKey('aideChatEditApplied', false, { type: 'boolean', description: localize('aideChatEditApplied', "True when the chat text edits have been applied.") }); - -export const CONTEXT_CHAT_INPUT_HAS_TEXT = new RawContextKey('aideChatInputHasText', false, { type: 'boolean', description: localize('aideChatInputHasText', "True when the chat input has text.") }); -export const CONTEXT_CHAT_INPUT_HAS_FOCUS = new RawContextKey('aideChatInputHasFocus', false, { type: 'boolean', description: localize('aideChatInputHasFocus', "True when the chat input has focus.") }); -export const CONTEXT_IN_CHAT_INPUT = new RawContextKey('inAideChatInput', false, { type: 'boolean', description: localize('inAideChatInput', "True when focus is in the chat input, false otherwise.") }); -export const CONTEXT_IN_CHAT_SESSION = new RawContextKey('inAideChat', false, { type: 'boolean', description: localize('inAideChat', "True when focus is in the chat widget, false otherwise.") }); - -export const CONTEXT_CHAT_ENABLED = new RawContextKey('aideChatIsEnabled', false, { type: 'boolean', description: localize('aideChatIsEnabled', "True when chat is enabled because a default chat participant is registered.") }); -export const CONTEXT_CHAT_INPUT_CURSOR_AT_TOP = new RawContextKey('aideChatCursorAtTop', false); -export const CONTEXT_CHAT_INPUT_HAS_AGENT = new RawContextKey('aideChatInputHasAgent', false); -export const CONTEXT_CHAT_LOCATION = new RawContextKey('aideChatLocation', undefined); -export const CONTEXT_IN_QUICK_CHAT = new RawContextKey('quickAideChatHasFocus', false, { type: 'boolean', description: localize('inQuickAideChat', "True when the quick chat UI has focus, false otherwise.") }); diff --git a/src/vs/workbench/contrib/aideChat/common/aideChatModel.ts b/src/vs/workbench/contrib/aideChat/common/aideChatModel.ts deleted file mode 100644 index de0e7d1a616..00000000000 --- a/src/vs/workbench/contrib/aideChat/common/aideChatModel.ts +++ /dev/null @@ -1,1079 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { localize } from '../../../../nls.js'; -import { asArray } from '../../../../base/common/arrays.js'; -import { DeferredPromise } from '../../../../base/common/async.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { IMarkdownString, MarkdownString, isMarkdownString } from '../../../../base/common/htmlContent.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { revive } from '../../../../base/common/marshalling.js'; -import { equals } from '../../../../base/common/objects.js'; -import { basename, isEqual } from '../../../../base/common/resources.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { URI, UriComponents, UriDto, isUriComponents } from '../../../../base/common/uri.js'; -import { generateUuid } from '../../../../base/common/uuid.js'; -import { IOffsetRange, OffsetRange } from '../../../../editor/common/core/offsetRange.js'; -import { TextEdit } from '../../../../editor/common/languages.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { AideChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentHistoryEntry, IAideChatAgentRequest, IAideChatAgentResult, IAideChatAgentService, reviveSerializedAgent } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { ChatRequestTextPart, IParsedChatRequest, getPromptText, reviveParsedChatRequest } from '../../../../workbench/contrib/aideChat/common/aideChatParserTypes.js'; -import { IAideChatAgentMarkdownContentWithVulnerability, IAideChatCommandButton, IAideChatConfirmation, IAideChatContentInlineReference, IAideChatContentReference, IAideChatFollowup, IAideChatMarkdownContent, IAideChatProgressMessage, IChatResponseProgressFileTreeData, IAideChatTask, IAideChatTextEdit, IChatTreeData, IChatUsedContext, IAideChatWarningMessage, AideChatAgentVoteDirection, isIUsedContext, IAideChatProgress } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { IAideChatRequestVariableValue } from '../../../../workbench/contrib/aideChat/common/aideChatVariables.js'; - -export interface IAideChatRequestVariableEntry { - id: string; - fullName?: string; - icon?: ThemeIcon; - name: string; - modelDescription?: string; - range?: IOffsetRange; - value: IAideChatRequestVariableValue; - references?: IAideChatContentReference[]; - isDynamic?: boolean; - isFile?: boolean; -} - -export interface IChatRequestVariableData { - variables: IAideChatRequestVariableEntry[]; -} - -export interface IChatRequestModel { - readonly id: string; - readonly username: string; - readonly avatarIconUri?: URI; - readonly session: IChatModel; - readonly message: IParsedChatRequest; - readonly attempt: number; - readonly variableData: IChatRequestVariableData; - readonly response?: IChatResponseModel; -} - -export interface IChatTextEditGroupState { - sha1: string; - applied: number; -} - -export interface IChatTextEditGroup { - uri: URI; - edits: TextEdit[][]; - state?: IChatTextEditGroupState; - kind: 'textEditGroup'; -} - -export type IAideChatProgressResponseContent = - | IAideChatMarkdownContent - | IAideChatAgentMarkdownContentWithVulnerability - | IChatTreeData - | IAideChatContentInlineReference - | IAideChatProgressMessage - | IAideChatCommandButton - | IAideChatWarningMessage - | IAideChatTask - | IChatTextEditGroup - | IAideChatConfirmation; - -export type IChatProgressRenderableResponseContent = Exclude; - -export interface IResponse { - readonly value: ReadonlyArray; - asString(): string; -} - -export interface IChatResponseModel { - readonly onDidChange: Event; - readonly id: string; - readonly requestId: string; - readonly username: string; - readonly avatarIcon?: ThemeIcon | URI; - readonly session: IChatModel; - readonly agent?: IChatAgentData; - readonly usedContext: IChatUsedContext | undefined; - readonly contentReferences: ReadonlyArray; - readonly progressMessages: ReadonlyArray; - readonly slashCommand?: IChatAgentCommand; - readonly agentOrSlashCommandDetected: boolean; - readonly response: IResponse; - readonly isComplete: boolean; - readonly isCanceled: boolean; - /** A stale response is one that has been persisted and rehydrated, so e.g. Commands that have their arguments stored in the EH are gone. */ - readonly isStale: boolean; - readonly vote: AideChatAgentVoteDirection | undefined; - readonly followups?: IAideChatFollowup[] | undefined; - readonly result?: IAideChatAgentResult; - setVote(vote: AideChatAgentVoteDirection): void; - setEditApplied(edit: IChatTextEditGroup, editCount: number): boolean; -} - -export class ChatRequestModel implements IChatRequestModel { - private static nextId = 0; - - public response: ChatResponseModel | undefined; - - public readonly id: string; - - public get session() { - return this._session; - } - - public get username(): string { - return this.session.requesterUsername; - } - - public get avatarIconUri(): URI | undefined { - return this.session.requesterAvatarIconUri; - } - - public get attempt(): number { - return this._attempt; - } - - public get variableData(): IChatRequestVariableData { - return this._variableData; - } - - public set variableData(v: IChatRequestVariableData) { - this._variableData = v; - } - - constructor( - private _session: ChatModel, - public readonly message: IParsedChatRequest, - private _variableData: IChatRequestVariableData, - private _attempt: number = 0 - ) { - this.id = 'request_' + ChatRequestModel.nextId++; - } - - adoptTo(session: ChatModel) { - this._session = session; - } -} - -export class Response implements IResponse { - private _onDidChangeValue = new Emitter(); - public get onDidChangeValue() { - return this._onDidChangeValue.event; - } - - // responseParts internally tracks all the response parts, including strings which are currently resolving, so that they can be updated when they do resolve - private _responseParts: IAideChatProgressResponseContent[]; - // responseRepr externally presents the response parts with consolidated contiguous strings (excluding tree data) - private _responseRepr!: string; - - get value(): IAideChatProgressResponseContent[] { - return this._responseParts; - } - - constructor(value: IMarkdownString | ReadonlyArray) { - this._responseParts = asArray(value).map((v) => (isMarkdownString(v) ? - { content: v, kind: 'markdownContent' } satisfies IAideChatMarkdownContent : - 'kind' in v ? v : { kind: 'treeData', treeData: v })); - - this._updateRepr(true); - } - - asString(): string { - return this._responseRepr; - } - - clear(): void { - this._responseParts = []; - this._updateRepr(true); - } - - updateContent(progress: IAideChatProgressResponseContent | IAideChatTextEdit | IAideChatTask, quiet?: boolean): void { - if (progress.kind === 'markdownContent') { - const responsePartLength = this._responseParts.length - 1; - const lastResponsePart = this._responseParts[responsePartLength]; - - if (!lastResponsePart || lastResponsePart.kind !== 'markdownContent' || !canMergeMarkdownStrings(lastResponsePart.content, progress.content)) { - // The last part can't be merged with- not markdown, or markdown with different permissions - this._responseParts.push(progress); - } else { - lastResponsePart.content = appendMarkdownString(lastResponsePart.content, progress.content); - } - this._updateRepr(quiet); - } else if (progress.kind === 'textEdit') { - if (progress.edits.length > 0) { - // merge text edits for the same file no matter when they come in - let found = false; - for (let i = 0; !found && i < this._responseParts.length; i++) { - const candidate = this._responseParts[i]; - if (candidate.kind === 'textEditGroup' && isEqual(candidate.uri, progress.uri)) { - candidate.edits.push(progress.edits); - found = true; - } - } - if (!found) { - this._responseParts.push({ - kind: 'textEditGroup', - uri: progress.uri, - edits: [progress.edits] - }); - } - this._updateRepr(quiet); - } - } else if (progress.kind === 'progressTask') { - // Add a new resolving part - const responsePosition = this._responseParts.push(progress) - 1; - this._updateRepr(quiet); - - const disp = progress.onDidAddProgress(() => { - this._updateRepr(false); - }); - - progress.task?.().then((content) => { - // Stop listening for progress updates once the task settles - disp.dispose(); - - // Replace the resolving part's content with the resolved response - if (typeof content === 'string') { - (this._responseParts[responsePosition] as IAideChatTask).content = new MarkdownString(content); - } - this._updateRepr(false); - }); - - } else { - this._responseParts.push(progress); - this._updateRepr(quiet); - } - } - - private _updateRepr(quiet?: boolean) { - this._responseRepr = this._responseParts.map(part => { - if (part.kind === 'treeData') { - return ''; - } else if (part.kind === 'inlineReference') { - return basename('uri' in part.inlineReference ? part.inlineReference.uri : part.inlineReference); - } else if (part.kind === 'command') { - return part.command.title; - } else if (part.kind === 'textEditGroup') { - return localize('editsSummary', "Made changes."); - } else if (part.kind === 'progressMessage') { - return ''; - } else if (part.kind === 'confirmation') { - return `${part.title}\n${part.message}`; - } else { - return part.content.value; - } - }) - .filter(s => s.length > 0) - .join('\n\n'); - - if (!quiet) { - this._onDidChangeValue.fire(); - } - } -} - -export class ChatResponseModel extends Disposable implements IChatResponseModel { - private readonly _onDidChange = this._register(new Emitter()); - readonly onDidChange = this._onDidChange.event; - - private static nextId = 0; - - public readonly id: string; - - public get session() { - return this._session; - } - - public get isComplete(): boolean { - return this._isComplete; - } - - public get isCanceled(): boolean { - return this._isCanceled; - } - - public get vote(): AideChatAgentVoteDirection | undefined { - return this._vote; - } - - public get followups(): IAideChatFollowup[] | undefined { - return this._followups; - } - - private _response: Response; - public get response(): IResponse { - return this._response; - } - - public get result(): IAideChatAgentResult | undefined { - return this._result; - } - - public get username(): string { - return this.session.responderUsername; - } - - public get avatarIcon(): ThemeIcon | URI | undefined { - return this.session.responderAvatarIcon; - } - - private _followups?: IAideChatFollowup[]; - - public get agent(): IChatAgentData | undefined { - return this._agent; - } - - public get slashCommand(): IChatAgentCommand | undefined { - return this._slashCommand; - } - - private _agentOrSlashCommandDetected: boolean | undefined; - public get agentOrSlashCommandDetected(): boolean { - return this._agentOrSlashCommandDetected ?? false; - } - - private _usedContext: IChatUsedContext | undefined; - public get usedContext(): IChatUsedContext | undefined { - return this._usedContext; - } - - private readonly _contentReferences: IAideChatContentReference[] = []; - public get contentReferences(): ReadonlyArray { - return this._contentReferences; - } - - private readonly _progressMessages: IAideChatProgressMessage[] = []; - public get progressMessages(): ReadonlyArray { - return this._progressMessages; - } - - private _isStale: boolean = false; - public get isStale(): boolean { - return this._isStale; - } - - constructor( - _response: IMarkdownString | ReadonlyArray, - private _session: ChatModel, - private _agent: IChatAgentData | undefined, - private _slashCommand: IChatAgentCommand | undefined, - public readonly requestId: string, - private _isComplete: boolean = false, - private _isCanceled = false, - private _vote?: AideChatAgentVoteDirection, - private _result?: IAideChatAgentResult, - followups?: ReadonlyArray - ) { - super(); - - // If we are creating a response with some existing content, consider it stale - this._isStale = Array.isArray(_response) && (_response.length !== 0 || isMarkdownString(_response) && _response.value.length !== 0); - - this._followups = followups ? [...followups] : undefined; - this._response = new Response(_response); - this._register(this._response.onDidChangeValue(() => this._onDidChange.fire())); - this.id = 'response_' + ChatResponseModel.nextId++; - } - - /** - * Apply a progress update to the actual response content. - */ - updateContent(responsePart: IAideChatProgressResponseContent | IAideChatTextEdit, quiet?: boolean) { - this._response.updateContent(responsePart, quiet); - } - - /** - * Apply one of the progress updates that are not part of the actual response content. - */ - applyReference(progress: IChatUsedContext | IAideChatContentReference) { - if (progress.kind === 'usedContext') { - this._usedContext = progress; - } else if (progress.kind === 'reference') { - this._contentReferences.push(progress); - this._onDidChange.fire(); - } - } - - setAgent(agent: IChatAgentData, slashCommand?: IChatAgentCommand) { - this._agent = agent; - this._slashCommand = slashCommand; - this._agentOrSlashCommandDetected = true; - this._onDidChange.fire(); - } - - setResult(result: IAideChatAgentResult): void { - this._result = result; - this._onDidChange.fire(); - } - - complete(): void { - if (this._result?.errorDetails?.responseIsRedacted) { - this._response.clear(); - } - - this._isComplete = true; - this._onDidChange.fire(); - } - - cancel(): void { - this._isComplete = true; - this._isCanceled = true; - this._onDidChange.fire(); - } - - setFollowups(followups: IAideChatFollowup[] | undefined): void { - this._followups = followups; - this._onDidChange.fire(); // Fire so that command followups get rendered on the row - } - - setVote(vote: AideChatAgentVoteDirection): void { - this._vote = vote; - this._onDidChange.fire(); - } - - setEditApplied(edit: IChatTextEditGroup, editCount: number): boolean { - if (!this.response.value.includes(edit)) { - return false; - } - if (!edit.state) { - return false; - } - edit.state.applied = editCount; // must not be edit.edits.length - this._onDidChange.fire(); - return true; - } - - adoptTo(session: ChatModel) { - this._session = session; - this._onDidChange.fire(); - } -} - -export interface IChatModel { - readonly onDidDispose: Event; - readonly onDidChange: Event; - readonly sessionId: string; - readonly initState: ChatModelInitState; - readonly initialLocation: AideChatAgentLocation; - readonly title: string; - readonly welcomeMessage: IChatWelcomeMessageModel | undefined; - readonly requestInProgress: boolean; - readonly inputPlaceholder?: string; - getRequests(): IChatRequestModel[]; - toExport(): IExportableChatData; - toJSON(): ISerializableChatData; -} - -export interface ISerializableChatsData { - [sessionId: string]: ISerializableChatData; -} - -export type ISerializableChatAgentData = UriDto; - -export interface ISerializableChatRequestData { - message: string | IParsedChatRequest; // string => old format - /** Is really like "prompt data". This is the message in the format in which the agent gets it + variable values. */ - variableData: IChatRequestVariableData; - response: ReadonlyArray | undefined; - agent?: ISerializableChatAgentData; - slashCommand?: IChatAgentCommand; - // responseErrorDetails: IAideChatResponseErrorDetails | undefined; - result?: IAideChatAgentResult; // Optional for backcompat - followups: ReadonlyArray | undefined; - isCanceled: boolean | undefined; - vote: AideChatAgentVoteDirection | undefined; - /** For backward compat: should be optional */ - usedContext?: IChatUsedContext; - contentReferences?: ReadonlyArray; -} - -export interface IExportableChatData { - initialLocation: AideChatAgentLocation | undefined; - welcomeMessage: (string | IAideChatFollowup[])[] | undefined; - requests: ISerializableChatRequestData[]; - requesterUsername: string; - responderUsername: string; - requesterAvatarIconUri: UriComponents | undefined; - responderAvatarIconUri: ThemeIcon | UriComponents | undefined; // Keeping Uri name for backcompat -} - -export interface ISerializableChatData extends IExportableChatData { - sessionId: string; - creationDate: number; - isImported: boolean; -} - -export function isExportableSessionData(obj: unknown): obj is IExportableChatData { - const data = obj as IExportableChatData; - return typeof data === 'object' && - typeof data.requesterUsername === 'string'; -} - -export function isSerializableSessionData(obj: unknown): obj is ISerializableChatData { - const data = obj as ISerializableChatData; - return isExportableSessionData(obj) && - typeof data.creationDate === 'number' && - typeof data.sessionId === 'string' && - obj.requests.every((request: ISerializableChatRequestData) => - !request.usedContext /* for backward compat allow missing usedContext */ || isIUsedContext(request.usedContext) - ); -} - -export type IChatChangeEvent = IChatAddRequestEvent | IChatAddResponseEvent | IChatInitEvent | IChatRemoveRequestEvent; - -export interface IChatAddRequestEvent { - kind: 'addRequest'; - request: IChatRequestModel; -} - -export interface IChatAddResponseEvent { - kind: 'addResponse'; - response: IChatResponseModel; -} - -export const enum ChatRequestRemovalReason { - /** - * "Normal" remove - */ - Removal, - - /** - * Removed because the request will be resent - */ - Resend, - - /** - * Remove because the request is moving to another model - */ - Adoption -} - -export interface IChatRemoveRequestEvent { - kind: 'removeRequest'; - requestId: string; - responseId?: string; - reason: ChatRequestRemovalReason; -} - -export interface IChatInitEvent { - kind: 'initialize'; -} - -export enum ChatModelInitState { - Created, - Initializing, - Initialized -} - -export class ChatModel extends Disposable implements IChatModel { - static getDefaultTitle(requests: (ISerializableChatRequestData | IChatRequestModel)[]): string { - const firstRequestMessage = requests.at(0)?.message ?? ''; - const message = typeof firstRequestMessage === 'string' ? - firstRequestMessage : - firstRequestMessage.text; - return message.split('\n')[0].substring(0, 50); - } - - private readonly _onDidDispose = this._register(new Emitter()); - readonly onDidDispose = this._onDidDispose.event; - - private readonly _onDidChange = this._register(new Emitter()); - readonly onDidChange = this._onDidChange.event; - - private _requests: ChatRequestModel[]; - private _initState: ChatModelInitState = ChatModelInitState.Created; - private _isInitializedDeferred = new DeferredPromise(); - - private _welcomeMessage: ChatWelcomeMessageModel | undefined; - get welcomeMessage(): ChatWelcomeMessageModel | undefined { - return this._welcomeMessage; - } - - // TODO to be clear, this is not the same as the id from the session object, which belongs to the provider. - // It's easier to be able to identify this model before its async initialization is complete - private _sessionId: string; - get sessionId(): string { - return this._sessionId; - } - - get requestInProgress(): boolean { - const lastRequest = this._requests[this._requests.length - 1]; - return !!lastRequest && !!lastRequest.response && !lastRequest.response.isComplete; - } - - get hasRequests(): boolean { - return this._requests.length > 0; - } - - private _creationDate: number; - get creationDate(): number { - return this._creationDate; - } - - private get _defaultAgent() { - return this.chatAgentService.getDefaultAgent(AideChatAgentLocation.Panel); - } - - get requesterUsername(): string { - return (this._defaultAgent ? - this._defaultAgent.metadata.requester?.name : - this.initialData?.requesterUsername) ?? ''; - } - - get responderUsername(): string { - return (this._defaultAgent ? - this._defaultAgent.fullName : - this.initialData?.responderUsername) ?? ''; - } - - private readonly _initialRequesterAvatarIconUri: URI | undefined; - get requesterAvatarIconUri(): URI | undefined { - return this._defaultAgent ? - this._defaultAgent.metadata.requester?.icon : - this._initialRequesterAvatarIconUri; - } - - private readonly _initialResponderAvatarIconUri: ThemeIcon | URI | undefined; - get responderAvatarIcon(): ThemeIcon | URI | undefined { - return this._defaultAgent ? - this._defaultAgent?.metadata.themeIcon : - this._initialResponderAvatarIconUri; - } - - get initState(): ChatModelInitState { - return this._initState; - } - - private _isImported = false; - get isImported(): boolean { - return this._isImported; - } - - get title(): string { - return ChatModel.getDefaultTitle(this._requests); - } - - get initialLocation() { - return this._initialLocation; - } - - constructor( - private readonly initialData: ISerializableChatData | IExportableChatData | undefined, - private readonly _initialLocation: AideChatAgentLocation, - @ILogService private readonly logService: ILogService, - @IAideChatAgentService private readonly chatAgentService: IAideChatAgentService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - ) { - super(); - - this._isImported = (!!initialData && !isSerializableSessionData(initialData)) || (initialData?.isImported ?? false); - this._sessionId = (isSerializableSessionData(initialData) && initialData.sessionId) || generateUuid(); - this._requests = initialData ? this._deserialize(initialData) : []; - this._creationDate = (isSerializableSessionData(initialData) && initialData.creationDate) || Date.now(); - - this._initialRequesterAvatarIconUri = initialData?.requesterAvatarIconUri && URI.revive(initialData.requesterAvatarIconUri); - this._initialResponderAvatarIconUri = isUriComponents(initialData?.responderAvatarIconUri) ? URI.revive(initialData.responderAvatarIconUri) : initialData?.responderAvatarIconUri; - } - - private _deserialize(obj: IExportableChatData): ChatRequestModel[] { - const requests = obj.requests; - if (!Array.isArray(requests)) { - this.logService.error(`Ignoring malformed session data: ${JSON.stringify(obj)}`); - return []; - } - - if (obj.welcomeMessage) { - const content = obj.welcomeMessage.map(item => typeof item === 'string' ? new MarkdownString(item) : item); - this._welcomeMessage = this.instantiationService.createInstance(ChatWelcomeMessageModel, content, []); - } - - try { - return requests.map((raw: ISerializableChatRequestData) => { - const parsedRequest = - typeof raw.message === 'string' - ? this.getParsedRequestFromString(raw.message) - : reviveParsedChatRequest(raw.message); - - // Old messages don't have variableData, or have it in the wrong (non-array) shape - const variableData: IChatRequestVariableData = this.reviveVariableData(raw.variableData); - const request = new ChatRequestModel(this, parsedRequest, variableData); - if (raw.response || raw.result || (raw as any).responseErrorDetails) { - const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format - reviveSerializedAgent(raw.agent) : undefined; - - // Port entries from old format - const result = 'responseErrorDetails' in raw ? - { errorDetails: raw.responseErrorDetails } as IAideChatAgentResult : raw.result; - request.response = new ChatResponseModel(raw.response ?? [new MarkdownString(raw.response)], this, agent, raw.slashCommand, request.id, true, raw.isCanceled, raw.vote, result, raw.followups); - if (raw.usedContext) { // @ulugbekna: if this's a new vscode sessions, doc versions are incorrect anyway? - request.response.applyReference(revive(raw.usedContext)); - } - - if (raw.contentReferences) { - raw.contentReferences.forEach(r => request.response!.applyReference(revive(r))); - } - } - return request; - }); - } catch (error) { - this.logService.error('Failed to parse chat data', error); - return []; - } - } - - private reviveVariableData(raw: IChatRequestVariableData): IChatRequestVariableData { - const variableData = raw && Array.isArray(raw.variables) - ? raw : - { variables: [] }; - - variableData.variables = variableData.variables.map((v): IAideChatRequestVariableEntry => { - // Old variables format - if (v && 'values' in v && Array.isArray(v.values)) { - return { - id: v.id ?? '', - name: v.name, - value: v.values[0]?.value, - range: v.range, - modelDescription: v.modelDescription, - references: v.references - }; - } else { - return v; - } - }); - - return variableData; - } - - private getParsedRequestFromString(message: string): IParsedChatRequest { - // TODO These offsets won't be used, but chat replies need to go through the parser as well - const parts = [new ChatRequestTextPart(new OffsetRange(0, message.length), { startColumn: 1, startLineNumber: 1, endColumn: 1, endLineNumber: 1 }, message)]; - return { - text: message, - parts - }; - } - - startInitialize(): void { - if (this.initState !== ChatModelInitState.Created) { - throw new Error(`ChatModel is in the wrong state for startInitialize: ${ChatModelInitState[this.initState]}`); - } - this._initState = ChatModelInitState.Initializing; - } - - deinitialize(): void { - this._initState = ChatModelInitState.Created; - this._isInitializedDeferred = new DeferredPromise(); - } - - initialize(welcomeMessage: ChatWelcomeMessageModel | undefined): void { - if (this.initState !== ChatModelInitState.Initializing) { - // Must call startInitialize before initialize, and only call it once - throw new Error(`ChatModel is in the wrong state for initialize: ${ChatModelInitState[this.initState]}`); - } - - this._initState = ChatModelInitState.Initialized; - if (!this._welcomeMessage) { - // Could also have loaded the welcome message from persisted data - this._welcomeMessage = welcomeMessage; - } - - this._isInitializedDeferred.complete(); - this._onDidChange.fire({ kind: 'initialize' }); - } - - setInitializationError(error: Error): void { - if (this.initState !== ChatModelInitState.Initializing) { - throw new Error(`ChatModel is in the wrong state for setInitializationError: ${ChatModelInitState[this.initState]}`); - } - - if (!this._isInitializedDeferred.isSettled) { - this._isInitializedDeferred.error(error); - } - } - - waitForInitialization(): Promise { - return this._isInitializedDeferred.p; - } - - getRequests(): ChatRequestModel[] { - return this._requests; - } - - addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, attempt: number, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand): ChatRequestModel { - const request = new ChatRequestModel(this, message, variableData, attempt); - request.response = new ChatResponseModel([], this, chatAgent, slashCommand, request.id); - - this._requests.push(request); - this._onDidChange.fire({ kind: 'addRequest', request }); - return request; - } - - adoptRequest(request: ChatRequestModel): void { - - // this doesn't use `removeRequest` because it must not dispose the request object - const oldOwner = request.session; - const index = oldOwner._requests.findIndex(candidate => candidate.id === request.id); - - if (index === -1) { - return; - } - - oldOwner._requests.splice(index, 1); - - request.adoptTo(this); - request.response?.adoptTo(this); - this._requests.push(request); - - oldOwner._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id, reason: ChatRequestRemovalReason.Adoption }); - this._onDidChange.fire({ kind: 'addRequest', request }); - } - - acceptResponseProgress(request: ChatRequestModel, progress: IAideChatProgress, quiet?: boolean): void { - if (!request.response) { - request.response = new ChatResponseModel([], this, undefined, undefined, request.id); - } - - if (request.response.isComplete) { - throw new Error('acceptResponseProgress: Adding progress to a completed response'); - } - - if (progress.kind === 'markdownContent' || - progress.kind === 'treeData' || - progress.kind === 'inlineReference' || - progress.kind === 'markdownVuln' || - progress.kind === 'progressMessage' || - progress.kind === 'command' || - progress.kind === 'textEdit' || - progress.kind === 'warning' || - progress.kind === 'progressTask' || - progress.kind === 'confirmation' - ) { - request.response.updateContent(progress, quiet); - } else if (progress.kind === 'usedContext' || progress.kind === 'reference') { - request.response.applyReference(progress); - } else if (progress.kind === 'agentDetection') { - const agent = this.chatAgentService.getAgent(progress.agentId); - if (agent) { - request.response.setAgent(agent, progress.command); - } - } else { - this.logService.error(`Couldn't handle progress: ${JSON.stringify(progress)}`); - } - } - - removeRequest(id: string, reason: ChatRequestRemovalReason = ChatRequestRemovalReason.Removal): void { - const index = this._requests.findIndex(request => request.id === id); - const request = this._requests[index]; - - if (index !== -1) { - this._onDidChange.fire({ kind: 'removeRequest', requestId: request.id, responseId: request.response?.id, reason }); - this._requests.splice(index, 1); - request.response?.dispose(); - } - } - - cancelRequest(request: ChatRequestModel): void { - if (request.response) { - request.response.cancel(); - } - } - - setResponse(request: ChatRequestModel, result: IAideChatAgentResult): void { - if (!request.response) { - request.response = new ChatResponseModel([], this, undefined, undefined, request.id); - } - - request.response.setResult(result); - } - - completeResponse(request: ChatRequestModel): void { - if (!request.response) { - throw new Error('Call setResponse before completeResponse'); - } - - request.response.complete(); - } - - setFollowups(request: ChatRequestModel, followups: IAideChatFollowup[] | undefined): void { - if (!request.response) { - // Maybe something went wrong? - return; - } - - request.response.setFollowups(followups); - } - - setResponseModel(request: ChatRequestModel, response: ChatResponseModel): void { - request.response = response; - this._onDidChange.fire({ kind: 'addResponse', response }); - } - - toExport(): IExportableChatData { - return { - requesterUsername: this.requesterUsername, - requesterAvatarIconUri: this.requesterAvatarIconUri, - responderUsername: this.responderUsername, - responderAvatarIconUri: this.responderAvatarIcon, - initialLocation: this.initialLocation, - welcomeMessage: this._welcomeMessage?.content.map(c => { - if (Array.isArray(c)) { - return c; - } else { - return c.value; - } - }), - requests: this._requests.map((r): ISerializableChatRequestData => { - const message = { - ...r.message, - parts: r.message.parts.map(p => p && 'toJSON' in p ? (p.toJSON as Function)() : p) - }; - return { - message, - variableData: r.variableData, - response: r.response ? - r.response.response.value.map(item => { - // Keeping the shape of the persisted data the same for back compat - if (item.kind === 'treeData') { - return item.treeData; - } else if (item.kind === 'markdownContent') { - return item.content; - } else { - return item as any; // TODO - } - }) - : undefined, - result: r.response?.result, - followups: r.response?.followups, - isCanceled: r.response?.isCanceled, - vote: r.response?.vote, - agent: r.response?.agent ? { ...r.response.agent } : undefined, - slashCommand: r.response?.slashCommand, - usedContext: r.response?.usedContext, - contentReferences: r.response?.contentReferences - }; - }), - }; - } - - toJSON(): ISerializableChatData { - return { - ...this.toExport(), - sessionId: this.sessionId, - creationDate: this._creationDate, - isImported: this._isImported - }; - } - - override dispose() { - this._requests.forEach(r => r.response?.dispose()); - this._onDidDispose.fire(); - - super.dispose(); - } -} - -export type IChatWelcomeMessageContent = IMarkdownString | IAideChatFollowup[]; - -export interface IChatWelcomeMessageModel { - readonly id: string; - readonly content: IChatWelcomeMessageContent[]; - readonly sampleQuestions: IAideChatFollowup[]; - readonly username: string; - readonly avatarIcon?: ThemeIcon | URI; - -} - -export class ChatWelcomeMessageModel implements IChatWelcomeMessageModel { - private static nextId = 0; - - private _id: string; - public get id(): string { - return this._id; - } - - constructor( - public readonly content: IChatWelcomeMessageContent[], - public readonly sampleQuestions: IAideChatFollowup[], - @IAideChatAgentService private readonly chatAgentService: IAideChatAgentService, - ) { - this._id = 'welcome_' + ChatWelcomeMessageModel.nextId++; - } - - public get username(): string { - return this.chatAgentService.getContributedDefaultAgent(AideChatAgentLocation.Panel)?.fullName ?? ''; - } - - public get avatarIcon(): ThemeIcon | URI | undefined { - return this.chatAgentService.getDefaultAgent(AideChatAgentLocation.Panel)?.metadata.themeIcon; - } -} - -export function getHistoryEntriesFromModel(model: IChatModel, forAgentId: string | undefined): IChatAgentHistoryEntry[] { - const history: IChatAgentHistoryEntry[] = []; - for (const request of model.getRequests()) { - if (!request.response) { - continue; - } - - if (forAgentId && forAgentId !== request.response.agent?.id) { - // An agent only gets to see requests that were sent to this agent. - // The default agent (the undefined case) gets to see all of them. - continue; - } - - const promptTextResult = getPromptText(request.message); - const historyRequest: IAideChatAgentRequest = { - sessionId: model.sessionId, - requestId: request.id, - agentId: request.response.agent?.id ?? '', - message: promptTextResult.message, - command: request.response.slashCommand?.name, - variables: updateRanges(request.variableData, promptTextResult.diff), // TODO bit of a hack - location: AideChatAgentLocation.Panel - }; - history.push({ request: historyRequest, response: request.response.response.value, result: request.response.result ?? {} }); - } - - return history; -} - -export function updateRanges(variableData: IChatRequestVariableData, diff: number): IChatRequestVariableData { - return { - variables: variableData.variables.map(v => ({ - ...v, - range: v.range && { - start: v.range.start - diff, - endExclusive: v.range.endExclusive - diff - } - })) - }; -} - -export function canMergeMarkdownStrings(md1: IMarkdownString, md2: IMarkdownString): boolean { - if (md1.baseUri && md2.baseUri) { - const baseUriEquals = md1.baseUri.scheme === md2.baseUri.scheme - && md1.baseUri.authority === md2.baseUri.authority - && md1.baseUri.path === md2.baseUri.path - && md1.baseUri.query === md2.baseUri.query - && md1.baseUri.fragment === md2.baseUri.fragment; - if (!baseUriEquals) { - return false; - } - } else if (md1.baseUri || md2.baseUri) { - return false; - } - - return equals(md1.isTrusted, md2.isTrusted) && - md1.supportHtml === md2.supportHtml && - md1.supportThemeIcons === md2.supportThemeIcons; -} - -export function appendMarkdownString(md1: IMarkdownString, md2: IMarkdownString | string): IMarkdownString { - const appendedValue = typeof md2 === 'string' ? md2 : md2.value; - return { - value: md1.value + appendedValue, - isTrusted: md1.isTrusted, - supportThemeIcons: md1.supportThemeIcons, - supportHtml: md1.supportHtml, - baseUri: md1.baseUri - }; -} diff --git a/src/vs/workbench/contrib/aideChat/common/aideChatParserTypes.ts b/src/vs/workbench/contrib/aideChat/common/aideChatParserTypes.ts deleted file mode 100644 index bb20a4a962b..00000000000 --- a/src/vs/workbench/contrib/aideChat/common/aideChatParserTypes.ts +++ /dev/null @@ -1,196 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { revive } from '../../../../base/common/marshalling.js'; -import { IOffsetRange, OffsetRange } from '../../../../editor/common/core/offsetRange.js'; -import { IRange } from '../../../../editor/common/core/range.js'; -import { IChatAgentCommand, IChatAgentData, reviveSerializedAgent } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { IChatSlashData } from '../../../../workbench/contrib/aideChat/common/aideChatSlashCommands.js'; -import { IAideChatRequestVariableValue } from '../../../../workbench/contrib/aideChat/common/aideChatVariables.js'; - -// These are in a separate file to avoid circular dependencies with the dependencies of the parser - -export interface IParsedChatRequest { - readonly parts: ReadonlyArray; - readonly text: string; -} - -export interface IParsedChatRequestPart { - readonly kind: string; // for serialization - readonly range: IOffsetRange; - readonly editorRange: IRange; - readonly text: string; - /** How this part is represented in the prompt going to the agent */ - readonly promptText: string; -} - -export function getPromptText(request: IParsedChatRequest): { message: string; diff: number } { - const message = request.parts.map(r => r.promptText).join('').trimStart(); - const diff = request.text.length - message.length; - - return { message, diff }; -} - -export class ChatRequestTextPart implements IParsedChatRequestPart { - static readonly Kind = 'text'; - readonly kind = ChatRequestTextPart.Kind; - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly text: string) { } - - get promptText(): string { - return this.text; - } -} - -// warning, these also show up in a regex in the parser -export const chatVariableLeader = '#'; -export const chatAgentLeader = '@'; -export const chatSubcommandLeader = '/'; - -/** - * An invocation of a static variable that can be resolved by the variable service - */ -export class ChatRequestVariablePart implements IParsedChatRequestPart { - static readonly Kind = 'var'; - readonly kind = ChatRequestVariablePart.Kind; - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly variableName: string, readonly variableArg: string, readonly variableId: string) { } - - get text(): string { - const argPart = this.variableArg ? `:${this.variableArg}` : ''; - return `${chatVariableLeader}${this.variableName}${argPart}`; - } - - get promptText(): string { - return this.text; - } -} - -/** - * An invocation of an agent that can be resolved by the agent service - */ -export class ChatRequestAgentPart implements IParsedChatRequestPart { - static readonly Kind = 'agent'; - readonly kind = ChatRequestAgentPart.Kind; - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly agent: IChatAgentData) { } - - get text(): string { - return `${chatAgentLeader}${this.agent.name}`; - } - - get promptText(): string { - return ''; - } -} - -/** - * An invocation of an agent's subcommand - */ -export class ChatRequestAgentSubcommandPart implements IParsedChatRequestPart { - static readonly Kind = 'subcommand'; - readonly kind = ChatRequestAgentSubcommandPart.Kind; - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly command: IChatAgentCommand) { } - - get text(): string { - return `${chatSubcommandLeader}${this.command.name}`; - } - - get promptText(): string { - return ''; - } -} - -/** - * An invocation of a standalone slash command - */ -export class ChatRequestSlashCommandPart implements IParsedChatRequestPart { - static readonly Kind = 'slash'; - readonly kind = ChatRequestSlashCommandPart.Kind; - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly slashCommand: IChatSlashData) { } - - get text(): string { - return `${chatSubcommandLeader}${this.slashCommand.command}`; - } - - get promptText(): string { - return `${chatSubcommandLeader}${this.slashCommand.command}`; - } -} - -/** - * An invocation of a dynamic reference like '#file:' - */ -export class ChatRequestDynamicVariablePart implements IParsedChatRequestPart { - static readonly Kind = 'dynamic'; - readonly kind = ChatRequestDynamicVariablePart.Kind; - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly text: string, readonly id: string, readonly modelDescription: string | undefined, readonly data: IAideChatRequestVariableValue) { } - - get referenceText(): string { - return this.text.replace(chatVariableLeader, ''); - } - - get promptText(): string { - return this.text; - } -} - -export function reviveParsedChatRequest(serialized: IParsedChatRequest): IParsedChatRequest { - return { - text: serialized.text, - parts: serialized.parts.map(part => { - if (part.kind === ChatRequestTextPart.Kind) { - return new ChatRequestTextPart( - new OffsetRange(part.range.start, part.range.endExclusive), - part.editorRange, - part.text - ); - } else if (part.kind === ChatRequestVariablePart.Kind) { - return new ChatRequestVariablePart( - new OffsetRange(part.range.start, part.range.endExclusive), - part.editorRange, - (part as ChatRequestVariablePart).variableName, - (part as ChatRequestVariablePart).variableArg, - (part as ChatRequestVariablePart).variableName || '', - ); - } else if (part.kind === ChatRequestAgentPart.Kind) { - let agent = (part as ChatRequestAgentPart).agent; - agent = reviveSerializedAgent(agent); - - return new ChatRequestAgentPart( - new OffsetRange(part.range.start, part.range.endExclusive), - part.editorRange, - agent - ); - } else if (part.kind === ChatRequestAgentSubcommandPart.Kind) { - return new ChatRequestAgentSubcommandPart( - new OffsetRange(part.range.start, part.range.endExclusive), - part.editorRange, - (part as ChatRequestAgentSubcommandPart).command - ); - } else if (part.kind === ChatRequestSlashCommandPart.Kind) { - return new ChatRequestSlashCommandPart( - new OffsetRange(part.range.start, part.range.endExclusive), - part.editorRange, - (part as ChatRequestSlashCommandPart).slashCommand - ); - } else if (part.kind === ChatRequestDynamicVariablePart.Kind) { - return new ChatRequestDynamicVariablePart( - new OffsetRange(part.range.start, part.range.endExclusive), - part.editorRange, - (part as ChatRequestDynamicVariablePart).text, - (part as ChatRequestDynamicVariablePart).id, - (part as ChatRequestDynamicVariablePart).modelDescription, - revive((part as ChatRequestDynamicVariablePart).data) - ); - } else { - throw new Error(`Unknown chat request part: ${part.kind}`); - } - }) - }; -} - -export function extractAgentAndCommand(parsed: IParsedChatRequest): { agentPart: ChatRequestAgentPart | undefined; commandPart: ChatRequestAgentSubcommandPart | undefined } { - const agentPart = parsed.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart); - const commandPart = parsed.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart); - return { agentPart, commandPart }; -} diff --git a/src/vs/workbench/contrib/aideChat/common/aideChatParticipantContribTypes.ts b/src/vs/workbench/contrib/aideChat/common/aideChatParticipantContribTypes.ts deleted file mode 100644 index 638dce2633c..00000000000 --- a/src/vs/workbench/contrib/aideChat/common/aideChatParticipantContribTypes.ts +++ /dev/null @@ -1,36 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export interface IRawChatCommandContribution { - name: string; - description: string; - sampleRequest?: string; - isSticky?: boolean; - when?: string; - defaultImplicitVariables?: string[]; -} - -export type RawChatParticipantLocation = 'panel' | 'terminal' | 'notebook'; - -export interface IRawChatParticipantContribution { - id: string; - name: string; - fullName: string; - when?: string; - description?: string; - isDefault?: boolean; - isSticky?: boolean; - sampleRequest?: string; - commands?: IRawChatCommandContribution[]; - defaultImplicitVariables?: string[]; - locations?: RawChatParticipantLocation[]; -} - -/** - * Hardcoding the previous id of the Copilot Chat provider to avoid breaking view locations, persisted data, etc. - * DON'T use this for any new data, only for old persisted data. - * @deprecated - */ -export const CHAT_PROVIDER_ID = 'copilot'; diff --git a/src/vs/workbench/contrib/aideChat/common/aideChatRequestParser.ts b/src/vs/workbench/contrib/aideChat/common/aideChatRequestParser.ts deleted file mode 100644 index 9518fdc1cf2..00000000000 --- a/src/vs/workbench/contrib/aideChat/common/aideChatRequestParser.ts +++ /dev/null @@ -1,221 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { OffsetRange } from '../../../../editor/common/core/offsetRange.js'; -import { IPosition, Position } from '../../../../editor/common/core/position.js'; -import { Range } from '../../../../editor/common/core/range.js'; -import { AideChatAgentLocation, IChatAgentData, IAideChatAgentService } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestDynamicVariablePart, ChatRequestSlashCommandPart, ChatRequestTextPart, ChatRequestVariablePart, IParsedChatRequest, IParsedChatRequestPart, chatAgentLeader, chatSubcommandLeader, chatVariableLeader } from '../../../../workbench/contrib/aideChat/common/aideChatParserTypes.js'; -import { IAideChatSlashCommandService } from '../../../../workbench/contrib/aideChat/common/aideChatSlashCommands.js'; -import { IAideChatVariablesService, IDynamicVariable } from '../../../../workbench/contrib/aideChat/common/aideChatVariables.js'; - -const agentReg = /^@([\w_\-\.]+)(?=(\s|$|\b))/i; // An @-agent -const variableReg = /^#([\w_\-]+)(:\d+)?(?=(\s|$|\b))/i; // A #-variable with an optional numeric : arg (@response:2) -const slashReg = /\/([\w_\-]+)(?=(\s|$|\b))/i; // A / command - -export interface IChatParserContext { - /** Used only as a disambiguator, when the query references an agent that has a duplicate with the same name. */ - selectedAgent?: IChatAgentData; -} - -export class ChatRequestParser { - constructor( - @IAideChatAgentService private readonly agentService: IAideChatAgentService, - @IAideChatVariablesService private readonly variableService: IAideChatVariablesService, - @IAideChatSlashCommandService private readonly slashCommandService: IAideChatSlashCommandService - ) { } - - parseChatRequest(sessionId: string, message: string, location: AideChatAgentLocation = AideChatAgentLocation.Panel, context?: IChatParserContext): IParsedChatRequest { - const parts: IParsedChatRequestPart[] = []; - const references = this.variableService.getDynamicVariables(sessionId); // must access this list before any async calls - - let lineNumber = 1; - let column = 1; - for (let i = 0; i < message.length; i++) { - const previousChar = message.charAt(i - 1); - const char = message.charAt(i); - let newPart: IParsedChatRequestPart | undefined; - if (previousChar.match(/\s/) || i === 0) { - if (char === chatVariableLeader) { - newPart = this.tryToParseVariable(message.slice(i), i, new Position(lineNumber, column), parts); - } else if (char === chatAgentLeader) { - newPart = this.tryToParseAgent(message.slice(i), message, i, new Position(lineNumber, column), parts, location, context); - } else if (char === chatSubcommandLeader) { - newPart = this.tryToParseSlashCommand(message.slice(i), message, i, new Position(lineNumber, column), parts); - } - - if (!newPart) { - newPart = this.tryToParseDynamicVariable(message.slice(i), i, new Position(lineNumber, column), references); - } - } - - if (newPart) { - if (i !== 0) { - // Insert a part for all the text we passed over, then insert the new parsed part - const previousPart = parts.at(-1); - const previousPartEnd = previousPart?.range.endExclusive ?? 0; - const previousPartEditorRangeEndLine = previousPart?.editorRange.endLineNumber ?? 1; - const previousPartEditorRangeEndCol = previousPart?.editorRange.endColumn ?? 1; - parts.push(new ChatRequestTextPart( - new OffsetRange(previousPartEnd, i), - new Range(previousPartEditorRangeEndLine, previousPartEditorRangeEndCol, lineNumber, column), - message.slice(previousPartEnd, i))); - } - - parts.push(newPart); - } - - if (char === '\n') { - lineNumber++; - column = 1; - } else { - column++; - } - } - - const lastPart = parts.at(-1); - const lastPartEnd = lastPart?.range.endExclusive ?? 0; - if (lastPartEnd < message.length) { - parts.push(new ChatRequestTextPart( - new OffsetRange(lastPartEnd, message.length), - new Range(lastPart?.editorRange.endLineNumber ?? 1, lastPart?.editorRange.endColumn ?? 1, lineNumber, column), - message.slice(lastPartEnd, message.length))); - } - - return { - parts, - text: message, - }; - } - - private tryToParseAgent(message: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray, location: AideChatAgentLocation, context: IChatParserContext | undefined): ChatRequestAgentPart | ChatRequestVariablePart | undefined { - const nextAgentMatch = message.match(agentReg); - if (!nextAgentMatch) { - return; - } - - const [full, name] = nextAgentMatch; - const agentRange = new OffsetRange(offset, offset + full.length); - const agentEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); - - let agents = this.agentService.getAgentsByName(name); - if (!agents.length) { - const fqAgent = this.agentService.getAgentByFullyQualifiedId(name); - if (fqAgent) { - agents = [fqAgent]; - } - } - - // If there is more than one agent with this name, and the user picked it from the suggest widget, then the selected agent should be in the - // context and we use that one. Otherwise just pick the first. - const agent = agents.length > 1 && context?.selectedAgent ? - context.selectedAgent : - agents[0]; - if (!agent || !agent.locations.includes(location)) { - return; - } - - if (parts.some(p => p instanceof ChatRequestAgentPart)) { - // Only one agent allowed - return; - } - - // The agent must come first - if (parts.some(p => (p instanceof ChatRequestTextPart && p.text.trim() !== '') || !(p instanceof ChatRequestAgentPart))) { - return; - } - - const previousPart = parts.at(-1); - const previousPartEnd = previousPart?.range.endExclusive ?? 0; - const textSincePreviousPart = fullMessage.slice(previousPartEnd, offset); - if (textSincePreviousPart.trim() !== '') { - return; - } - - return new ChatRequestAgentPart(agentRange, agentEditorRange, agent); - } - - private tryToParseVariable(message: string, offset: number, position: IPosition, parts: ReadonlyArray): ChatRequestAgentPart | ChatRequestVariablePart | undefined { - const nextVariableMatch = message.match(variableReg); - if (!nextVariableMatch) { - return; - } - - const [full, name] = nextVariableMatch; - const variableArg = nextVariableMatch[2] ?? ''; - const varRange = new OffsetRange(offset, offset + full.length); - const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); - const usedAgent = parts.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); - const allowSlow = !usedAgent || usedAgent.agent.metadata.supportsSlowVariables; - - // TODO - not really handling duplicate variables names yet - const variable = this.variableService.getVariable(name); - if (variable && (!variable.isSlow || allowSlow)) { - return new ChatRequestVariablePart(varRange, varEditorRange, name, variableArg, variable.id); - } - - return; - } - - private tryToParseSlashCommand(remainingMessage: string, fullMessage: string, offset: number, position: IPosition, parts: ReadonlyArray): ChatRequestSlashCommandPart | ChatRequestAgentSubcommandPart | undefined { - const nextSlashMatch = remainingMessage.match(slashReg); - if (!nextSlashMatch) { - return; - } - - if (parts.some(p => p instanceof ChatRequestSlashCommandPart)) { - // Only one slash command allowed - return; - } - - const [full, command] = nextSlashMatch; - const slashRange = new OffsetRange(offset, offset + full.length); - const slashEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); - - const usedAgent = parts.find((p): p is ChatRequestAgentPart => p instanceof ChatRequestAgentPart); - if (usedAgent) { - // The slash command must come immediately after the agent - if (parts.some(p => (p instanceof ChatRequestTextPart && p.text.trim() !== '') || !(p instanceof ChatRequestAgentPart) && !(p instanceof ChatRequestTextPart))) { - return; - } - - const previousPart = parts.at(-1); - const previousPartEnd = previousPart?.range.endExclusive ?? 0; - const textSincePreviousPart = fullMessage.slice(previousPartEnd, offset); - if (textSincePreviousPart.trim() !== '') { - return; - } - - const subCommand = usedAgent.agent.slashCommands.find(c => c.name === command); - if (subCommand) { - // Valid agent subcommand - return new ChatRequestAgentSubcommandPart(slashRange, slashEditorRange, subCommand); - } - } else { - const slashCommands = this.slashCommandService.getCommands(); - const slashCommand = slashCommands.find(c => c.command === command); - if (slashCommand) { - // Valid standalone slash command - return new ChatRequestSlashCommandPart(slashRange, slashEditorRange, slashCommand); - } - } - - return; - } - - private tryToParseDynamicVariable(message: string, offset: number, position: IPosition, references: ReadonlyArray): ChatRequestDynamicVariablePart | undefined { - const refAtThisPosition = references.find(r => - r.range.startLineNumber === position.lineNumber && - r.range.startColumn === position.column); - if (refAtThisPosition) { - const length = refAtThisPosition.range.endColumn - refAtThisPosition.range.startColumn; - const text = message.substring(0, length); - const range = new OffsetRange(offset, offset + length); - return new ChatRequestDynamicVariablePart(range, refAtThisPosition.range, text, refAtThisPosition.id, refAtThisPosition.modelDescription, refAtThisPosition.data); - } - - return; - } -} diff --git a/src/vs/workbench/contrib/aideChat/common/aideChatService.ts b/src/vs/workbench/contrib/aideChat/common/aideChatService.ts deleted file mode 100644 index 57e35250a8f..00000000000 --- a/src/vs/workbench/contrib/aideChat/common/aideChatService.ts +++ /dev/null @@ -1,362 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { DeferredPromise } from '../../../../base/common/async.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Event } from '../../../../base/common/event.js'; -import { IMarkdownString } from '../../../../base/common/htmlContent.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { URI } from '../../../../base/common/uri.js'; -import { IRange, Range } from '../../../../editor/common/core/range.js'; -import { Command, Location, TextEdit } from '../../../../editor/common/languages.js'; -import { FileType } from '../../../../platform/files/common/files.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { AideChatAgentLocation, IAideChatAgentResult, IChatAgentCommand, IChatAgentData } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { ChatModel, IAideChatRequestVariableEntry, IChatModel, IChatRequestModel, IChatRequestVariableData, IChatResponseModel, IExportableChatData, ISerializableChatData } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { IParsedChatRequest } from '../../../../workbench/contrib/aideChat/common/aideChatParserTypes.js'; -import { IChatParserContext } from '../../../../workbench/contrib/aideChat/common/aideChatRequestParser.js'; -import { IAideChatRequestVariableValue } from '../../../../workbench/contrib/aideChat/common/aideChatVariables.js'; -import { IChatCodeCitation, IChatMoveMessage, IChatResponseCodeblockUriPart } from '../../chat/common/chatService.js'; -import { IWorkspaceSymbol } from '../../search/common/search.js'; - -export interface IChatRequest { - message: string; - variables: Record; -} - -export interface IAideChatResponseErrorDetails { - message: string; - responseIsIncomplete?: boolean; - responseIsFiltered?: boolean; - responseIsRedacted?: boolean; -} - -export interface IChatResponseProgressFileTreeData { - label: string; - uri: URI; - type?: FileType; - children?: IChatResponseProgressFileTreeData[]; -} - -export type IDocumentContext = { - uri: URI; - version: number; - ranges: IRange[]; -}; - -export function isIDocumentContext(obj: unknown): obj is IDocumentContext { - return ( - !!obj && - typeof obj === 'object' && - 'uri' in obj && obj.uri instanceof URI && - 'version' in obj && typeof obj.version === 'number' && - 'ranges' in obj && Array.isArray(obj.ranges) && obj.ranges.every(Range.isIRange) - ); -} - -export interface IChatUsedContext { - documents: IDocumentContext[]; - kind: 'usedContext'; -} - -export function isIUsedContext(obj: unknown): obj is IChatUsedContext { - return ( - !!obj && - typeof obj === 'object' && - 'documents' in obj && - Array.isArray(obj.documents) && - obj.documents.every(isIDocumentContext) - ); -} - -export interface IChatContentVariableReference { - variableName: string; - value?: URI | Location; -} - -export enum ChatResponseReferencePartStatusKind { - Complete = 1, - Partial = 2, - Omitted = 3 -} - -export interface IAideChatContentReference { - reference: URI | Location | IChatContentVariableReference | string; - iconPath?: ThemeIcon | { light: URI; dark?: URI }; - options?: { status?: { description: string; kind: ChatResponseReferencePartStatusKind } }; - kind: 'reference'; -} - -export interface IAideChatContentInlineReference { - inlineReference: URI | Location | IWorkspaceSymbol; - name?: string; - kind: 'inlineReference'; -} - -export interface IAideChatAgentDetection { - agentId: string; - command?: IChatAgentCommand; - kind: 'agentDetection'; -} - -export interface IAideChatMarkdownContent { - content: IMarkdownString; - kind: 'markdownContent'; -} - -export interface IChatTreeData { - treeData: IChatResponseProgressFileTreeData; - kind: 'treeData'; -} - -export interface IAideChatProgressMessage { - content: IMarkdownString; - kind: 'progressMessage'; -} - -export interface IAideChatTask extends IAideChatTaskDto { - deferred: DeferredPromise; - progress: (IAideChatWarningMessage | IAideChatContentReference)[]; - onDidAddProgress: Event; - add(progress: IAideChatWarningMessage | IAideChatContentReference): void; - - complete: (result: string | void) => void; - task: () => Promise; - isSettled: () => boolean; -} - -export interface IAideChatTaskDto { - content: IMarkdownString; - kind: 'progressTask'; -} - -export interface IAideChatTaskResult { - content: IMarkdownString | void; - kind: 'progressTaskResult'; -} - -export interface IAideChatWarningMessage { - content: IMarkdownString; - kind: 'warning'; -} - -export interface IChatAgentVulnerabilityDetails { - title: string; - description: string; -} - -export interface IAideChatAgentMarkdownContentWithVulnerability { - content: IMarkdownString; - vulnerabilities: IChatAgentVulnerabilityDetails[]; - kind: 'markdownVuln'; -} - -export interface IAideChatCommandButton { - command: Command; - kind: 'command'; -} - -export interface IAideChatTextEdit { - uri: URI; - edits: TextEdit[]; - kind: 'textEdit'; -} - -export interface IAideChatConfirmation { - title: string; - message: string; - data: any; - isUsed?: boolean; - kind: 'confirmation'; -} - -export type IAideChatProgress = - | IAideChatMarkdownContent - | IAideChatAgentMarkdownContentWithVulnerability - | IChatTreeData - | IChatUsedContext - | IAideChatContentReference - | IAideChatContentInlineReference - | IChatCodeCitation - | IAideChatAgentDetection - | IAideChatProgressMessage - | IAideChatTask - | IAideChatTaskResult - | IAideChatCommandButton - | IAideChatWarningMessage - | IAideChatTextEdit - | IChatMoveMessage - | IChatResponseCodeblockUriPart - | IAideChatConfirmation; - -export interface IAideChatFollowup { - kind: 'reply'; - message: string; - agentId: string; - subCommand?: string; - title?: string; - tooltip?: string; -} - -export enum AideChatAgentVoteDirection { - Down = 0, - Up = 1 -} - -export interface IChatVoteAction { - kind: 'vote'; - direction: AideChatAgentVoteDirection; - reportIssue?: boolean; -} - -export enum ChatCopyKind { - // Keyboard shortcut or context menu - Action = 1, - Toolbar = 2 -} - -export interface IChatCopyAction { - kind: 'copy'; - codeBlockIndex: number; - copyKind: ChatCopyKind; - copiedCharacters: number; - totalCharacters: number; - copiedText: string; -} - -export interface IChatInsertAction { - kind: 'insert'; - codeBlockIndex: number; - totalCharacters: number; - newFile?: boolean; -} - -export interface IChatTerminalAction { - kind: 'runInTerminal'; - codeBlockIndex: number; - languageId?: string; -} - -export interface IChatCommandAction { - kind: 'command'; - commandButton: IAideChatCommandButton; -} - -export interface IAideChatFollowupAction { - kind: 'followUp'; - followup: IAideChatFollowup; -} - -export interface IChatBugReportAction { - kind: 'bug'; -} - -export interface IChatInlineChatCodeAction { - kind: 'inlineChat'; - action: 'accepted' | 'discarded'; -} - -export type ChatUserAction = IChatVoteAction | IChatCopyAction | IChatInsertAction | IChatTerminalAction | IChatCommandAction | IAideChatFollowupAction | IChatBugReportAction | IChatInlineChatCodeAction; - -export interface IAideChatUserActionEvent { - action: ChatUserAction; - agentId: string | undefined; - sessionId: string; - requestId: string; - result: IAideChatAgentResult | undefined; -} - -export interface IChatDynamicRequest { - /** - * The message that will be displayed in the UI - */ - message: string; - - /** - * Any extra metadata/context that will go to the provider. - */ - metadata?: any; -} - -export interface IChatCompleteResponse { - message: string | ReadonlyArray; - result?: IAideChatAgentResult; - followups?: IAideChatFollowup[]; -} - -export interface IChatDetail { - sessionId: string; - title: string; -} - -export interface IChatProviderInfo { - id: string; -} - -export interface IChatTransferredSessionData { - sessionId: string; - inputValue: string; -} - -export interface IChatSendRequestResponseState { - responseCreatedPromise: Promise; - responseCompletePromise: Promise; -} - -export interface IChatSendRequestData extends IChatSendRequestResponseState { - agent: IChatAgentData; - slashCommand?: IChatAgentCommand; -} - -export interface IChatSendRequestOptions { - location?: AideChatAgentLocation; - parserContext?: IChatParserContext; - attempt?: number; - noCommandDetection?: boolean; - acceptedConfirmationData?: any[]; - rejectedConfirmationData?: any[]; - attachedContext?: IAideChatRequestVariableEntry[]; - - /** The target agent ID can be specified with this property instead of using @ in 'message' */ - agentId?: string; - slashCommand?: string; -} - -export const IAideChatService = createDecorator('IAideChatService'); - -export interface IAideChatService { - _serviceBrand: undefined; - transferredSessionData: IChatTransferredSessionData | undefined; - - isEnabled(location: AideChatAgentLocation): boolean; - hasSessions(): boolean; - startSession(location: AideChatAgentLocation, token: CancellationToken): ChatModel | undefined; - getSession(sessionId: string): IChatModel | undefined; - getOrRestoreSession(sessionId: string): IChatModel | undefined; - loadSessionFromContent(data: IExportableChatData | ISerializableChatData): IChatModel | undefined; - - /** - * Returns whether the request was accepted. - */ - sendRequest(sessionId: string, message: string, options?: IChatSendRequestOptions): Promise; - - resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions): Promise; - adoptRequest(sessionId: string, request: IChatRequestModel): Promise; - removeRequest(sessionid: string, requestId: string): Promise; - cancelCurrentRequestForSession(sessionId: string): void; - clearSession(sessionId: string): void; - addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, attempt: number | undefined, response: IChatCompleteResponse): void; - getHistory(): IChatDetail[]; - clearAllHistoryEntries(): void; - removeHistoryEntry(sessionId: string): void; - - onDidPerformUserAction: Event; - notifyUserAction(event: IAideChatUserActionEvent): void; - onDidDisposeSession: Event<{ sessionId: string; reason: 'initializationFailed' | 'cleared' }>; - - transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void; -} - -export const KEYWORD_ACTIVIATION_SETTING_ID = 'accessibility.voice.keywordActivation'; diff --git a/src/vs/workbench/contrib/aideChat/common/aideChatServiceImpl.ts b/src/vs/workbench/contrib/aideChat/common/aideChatServiceImpl.ts deleted file mode 100644 index 446cc4125a7..00000000000 --- a/src/vs/workbench/contrib/aideChat/common/aideChatServiceImpl.ts +++ /dev/null @@ -1,802 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { coalesce } from '../../../../base/common/arrays.js'; -import { DeferredPromise } from '../../../../base/common/async.js'; -import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { toErrorMessage } from '../../../../base/common/errorMessage.js'; -import { ErrorNoTelemetry } from '../../../../base/common/errors.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { Iterable } from '../../../../base/common/iterator.js'; -import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js'; -import { revive } from '../../../../base/common/marshalling.js'; -import { StopWatch } from '../../../../base/common/stopwatch.js'; -import { URI, UriComponents } from '../../../../base/common/uri.js'; -import { localize } from '../../../../nls.js'; -import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { Progress } from '../../../../platform/progress/common/progress.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; -import { AideChatAgentLocation, IChatAgent, IAideChatAgentRequest, IAideChatAgentResult, IAideChatAgentService } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { ChatModel, ChatRequestModel, ChatRequestRemovalReason, ChatWelcomeMessageModel, IChatModel, IChatRequestModel, IChatRequestVariableData, IAideChatRequestVariableEntry, IChatResponseModel, IExportableChatData, ISerializableChatData, ISerializableChatsData, getHistoryEntriesFromModel, updateRanges } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { ChatRequestAgentPart, ChatRequestAgentSubcommandPart, ChatRequestSlashCommandPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, getPromptText } from '../../../../workbench/contrib/aideChat/common/aideChatParserTypes.js'; -import { ChatRequestParser } from '../../../../workbench/contrib/aideChat/common/aideChatRequestParser.js'; -import { ChatCopyKind, IChatCompleteResponse, IChatDetail, IAideChatFollowup, IAideChatProgress, IChatSendRequestData, IChatSendRequestOptions, IChatSendRequestResponseState, IAideChatService, IChatTransferredSessionData, IAideChatUserActionEvent, AideChatAgentVoteDirection } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { IAideChatSlashCommandService } from '../../../../workbench/contrib/aideChat/common/aideChatSlashCommands.js'; -import { IAideChatVariablesService } from '../../../../workbench/contrib/aideChat/common/aideChatVariables.js'; -import { AideChatMessageRole, IAideChatMessage } from '../../../../workbench/contrib/aideChat/common/languageModels.js'; -import { IExtensionService } from '../../../../workbench/services/extensions/common/extensions.js'; - -const serializedChatKey = 'aideChat.sessions'; - -const globalChatKey = 'aideChat.workspaceTransfer'; -interface IChatTransfer { - toWorkspace: UriComponents; - timestampInMilliseconds: number; - chat: ISerializableChatData; - inputValue: string; -} -const SESSION_TRANSFER_EXPIRATION_IN_MILLISECONDS = 1000 * 60; - -type ChatProviderInvokedEvent = { - timeToFirstProgress: number | undefined; - totalTime: number | undefined; - result: 'success' | 'error' | 'errorWithOutput' | 'cancelled' | 'filtered'; - requestType: 'string' | 'followup' | 'slashCommand'; - chatSessionId: string; - agent: string; - slashCommand: string | undefined; - location: AideChatAgentLocation; -}; - -type ChatProviderInvokedClassification = { - timeToFirstProgress: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The time in milliseconds from invoking the provider to getting the first data.' }; - totalTime: { classification: 'SystemMetaData'; purpose: 'PerformanceAndHealth'; comment: 'The total time it took to run the provider\'s `provideResponseWithProgress`.' }; - result: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether invoking the ChatProvider resulted in an error.' }; - requestType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of request that the user made.' }; - chatSessionId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'A random ID for the session.' }; - agent: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of agent used.' }; - slashCommand?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The type of slashCommand used.' }; - location?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The location at which chat request was made.' }; - owner: 'roblourens'; - comment: 'Provides insight into the performance of Chat agents.'; -}; - -type ChatVoteEvent = { - direction: 'up' | 'down'; - agentId: string; -}; - -type ChatVoteClassification = { - direction: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the user voted up or down.' }; - agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat agent that this vote is for.' }; - owner: 'roblourens'; - comment: 'Provides insight into the performance of Chat agents.'; -}; - -type ChatCopyEvent = { - copyKind: 'action' | 'toolbar'; - agentId: string; -}; - -type ChatCopyClassification = { - copyKind: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How the copy was initiated.' }; - agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat agent that the copy acted on.' }; - owner: 'roblourens'; - comment: 'Provides insight into the usage of Chat features.'; -}; - -type ChatInsertEvent = { - newFile: boolean; - agentId: string; -}; - -type ChatInsertClassification = { - newFile: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the code was inserted into a new untitled file.' }; - agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the chat agent that this insertion is for.' }; - owner: 'roblourens'; - comment: 'Provides insight into the usage of Chat features.'; -}; - -type ChatCommandEvent = { - commandId: string; - agentId: string; -}; - -type ChatCommandClassification = { - commandId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The id of the command that was executed.' }; - agentId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The ID of the related chat agent.' }; - owner: 'roblourens'; - comment: 'Provides insight into the usage of Chat features.'; -}; - -type ChatTerminalEvent = { - languageId: string; -}; - -type ChatTerminalClassification = { - languageId: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The language of the code that was run in the terminal.' }; - owner: 'roblourens'; - comment: 'Provides insight into the usage of Chat features.'; -}; - -const maxPersistedSessions = 25; - -export class ChatService extends Disposable implements IAideChatService { - declare _serviceBrand: undefined; - - private readonly _sessionModels = this._register(new DisposableMap()); - private readonly _pendingRequests = this._register(new DisposableMap()); - private _persistedSessions: ISerializableChatsData; - - - private _transferredSessionData: IChatTransferredSessionData | undefined; - public get transferredSessionData(): IChatTransferredSessionData | undefined { - return this._transferredSessionData; - } - - private readonly _onDidPerformUserAction = this._register(new Emitter()); - public readonly onDidPerformUserAction: Event = this._onDidPerformUserAction.event; - - private readonly _onDidDisposeSession = this._register(new Emitter<{ sessionId: string; reason: 'initializationFailed' | 'cleared' }>()); - public readonly onDidDisposeSession = this._onDidDisposeSession.event; - - private readonly _sessionFollowupCancelTokens = this._register(new DisposableMap()); - - constructor( - @IStorageService private readonly storageService: IStorageService, - @ILogService private readonly logService: ILogService, - @IExtensionService private readonly extensionService: IExtensionService, - @IInstantiationService private readonly instantiationService: IInstantiationService, - @ITelemetryService private readonly telemetryService: ITelemetryService, - @IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService, - @IAideChatSlashCommandService private readonly chatSlashCommandService: IAideChatSlashCommandService, - @IAideChatVariablesService private readonly chatVariablesService: IAideChatVariablesService, - @IAideChatAgentService private readonly chatAgentService: IAideChatAgentService, - ) { - super(); - - const sessionData = storageService.get(serializedChatKey, StorageScope.WORKSPACE, ''); - if (sessionData) { - this._persistedSessions = this.deserializeChats(sessionData); - const countsForLog = Object.keys(this._persistedSessions).length; - if (countsForLog > 0) { - this.trace('constructor', `Restored ${countsForLog} persisted sessions`); - } - } else { - this._persistedSessions = {}; - } - - const transferredData = this.getTransferredSessionData(); - const transferredChat = transferredData?.chat; - if (transferredChat) { - this.trace('constructor', `Transferred session ${transferredChat.sessionId}`); - this._persistedSessions[transferredChat.sessionId] = transferredChat; - this._transferredSessionData = { sessionId: transferredChat.sessionId, inputValue: transferredData.inputValue }; - } - - this._register(storageService.onWillSaveState(() => this.saveState())); - } - - isEnabled(location: AideChatAgentLocation): boolean { - return this.chatAgentService.getContributedDefaultAgent(location) !== undefined; - } - - private saveState(): void { - let allSessions: (ChatModel | ISerializableChatData)[] = Array.from(this._sessionModels.values()) - .filter(session => session.initialLocation === AideChatAgentLocation.Panel) - .filter(session => session.getRequests().length > 0); - allSessions = allSessions.concat( - Object.values(this._persistedSessions) - .filter(session => !this._sessionModels.has(session.sessionId)) - .filter(session => session.requests.length)); - allSessions.sort((a, b) => (b.creationDate ?? 0) - (a.creationDate ?? 0)); - allSessions = allSessions.slice(0, maxPersistedSessions); - if (allSessions.length) { - this.trace('onWillSaveState', `Persisting ${allSessions.length} sessions`); - } - - const serialized = JSON.stringify(allSessions); - - if (allSessions.length) { - this.trace('onWillSaveState', `Persisting ${serialized.length} chars`); - } - - this.storageService.store(serializedChatKey, serialized, StorageScope.WORKSPACE, StorageTarget.MACHINE); - } - - notifyUserAction(action: IAideChatUserActionEvent): void { - if (action.action.kind === 'vote') { - this.telemetryService.publicLog2('aideChatVote', { - direction: action.action.direction === AideChatAgentVoteDirection.Up ? 'up' : 'down', - agentId: action.agentId ?? '' - }); - } else if (action.action.kind === 'copy') { - this.telemetryService.publicLog2('aideChatCopy', { - copyKind: action.action.copyKind === ChatCopyKind.Action ? 'action' : 'toolbar', - agentId: action.agentId ?? '' - }); - } else if (action.action.kind === 'insert') { - this.telemetryService.publicLog2('aideChatInsert', { - newFile: !!action.action.newFile, - agentId: action.agentId ?? '' - }); - } else if (action.action.kind === 'command') { - // TODO not currently called - const command = CommandsRegistry.getCommand(action.action.commandButton.command.id); - const commandId = command ? action.action.commandButton.command.id : 'INVALID'; - this.telemetryService.publicLog2('aideChatCommand', { - commandId, - agentId: action.agentId ?? '' - }); - } else if (action.action.kind === 'runInTerminal') { - this.telemetryService.publicLog2('aideChatRunInTerminal', { - languageId: action.action.languageId ?? '' - }); - } - - this._onDidPerformUserAction.fire(action); - } - - private trace(method: string, message?: string): void { - if (message) { - this.logService.trace(`ChatService#${method}: ${message}`); - } else { - this.logService.trace(`ChatService#${method}`); - } - } - - private error(method: string, message: string): void { - this.logService.error(`ChatService#${method} ${message}`); - } - - private deserializeChats(sessionData: string): ISerializableChatsData { - try { - const arrayOfSessions: ISerializableChatData[] = revive(JSON.parse(sessionData)); // Revive serialized URIs in session data - if (!Array.isArray(arrayOfSessions)) { - throw new Error('Expected array'); - } - - const sessions = arrayOfSessions.reduce((acc, session) => { - // Revive serialized markdown strings in response data - for (const request of session.requests) { - if (Array.isArray(request.response)) { - request.response = request.response.map((response) => { - if (typeof response === 'string') { - return new MarkdownString(response); - } - return response; - }); - } else if (typeof request.response === 'string') { - request.response = [new MarkdownString(request.response)]; - } - } - - acc[session.sessionId] = session; - return acc; - }, {}); - return sessions; - } catch (err) { - this.error('deserializeChats', `Malformed session data: ${err}. [${sessionData.substring(0, 20)}${sessionData.length > 20 ? '...' : ''}]`); - return {}; - } - } - - private getTransferredSessionData(): IChatTransfer | undefined { - const data: IChatTransfer[] = this.storageService.getObject(globalChatKey, StorageScope.PROFILE, []); - const workspaceUri = this.workspaceContextService.getWorkspace().folders[0]?.uri; - if (!workspaceUri) { - return; - } - - const thisWorkspace = workspaceUri.toString(); - const currentTime = Date.now(); - // Only use transferred data if it was created recently - const transferred = data.find(item => URI.revive(item.toWorkspace).toString() === thisWorkspace && (currentTime - item.timestampInMilliseconds < SESSION_TRANSFER_EXPIRATION_IN_MILLISECONDS)); - // Keep data that isn't for the current workspace and that hasn't expired yet - const filtered = data.filter(item => URI.revive(item.toWorkspace).toString() !== thisWorkspace && (currentTime - item.timestampInMilliseconds < SESSION_TRANSFER_EXPIRATION_IN_MILLISECONDS)); - this.storageService.store(globalChatKey, JSON.stringify(filtered), StorageScope.PROFILE, StorageTarget.MACHINE); - return transferred; - } - - /** - * Returns an array of chat details for all persisted chat sessions that have at least one request. - * The array is sorted by creation date in descending order. - * Chat sessions that have already been loaded into the chat view are excluded from the result. - * Imported chat sessions are also excluded from the result. - */ - getHistory(): IChatDetail[] { - const sessions = Object.values(this._persistedSessions) - .filter(session => session.requests.length > 0); - sessions.sort((a, b) => (b.creationDate ?? 0) - (a.creationDate ?? 0)); - - return sessions - .filter(session => !this._sessionModels.has(session.sessionId)) - .filter(session => !session.isImported) - .map(item => { - const title = ChatModel.getDefaultTitle(item.requests); - return { - sessionId: item.sessionId, - title - }; - }); - } - - removeHistoryEntry(sessionId: string): void { - delete this._persistedSessions[sessionId]; - this.saveState(); - } - - clearAllHistoryEntries(): void { - this._persistedSessions = {}; - this.saveState(); - } - - startSession(location: AideChatAgentLocation, token: CancellationToken): ChatModel { - this.trace('startSession'); - return this._startSession(undefined, location, token); - } - - private _startSession(someSessionHistory: IExportableChatData | ISerializableChatData | undefined, location: AideChatAgentLocation, token: CancellationToken): ChatModel { - const model = this.instantiationService.createInstance(ChatModel, someSessionHistory, location); - this._sessionModels.set(model.sessionId, model); - this.initializeSession(model, token); - return model; - } - - private async initializeSession(model: ChatModel, token: CancellationToken): Promise { - try { - this.trace('initializeSession', `Initialize session ${model.sessionId}`); - model.startInitialize(); - - await this.extensionService.whenInstalledExtensionsRegistered(); - const defaultAgentData = this.chatAgentService.getContributedDefaultAgent(model.initialLocation) ?? this.chatAgentService.getContributedDefaultAgent(AideChatAgentLocation.Panel); - if (!defaultAgentData) { - throw new ErrorNoTelemetry('No default agent contributed'); - } - - await this.extensionService.activateByEvent(`onChatParticipant:${defaultAgentData.id}`); - - const defaultAgent = this.chatAgentService.getActivatedAgents().find(agent => agent.id === defaultAgentData.id); - if (!defaultAgent) { - throw new ErrorNoTelemetry('No default agent registered'); - } - const welcomeMessage = model.welcomeMessage ? undefined : await defaultAgent.provideWelcomeMessage?.(model.initialLocation, token) ?? undefined; - const welcomeModel = welcomeMessage && this.instantiationService.createInstance( - ChatWelcomeMessageModel, - welcomeMessage.map(item => typeof item === 'string' ? new MarkdownString(item) : item), - await defaultAgent.provideSampleQuestions?.(model.initialLocation, token) ?? [] - ); - - model.initialize(welcomeModel); - } catch (err) { - this.trace('startSession', `initializeSession failed: ${err}`); - model.setInitializationError(err); - this._sessionModels.deleteAndDispose(model.sessionId); - this._onDidDisposeSession.fire({ sessionId: model.sessionId, reason: 'initializationFailed' }); - } - } - - getSession(sessionId: string): IChatModel | undefined { - return this._sessionModels.get(sessionId); - } - - getOrRestoreSession(sessionId: string): ChatModel | undefined { - this.trace('getOrRestoreSession', `sessionId: ${sessionId}`); - const model = this._sessionModels.get(sessionId); - if (model) { - return model; - } - - const sessionData = revive(this._persistedSessions[sessionId]); - if (!sessionData) { - return undefined; - } - - if (sessionId === this.transferredSessionData?.sessionId) { - this._transferredSessionData = undefined; - } - - return this._startSession(sessionData, sessionData.initialLocation ?? AideChatAgentLocation.Panel, CancellationToken.None); - } - - loadSessionFromContent(data: IExportableChatData | ISerializableChatData): IChatModel | undefined { - return this._startSession(data, data.initialLocation ?? AideChatAgentLocation.Panel, CancellationToken.None); - } - - async resendRequest(request: IChatRequestModel, options?: IChatSendRequestOptions): Promise { - const model = this._sessionModels.get(request.session.sessionId); - if (!model && model !== request.session) { - throw new Error(`Unknown session: ${request.session.sessionId}`); - } - - await model.waitForInitialization(); - - const cts = this._pendingRequests.get(request.session.sessionId); - if (cts) { - this.trace('resendRequest', `Session ${request.session.sessionId} already has a pending request, cancelling...`); - cts.cancel(); - } - - const location = options?.location ?? model.initialLocation; - const attempt = options?.attempt ?? 0; - const enableCommandDetection = !options?.noCommandDetection; - const defaultAgent = this.chatAgentService.getDefaultAgent(location)!; - - model.removeRequest(request.id, ChatRequestRemovalReason.Resend); - - await this._sendRequestAsync(model, model.sessionId, request.message, attempt, enableCommandDetection, defaultAgent, location, options).responseCompletePromise; - } - - async sendRequest(sessionId: string, request: string, options?: IChatSendRequestOptions): Promise { - - this.trace('sendRequest', `sessionId: ${sessionId}, message: ${request.substring(0, 20)}${request.length > 20 ? '[...]' : ''}}`); - if (!request.trim()) { - this.trace('sendRequest', 'Rejected empty message'); - return; - } - - const model = this._sessionModels.get(sessionId); - if (!model) { - throw new Error(`Unknown session: ${sessionId}`); - } - - await model.waitForInitialization(); - - if (this._pendingRequests.has(sessionId)) { - this.trace('sendRequest', `Session ${sessionId} already has a pending request`); - return; - } - - const location = options?.location ?? model.initialLocation; - const attempt = options?.attempt ?? 0; - const defaultAgent = this.chatAgentService.getDefaultAgent(location)!; - - const parsedRequest = this.parseChatRequest(sessionId, request, location, options); - const agent = parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart)?.agent ?? defaultAgent; - const agentSlashCommandPart = parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart); - - // This method is only returning whether the request was accepted - don't block on the actual request - return { - ...this._sendRequestAsync(model, sessionId, parsedRequest, attempt, !options?.noCommandDetection, defaultAgent, location, options), - agent, - slashCommand: agentSlashCommandPart?.command, - }; - } - - private parseChatRequest(sessionId: string, request: string, location: AideChatAgentLocation, options: IChatSendRequestOptions | undefined): IParsedChatRequest { - let parserContext = options?.parserContext; - if (options?.agentId) { - const agent = this.chatAgentService.getAgent(options.agentId); - if (!agent) { - throw new Error(`Unknown agent: ${options.agentId}`); - } - parserContext = { selectedAgent: agent }; - const commandPart = options.slashCommand ? ` ${chatSubcommandLeader}${options.slashCommand}` : ''; - request = `${chatAgentLeader}${agent.name}${commandPart} ${request}`; - } - - const parsedRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, request, location, parserContext); - return parsedRequest; - } - - private refreshFollowupsCancellationToken(sessionId: string): CancellationToken { - this._sessionFollowupCancelTokens.get(sessionId)?.cancel(); - const newTokenSource = new CancellationTokenSource(); - this._sessionFollowupCancelTokens.set(sessionId, newTokenSource); - - return newTokenSource.token; - } - - private _sendRequestAsync(model: ChatModel, sessionId: string, parsedRequest: IParsedChatRequest, attempt: number, enableCommandDetection: boolean, defaultAgent: IChatAgent, location: AideChatAgentLocation, options?: IChatSendRequestOptions): IChatSendRequestResponseState { - const followupsCancelToken = this.refreshFollowupsCancellationToken(sessionId); - let request: ChatRequestModel; - const agentPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentPart => r instanceof ChatRequestAgentPart); - const agentSlashCommandPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestAgentSubcommandPart => r instanceof ChatRequestAgentSubcommandPart); - const commandPart = 'kind' in parsedRequest ? undefined : parsedRequest.parts.find((r): r is ChatRequestSlashCommandPart => r instanceof ChatRequestSlashCommandPart); - - let gotProgress = false; - const requestType = commandPart ? 'slashCommand' : 'string'; - - const responseCreated = new DeferredPromise(); - let responseCreatedComplete = false; - function completeResponseCreated(): void { - if (!responseCreatedComplete && request?.response) { - responseCreated.complete(request.response); - responseCreatedComplete = true; - } - } - - const source = new CancellationTokenSource(); - const token = source.token; - const sendRequestInternal = async () => { - const progressCallback = (progress: IAideChatProgress) => { - if (token.isCancellationRequested) { - return; - } - - gotProgress = true; - - if (progress.kind === 'markdownContent') { - this.trace('sendRequest', `Provider returned progress for session ${model.sessionId}, ${progress.content.value.length} chars`); - } else { - this.trace('sendRequest', `Provider returned progress: ${JSON.stringify(progress)}`); - } - - model.acceptResponseProgress(request, progress); - completeResponseCreated(); - }; - - const stopWatch = new StopWatch(false); - const listener = token.onCancellationRequested(() => { - this.trace('sendRequest', `Request for session ${model.sessionId} was cancelled`); - this.telemetryService.publicLog2('aideChatProviderInvoked', { - timeToFirstProgress: undefined, - // Normally timings happen inside the EH around the actual provider. For cancellation we can measure how long the user waited before cancelling - totalTime: stopWatch.elapsed(), - result: 'cancelled', - requestType, - agent: agentPart?.agent.id ?? '', - slashCommand: agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command, - chatSessionId: model.sessionId, - location, - }); - - model.cancelRequest(request); - }); - - try { - let rawResult: IAideChatAgentResult | null | undefined; - let agentOrCommandFollowups: Promise | undefined = undefined; - - if (agentPart || (defaultAgent && !commandPart)) { - const agent = (agentPart?.agent ?? defaultAgent)!; - await this.extensionService.activateByEvent(`onChatParticipant:${agent.id}`); - const history = getHistoryEntriesFromModel(model, agentPart?.agent.id); - - const initVariableData: IChatRequestVariableData = { variables: [] }; - request = model.addRequest(parsedRequest, initVariableData, attempt, agent, agentSlashCommandPart?.command); - completeResponseCreated(); - const variableData = await this.chatVariablesService.resolveVariables(parsedRequest, options?.attachedContext, model, progressCallback, token); - request.variableData = variableData; - - const promptTextResult = getPromptText(request.message); - const updatedVariableData = updateRanges(variableData, promptTextResult.diff); // TODO bit of a hack - - // TODO- should figure out how to get rid of implicit variables for inline chat - const implicitVariablesEnabled = (location === AideChatAgentLocation.Editor || location === AideChatAgentLocation.Notebook); - if (implicitVariablesEnabled) { - const implicitVariables = agent.defaultImplicitVariables; - if (implicitVariables) { - const resolvedImplicitVariables = await Promise.all(implicitVariables.map(async v => { - const id = this.chatVariablesService.getVariable(v)?.id ?? ''; - const value = await this.chatVariablesService.resolveVariable(v, parsedRequest.text, model, progressCallback, token); - return value ? { id, name: v, value } satisfies IAideChatRequestVariableEntry : - undefined; - })); - updatedVariableData.variables.push(...coalesce(resolvedImplicitVariables)); - } - } - - const requestProps: IAideChatAgentRequest = { - sessionId, - requestId: request.id, - agentId: agent.id, - message: promptTextResult.message, - command: agentSlashCommandPart?.command.name, - variables: updatedVariableData, - enableCommandDetection, - attempt, - location, - acceptedConfirmationData: options?.acceptedConfirmationData, - rejectedConfirmationData: options?.rejectedConfirmationData, - }; - - const agentResult = await this.chatAgentService.invokeAgent(agent.id, requestProps, progressCallback, history, token); - rawResult = agentResult; - agentOrCommandFollowups = this.chatAgentService.getFollowups(agent.id, requestProps, agentResult, history, followupsCancelToken); - } else if (commandPart && this.chatSlashCommandService.hasCommand(commandPart.slashCommand.command)) { - request = model.addRequest(parsedRequest, { variables: [] }, attempt); - completeResponseCreated(); - // contributed slash commands - // TODO: spell this out in the UI - const history: IAideChatMessage[] = []; - for (const request of model.getRequests()) { - if (!request.response) { - continue; - } - history.push({ role: AideChatMessageRole.User, content: { type: 'text', value: request.message.text } }); - history.push({ role: AideChatMessageRole.Assistant, content: { type: 'text', value: request.response.response.asString() } }); - } - const message = parsedRequest.text; - const commandResult = await this.chatSlashCommandService.executeCommand(commandPart.slashCommand.command, message.substring(commandPart.slashCommand.command.length + 1).trimStart(), new Progress(p => { - progressCallback(p); - }), history, token); - agentOrCommandFollowups = Promise.resolve(commandResult?.followUp); - rawResult = {}; - - } else { - throw new Error(`Cannot handle request`); - } - - if (token.isCancellationRequested) { - return; - } else { - if (!rawResult) { - this.trace('sendRequest', `Provider returned no response for session ${model.sessionId}`); - rawResult = { errorDetails: { message: localize('emptyResponse', "Provider returned null response") } }; - } - - const result = rawResult.errorDetails?.responseIsFiltered ? 'filtered' : - rawResult.errorDetails && gotProgress ? 'errorWithOutput' : - rawResult.errorDetails ? 'error' : - 'success'; - this.telemetryService.publicLog2('aideChatProviderInvoked', { - timeToFirstProgress: rawResult.timings?.firstProgress, - totalTime: rawResult.timings?.totalElapsed, - result, - requestType, - agent: agentPart?.agent.id ?? '', - slashCommand: agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command, - chatSessionId: model.sessionId, - location - }); - model.setResponse(request, rawResult); - completeResponseCreated(); - this.trace('sendRequest', `Provider returned response for session ${model.sessionId}`); - - model.completeResponse(request); - if (agentOrCommandFollowups) { - agentOrCommandFollowups.then(followups => { - model.setFollowups(request, followups); - }); - } - } - } catch (err) { - const result = 'error'; - this.telemetryService.publicLog2('aideChatProviderInvoked', { - timeToFirstProgress: undefined, - totalTime: undefined, - result, - requestType, - agent: agentPart?.agent.id ?? '', - slashCommand: agentSlashCommandPart ? agentSlashCommandPart.command.name : commandPart?.slashCommand.command, - chatSessionId: model.sessionId, - location - }); - this.logService.error(`Error while handling chat request: ${toErrorMessage(err, true)}`); - if (request) { - const rawResult: IAideChatAgentResult = { errorDetails: { message: err.message } }; - model.setResponse(request, rawResult); - completeResponseCreated(); - model.completeResponse(request); - } - } finally { - listener.dispose(); - } - }; - const rawResponsePromise = sendRequestInternal(); - this._pendingRequests.set(model.sessionId, source); - rawResponsePromise.finally(() => { - this._pendingRequests.deleteAndDispose(model.sessionId); - }); - return { - responseCreatedPromise: responseCreated.p, - responseCompletePromise: rawResponsePromise, - }; - } - - async removeRequest(sessionId: string, requestId: string): Promise { - const model = this._sessionModels.get(sessionId); - if (!model) { - throw new Error(`Unknown session: ${sessionId}`); - } - - await model.waitForInitialization(); - - model.removeRequest(requestId); - } - - async adoptRequest(sessionId: string, request: IChatRequestModel) { - if (!(request instanceof ChatRequestModel)) { - throw new TypeError('Can only adopt requests of type ChatRequestModel'); - } - const target = this._sessionModels.get(sessionId); - if (!target) { - throw new Error(`Unknown session: ${sessionId}`); - } - - await target.waitForInitialization(); - - const oldOwner = request.session; - target.adoptRequest(request); - - if (request.response && !request.response.isComplete) { - const cts = this._pendingRequests.deleteAndLeak(oldOwner.sessionId); - if (cts) { - this._pendingRequests.set(target.sessionId, cts); - } - } - } - - async addCompleteRequest(sessionId: string, message: IParsedChatRequest | string, variableData: IChatRequestVariableData | undefined, attempt: number | undefined, response: IChatCompleteResponse): Promise { - this.trace('addCompleteRequest', `message: ${message}`); - - const model = this._sessionModels.get(sessionId); - if (!model) { - throw new Error(`Unknown session: ${sessionId}`); - } - - await model.waitForInitialization(); - const parsedRequest = typeof message === 'string' ? - this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(sessionId, message) : - message; - const request = model.addRequest(parsedRequest, variableData || { variables: [] }, attempt ?? 0); - if (typeof response.message === 'string') { - // TODO is this possible? - model.acceptResponseProgress(request, { content: new MarkdownString(response.message), kind: 'markdownContent' }); - } else { - for (const part of response.message) { - model.acceptResponseProgress(request, part, true); - } - } - model.setResponse(request, response.result || {}); - if (response.followups !== undefined) { - model.setFollowups(request, response.followups); - } - model.completeResponse(request); - } - - cancelCurrentRequestForSession(sessionId: string): void { - this.trace('cancelCurrentRequestForSession', `sessionId: ${sessionId}`); - this._pendingRequests.get(sessionId)?.cancel(); - this._pendingRequests.deleteAndDispose(sessionId); - } - - clearSession(sessionId: string): void { - this.trace('clearSession', `sessionId: ${sessionId}`); - const model = this._sessionModels.get(sessionId); - if (!model) { - throw new Error(`Unknown session: ${sessionId}`); - } - - if (model.initialLocation === AideChatAgentLocation.Panel) { - // Turn all the real objects into actual JSON, otherwise, calling 'revive' may fail when it tries to - // assign values to properties that are getters- microsoft/vscode-copilot-release#1233 - this._persistedSessions[sessionId] = JSON.parse(JSON.stringify(model)); - } - - this._sessionModels.deleteAndDispose(sessionId); - this._pendingRequests.get(sessionId)?.cancel(); - this._pendingRequests.deleteAndDispose(sessionId); - this._onDidDisposeSession.fire({ sessionId, reason: 'cleared' }); - } - - public hasSessions(): boolean { - return !!Object.values(this._persistedSessions); - } - - transferChatSession(transferredSessionData: IChatTransferredSessionData, toWorkspace: URI): void { - const model = Iterable.find(this._sessionModels.values(), model => model.sessionId === transferredSessionData.sessionId); - if (!model) { - throw new Error(`Failed to transfer session. Unknown session ID: ${transferredSessionData.sessionId}`); - } - - const existingRaw: IChatTransfer[] = this.storageService.getObject(globalChatKey, StorageScope.PROFILE, []); - existingRaw.push({ - chat: model.toJSON(), - timestampInMilliseconds: Date.now(), - toWorkspace: toWorkspace, - inputValue: transferredSessionData.inputValue, - }); - - this.storageService.store(globalChatKey, JSON.stringify(existingRaw), StorageScope.PROFILE, StorageTarget.MACHINE); - this.trace('transferChatSession', `Transferred session ${model.sessionId} to workspace ${toWorkspace.toString()}`); - } -} diff --git a/src/vs/workbench/contrib/aideChat/common/aideChatSlashCommands.ts b/src/vs/workbench/contrib/aideChat/common/aideChatSlashCommands.ts deleted file mode 100644 index 56d00a339de..00000000000 --- a/src/vs/workbench/contrib/aideChat/common/aideChatSlashCommands.ts +++ /dev/null @@ -1,105 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { Emitter, Event } from '../../../../base/common/event.js'; -import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IProgress } from '../../../../platform/progress/common/progress.js'; -import { IAideChatMessage } from '../../../../workbench/contrib/aideChat/common/languageModels.js'; -import { IAideChatFollowup, IAideChatProgress, IChatResponseProgressFileTreeData } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { IExtensionService } from '../../../../workbench/services/extensions/common/extensions.js'; - -//#region slash service, commands etc - -export interface IChatSlashData { - command: string; - detail: string; - sortText?: string; - - /** - * Whether the command should execute as soon - * as it is entered. Defaults to `false`. - */ - executeImmediately?: boolean; -} - -export interface IChatSlashFragment { - content: string | { treeData: IChatResponseProgressFileTreeData }; -} -export type IChatSlashCallback = { (prompt: string, progress: IProgress, history: IAideChatMessage[], token: CancellationToken): Promise<{ followUp: IAideChatFollowup[] } | void> }; - -export const IAideChatSlashCommandService = createDecorator('aideChatSlashCommandService'); - -/** - * This currently only exists to drive /clear and /help - */ -export interface IAideChatSlashCommandService { - _serviceBrand: undefined; - readonly onDidChangeCommands: Event; - registerSlashCommand(data: IChatSlashData, command: IChatSlashCallback): IDisposable; - executeCommand(id: string, prompt: string, progress: IProgress, history: IAideChatMessage[], token: CancellationToken): Promise<{ followUp: IAideChatFollowup[] } | void>; - getCommands(): Array; - hasCommand(id: string): boolean; -} - -type Tuple = { data: IChatSlashData; command?: IChatSlashCallback }; - -export class ChatSlashCommandService extends Disposable implements IAideChatSlashCommandService { - - declare _serviceBrand: undefined; - - private readonly _commands = new Map(); - - private readonly _onDidChangeCommands = this._register(new Emitter()); - readonly onDidChangeCommands: Event = this._onDidChangeCommands.event; - - constructor(@IExtensionService private readonly _extensionService: IExtensionService) { - super(); - } - - override dispose(): void { - super.dispose(); - this._commands.clear(); - } - - registerSlashCommand(data: IChatSlashData, command: IChatSlashCallback): IDisposable { - if (this._commands.has(data.command)) { - throw new Error(`Already registered a command with id ${data.command}}`); - } - - this._commands.set(data.command, { data, command }); - this._onDidChangeCommands.fire(); - - return toDisposable(() => { - if (this._commands.delete(data.command)) { - this._onDidChangeCommands.fire(); - } - }); - } - - getCommands(): Array { - return Array.from(this._commands.values(), v => v.data); - } - - hasCommand(id: string): boolean { - return this._commands.has(id); - } - - async executeCommand(id: string, prompt: string, progress: IProgress, history: IAideChatMessage[], token: CancellationToken): Promise<{ followUp: IAideChatFollowup[] } | void> { - const data = this._commands.get(id); - if (!data) { - throw new Error('No command with id ${id} NOT registered'); - } - if (!data.command) { - await this._extensionService.activateByEvent(`onSlash:${id}`); - } - if (!data.command) { - throw new Error(`No command with id ${id} NOT resolved`); - } - - return await data.command(prompt, progress, history, token); - } -} diff --git a/src/vs/workbench/contrib/aideChat/common/aideChatVariables.ts b/src/vs/workbench/contrib/aideChat/common/aideChatVariables.ts deleted file mode 100644 index f6450847471..00000000000 --- a/src/vs/workbench/contrib/aideChat/common/aideChatVariables.ts +++ /dev/null @@ -1,66 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { IDisposable } from '../../../../base/common/lifecycle.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { URI } from '../../../../base/common/uri.js'; -import { IRange } from '../../../../editor/common/core/range.js'; -import { Location } from '../../../../editor/common/languages.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { AideChatAgentLocation } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { IChatModel, IChatRequestVariableData, IAideChatRequestVariableEntry } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { IParsedChatRequest } from '../../../../workbench/contrib/aideChat/common/aideChatParserTypes.js'; -import { IAideChatContentReference, IAideChatProgressMessage } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; - -export interface IAideChatVariableData { - id: string; - name: string; - icon?: ThemeIcon; - fullName?: string; - description: string; - modelDescription?: string; - isSlow?: boolean; - hidden?: boolean; - canTakeArgument?: boolean; -} - -export type IAideChatRequestVariableValue = string | URI | Location | unknown; - -export type IAideChatVariableResolverProgress = - | IAideChatContentReference - | IAideChatProgressMessage; - -export interface IChatVariableResolver { - (messageText: string, arg: string | undefined, model: IChatModel, progress: (part: IAideChatVariableResolverProgress) => void, token: CancellationToken): Promise; -} - -export const IAideChatVariablesService = createDecorator('IAideChatVariablesService'); - -export interface IAideChatVariablesService { - _serviceBrand: undefined; - registerVariable(data: IAideChatVariableData, resolver: IChatVariableResolver): IDisposable; - hasVariable(name: string): boolean; - getVariable(name: string): IAideChatVariableData | undefined; - getVariables(): Iterable>; - getDynamicVariables(sessionId: string): ReadonlyArray; // should be its own service? - attachContext(name: string, value: string | URI | Location | unknown, location: AideChatAgentLocation): void; - - /** - * Resolves all variables that occur in `prompt` - */ - resolveVariables(prompt: IParsedChatRequest, attachedContextVariables: IAideChatRequestVariableEntry[] | undefined, model: IChatModel, progress: (part: IAideChatVariableResolverProgress) => void, token: CancellationToken): Promise; - resolveVariable(variableName: string, promptText: string, model: IChatModel, progress: (part: IAideChatVariableResolverProgress) => void, token: CancellationToken): Promise; -} - -export interface IDynamicVariable { - range: IRange; - id: string; - fullName?: string; - icon?: ThemeIcon; - prefix?: string; - modelDescription?: string; - data: IAideChatRequestVariableValue; -} diff --git a/src/vs/workbench/contrib/aideChat/common/aideChatViewModel.ts b/src/vs/workbench/contrib/aideChat/common/aideChatViewModel.ts deleted file mode 100644 index c0319479a5f..00000000000 --- a/src/vs/workbench/contrib/aideChat/common/aideChatViewModel.ts +++ /dev/null @@ -1,538 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { IMarkdownString } from '../../../../base/common/htmlContent.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; -import { marked } from '../../../../base/common/marked/marked.js'; -import { ThemeIcon } from '../../../../base/common/themables.js'; -import { URI } from '../../../../base/common/uri.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { annotateVulnerabilitiesInText } from '../../../../workbench/contrib/aideChat/common/annotations.js'; -import { getFullyQualifiedId, IChatAgentCommand, IChatAgentData, IAideChatAgentNameService, IAideChatAgentResult } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { ChatModelInitState, IChatModel, IChatProgressRenderableResponseContent, IChatRequestModel, IChatResponseModel, IChatTextEditGroup, IChatWelcomeMessageContent, IResponse } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { IParsedChatRequest } from '../../../../workbench/contrib/aideChat/common/aideChatParserTypes.js'; -import { AideChatAgentVoteDirection, IAideChatContentReference, IAideChatFollowup, IAideChatProgressMessage, IAideChatResponseErrorDetails, IAideChatTask, IChatUsedContext } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { countWords } from '../../../../workbench/contrib/aideChat/common/aideChatWordCounter.js'; -import { CodeBlockModelCollection } from './codeBlockModelCollection.js'; - -export function isRequestVM(item: unknown): item is IChatRequestViewModel { - return !!item && typeof item === 'object' && 'message' in item; -} - -export function isResponseVM(item: unknown): item is IChatResponseViewModel { - return !!item && typeof (item as IChatResponseViewModel).setVote !== 'undefined'; -} - -export function isWelcomeVM(item: unknown): item is IChatWelcomeMessageViewModel { - return !!item && typeof item === 'object' && 'content' in item; -} - -export type IChatViewModelChangeEvent = IChatAddRequestEvent | IChangePlaceholderEvent | IChatSessionInitEvent | null; - -export interface IChatAddRequestEvent { - kind: 'addRequest'; -} - -export interface IChangePlaceholderEvent { - kind: 'changePlaceholder'; -} - -export interface IChatSessionInitEvent { - kind: 'initialize'; -} - -export interface IChatViewModel { - readonly model: IChatModel; - readonly initState: ChatModelInitState; - readonly sessionId: string; - readonly onDidDisposeModel: Event; - readonly onDidChange: Event; - readonly requestInProgress: boolean; - readonly inputPlaceholder?: string; - getItems(): (IChatRequestViewModel | IChatResponseViewModel | IChatWelcomeMessageViewModel)[]; - setInputPlaceholder(text: string): void; - resetInputPlaceholder(): void; -} - -export interface IChatRequestViewModel { - readonly id: string; - readonly sessionId: string; - /** This ID updates every time the underlying data changes */ - readonly dataId: string; - readonly username: string; - readonly avatarIcon?: URI | ThemeIcon; - readonly message: IParsedChatRequest | IAideChatFollowup; - readonly messageText: string; - readonly attempt: number; - currentRenderedHeight: number | undefined; -} - -export interface IChatResponseMarkdownRenderData { - renderedWordCount: number; - lastRenderTime: number; - isFullyRendered: boolean; - originalMarkdown: IMarkdownString; -} - -export interface IChatResponseMarkdownRenderData2 { - renderedWordCount: number; - lastRenderTime: number; - isFullyRendered: boolean; - originalMarkdown: IMarkdownString; -} - -export interface IAideChatProgressMessageRenderData { - progressMessage: IAideChatProgressMessage; - - /** - * Indicates whether this is part of a group of progress messages that are at the end of the response. - * (Not whether this particular item is the very last one in the response). - * Need to re-render and add to partsToRender when this changes. - */ - isAtEndOfResponse: boolean; - - /** - * Whether this progress message the very last item in the response. - * Need to re-render to update spinner vs check when this changes. - */ - isLast: boolean; -} - -export interface IAideChatTaskRenderData { - task: IAideChatTask; - isSettled: boolean; - progressLength: number; -} - -export interface IChatResponseRenderData { - renderedParts: IChatRendererContent[]; - - renderedWordCount: number; - lastRenderTime: number; -} - -/** - * Content type for references used during rendering, not in the model - */ -export interface IChatReferences { - references: ReadonlyArray; - kind: 'references'; -} - -/** - * Type for content parts rendered by IChatListRenderer - */ -export type IChatRendererContent = IChatProgressRenderableResponseContent | IChatReferences; - -export interface IChatLiveUpdateData { - loadingStartTime: number; - lastUpdateTime: number; - impliedWordLoadRate: number; - lastWordCount: number; -} - -export interface IChatResponseViewModel { - readonly model: IChatResponseModel; - readonly id: string; - readonly sessionId: string; - /** This ID updates every time the underlying data changes */ - readonly dataId: string; - /** The ID of the associated IChatRequestViewModel */ - readonly requestId: string; - readonly username: string; - readonly avatarIcon?: URI | ThemeIcon; - readonly agent?: IChatAgentData; - readonly slashCommand?: IChatAgentCommand; - readonly agentOrSlashCommandDetected: boolean; - readonly response: IResponse; - readonly usedContext: IChatUsedContext | undefined; - readonly contentReferences: ReadonlyArray; - readonly progressMessages: ReadonlyArray; - readonly isComplete: boolean; - readonly isCanceled: boolean; - readonly isStale: boolean; - readonly vote: AideChatAgentVoteDirection | undefined; - readonly replyFollowups?: IAideChatFollowup[]; - readonly errorDetails?: IAideChatResponseErrorDetails; - readonly result?: IAideChatAgentResult; - readonly contentUpdateTimings?: IChatLiveUpdateData; - renderData?: IChatResponseRenderData; - currentRenderedHeight: number | undefined; - setVote(vote: AideChatAgentVoteDirection): void; - usedReferencesExpanded?: boolean; - vulnerabilitiesListExpanded: boolean; - setEditApplied(edit: IChatTextEditGroup, editCount: number): void; -} - -export class ChatViewModel extends Disposable implements IChatViewModel { - - private readonly _onDidDisposeModel = this._register(new Emitter()); - readonly onDidDisposeModel = this._onDidDisposeModel.event; - - private readonly _onDidChange = this._register(new Emitter()); - readonly onDidChange = this._onDidChange.event; - - private readonly _items: (ChatRequestViewModel | ChatResponseViewModel)[] = []; - - private _inputPlaceholder: string | undefined = undefined; - get inputPlaceholder(): string | undefined { - return this._inputPlaceholder; - } - - get model(): IChatModel { - return this._model; - } - - setInputPlaceholder(text: string): void { - this._inputPlaceholder = text; - this._onDidChange.fire({ kind: 'changePlaceholder' }); - } - - resetInputPlaceholder(): void { - this._inputPlaceholder = undefined; - this._onDidChange.fire({ kind: 'changePlaceholder' }); - } - - get sessionId() { - return this._model.sessionId; - } - - get requestInProgress(): boolean { - return this._model.requestInProgress; - } - - get initState() { - return this._model.initState; - } - - constructor( - private readonly _model: IChatModel, - public readonly codeBlockModelCollection: CodeBlockModelCollection, - @IInstantiationService private readonly instantiationService: IInstantiationService, - ) { - super(); - - _model.getRequests().forEach((request, i) => { - const requestModel = this.instantiationService.createInstance(ChatRequestViewModel, request); - this._items.push(requestModel); - this.updateCodeBlockTextModels(requestModel); - - if (request.response) { - this.onAddResponse(request.response); - } - }); - - this._register(_model.onDidDispose(() => this._onDidDisposeModel.fire())); - this._register(_model.onDidChange(e => { - if (e.kind === 'addRequest') { - const requestModel = this.instantiationService.createInstance(ChatRequestViewModel, e.request); - this._items.push(requestModel); - this.updateCodeBlockTextModels(requestModel); - - if (e.request.response) { - this.onAddResponse(e.request.response); - } - } else if (e.kind === 'addResponse') { - this.onAddResponse(e.response); - } else if (e.kind === 'removeRequest') { - const requestIdx = this._items.findIndex(item => isRequestVM(item) && item.id === e.requestId); - if (requestIdx >= 0) { - this._items.splice(requestIdx, 1); - } - - const responseIdx = e.responseId && this._items.findIndex(item => isResponseVM(item) && item.id === e.responseId); - if (typeof responseIdx === 'number' && responseIdx >= 0) { - const items = this._items.splice(responseIdx, 1); - const item = items[0]; - if (item instanceof ChatResponseViewModel) { - item.dispose(); - } - } - } - - const modelEventToVmEvent: IChatViewModelChangeEvent = e.kind === 'addRequest' ? { kind: 'addRequest' } : - e.kind === 'initialize' ? { kind: 'initialize' } : - null; - this._onDidChange.fire(modelEventToVmEvent); - })); - } - - private onAddResponse(responseModel: IChatResponseModel) { - const response = this.instantiationService.createInstance(ChatResponseViewModel, responseModel); - this._register(response.onDidChange(() => { - if (response.isComplete) { - this.updateCodeBlockTextModels(response); - } - return this._onDidChange.fire(null); - })); - this._items.push(response); - this.updateCodeBlockTextModels(response); - } - - getItems(): (IChatRequestViewModel | IChatResponseViewModel | IChatWelcomeMessageViewModel)[] { - return [...(this._model.welcomeMessage ? [this._model.welcomeMessage] : []), ...this._items]; - } - - override dispose() { - super.dispose(); - this._items - .filter((item): item is ChatResponseViewModel => item instanceof ChatResponseViewModel) - .forEach((item: ChatResponseViewModel) => item.dispose()); - } - - updateCodeBlockTextModels(model: IChatRequestViewModel | IChatResponseViewModel) { - let content: string; - if (isRequestVM(model)) { - content = model.messageText; - } else { - content = annotateVulnerabilitiesInText(model.response.value).map(x => x.content.value).join(''); - } - - let codeBlockIndex = 0; - marked.walkTokens(marked.lexer(content), token => { - if (token.type === 'code') { - const lang = token.lang || ''; - const text = token.text; - this.codeBlockModelCollection.update(this._model.sessionId, model, codeBlockIndex++, { text, languageId: lang }); - } - }); - } -} - -export class ChatRequestViewModel implements IChatRequestViewModel { - get id() { - return this._model.id; - } - - get dataId() { - return this.id + `_${ChatModelInitState[this._model.session.initState]}`; - } - - get sessionId() { - return this._model.session.sessionId; - } - - get username() { - return this._model.username; - } - - get avatarIcon() { - return this._model.avatarIconUri; - } - - get message() { - return this._model.message; - } - - get messageText() { - return this.message.text; - } - - get attempt() { - return this._model.attempt; - } - - currentRenderedHeight: number | undefined; - - constructor( - private readonly _model: IChatRequestModel, - ) { } -} - -export class ChatResponseViewModel extends Disposable implements IChatResponseViewModel { - private _modelChangeCount = 0; - - private readonly _onDidChange = this._register(new Emitter()); - readonly onDidChange = this._onDidChange.event; - - get model() { - return this._model; - } - - get id() { - return this._model.id; - } - - get dataId() { - return this._model.id + `_${this._modelChangeCount}` + `_${ChatModelInitState[this._model.session.initState]}`; - } - - get sessionId() { - return this._model.session.sessionId; - } - - get username() { - if (this.agent) { - const isAllowed = this.chatAgentNameService.getAgentNameRestriction(this.agent); - if (isAllowed) { - return this.agent.fullName || this.agent.name; - } else { - return getFullyQualifiedId(this.agent); - } - } - - return this._model.username; - } - - get avatarIcon() { - return this._model.avatarIcon; - } - - get agent() { - return this._model.agent; - } - - get slashCommand() { - return this._model.slashCommand; - } - - get agentOrSlashCommandDetected() { - return this._model.agentOrSlashCommandDetected; - } - - get response(): IResponse { - return this._model.response; - } - - get usedContext(): IChatUsedContext | undefined { - return this._model.usedContext; - } - - get contentReferences(): ReadonlyArray { - return this._model.contentReferences; - } - - get progressMessages(): ReadonlyArray { - return this._model.progressMessages; - } - - get isComplete() { - return this._model.isComplete; - } - - get isCanceled() { - return this._model.isCanceled; - } - - get replyFollowups() { - return this._model.followups?.filter((f): f is IAideChatFollowup => f.kind === 'reply'); - } - - get result() { - return this._model.result; - } - - get errorDetails(): IAideChatResponseErrorDetails | undefined { - return this.result?.errorDetails; - } - - get vote() { - return this._model.vote; - } - - get requestId() { - return this._model.requestId; - } - - get isStale() { - return this._model.isStale; - } - - renderData: IChatResponseRenderData | undefined = undefined; - currentRenderedHeight: number | undefined; - - private _usedReferencesExpanded: boolean | undefined; - get usedReferencesExpanded(): boolean | undefined { - if (typeof this._usedReferencesExpanded === 'boolean') { - return this._usedReferencesExpanded; - } - - return this.response.value.length === 0; - } - - set usedReferencesExpanded(v: boolean) { - this._usedReferencesExpanded = v; - } - - private _vulnerabilitiesListExpanded: boolean = false; - get vulnerabilitiesListExpanded(): boolean { - return this._vulnerabilitiesListExpanded; - } - - set vulnerabilitiesListExpanded(v: boolean) { - this._vulnerabilitiesListExpanded = v; - } - - private _contentUpdateTimings: IChatLiveUpdateData | undefined = undefined; - get contentUpdateTimings(): IChatLiveUpdateData | undefined { - return this._contentUpdateTimings; - } - - constructor( - private readonly _model: IChatResponseModel, - @ILogService private readonly logService: ILogService, - @IAideChatAgentNameService private readonly chatAgentNameService: IAideChatAgentNameService, - ) { - super(); - - if (!_model.isComplete) { - this._contentUpdateTimings = { - loadingStartTime: Date.now(), - lastUpdateTime: Date.now(), - impliedWordLoadRate: 0, - lastWordCount: 0 - }; - } - - this._register(_model.onDidChange(() => { - if (this._contentUpdateTimings) { - // This should be true, if the model is changing - const now = Date.now(); - const wordCount = countWords(_model.response.asString()); - const timeDiff = now - this._contentUpdateTimings.loadingStartTime; - const impliedWordLoadRate = this._contentUpdateTimings.lastWordCount / (timeDiff / 1000); - this.trace('onDidChange', `Update- got ${this._contentUpdateTimings.lastWordCount} words over ${timeDiff}ms = ${impliedWordLoadRate} words/s. ${wordCount} words are now available.`); - this._contentUpdateTimings = { - loadingStartTime: this._contentUpdateTimings.loadingStartTime, - lastUpdateTime: now, - impliedWordLoadRate, - lastWordCount: wordCount - }; - } else { - this.logService.warn('ChatResponseViewModel#onDidChange: got model update but contentUpdateTimings is not initialized'); - } - - // new data -> new id, new content to render - this._modelChangeCount++; - - this._onDidChange.fire(); - })); - } - - private trace(tag: string, message: string) { - this.logService.trace(`ChatResponseViewModel#${tag}: ${message}`); - } - - setVote(vote: AideChatAgentVoteDirection): void { - this._modelChangeCount++; - this._model.setVote(vote); - } - - setEditApplied(edit: IChatTextEditGroup, editCount: number) { - this._modelChangeCount++; - this._model.setEditApplied(edit, editCount); - } -} - -export interface IChatWelcomeMessageViewModel { - readonly id: string; - readonly username: string; - readonly avatarIcon?: URI | ThemeIcon; - readonly content: IChatWelcomeMessageContent[]; - readonly sampleQuestions: IAideChatFollowup[]; - currentRenderedHeight?: number; -} diff --git a/src/vs/workbench/contrib/aideChat/common/aideChatWidgetHistoryService.ts b/src/vs/workbench/contrib/aideChat/common/aideChatWidgetHistoryService.ts deleted file mode 100644 index 8a887a58450..00000000000 --- a/src/vs/workbench/contrib/aideChat/common/aideChatWidgetHistoryService.ts +++ /dev/null @@ -1,80 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js'; -import { Memento } from '../../../../workbench/common/memento.js'; -import { AideChatAgentLocation } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { CHAT_PROVIDER_ID } from '../../../../workbench/contrib/aideChat/common/aideChatParticipantContribTypes.js'; - -export interface IChatHistoryEntry { - text: string; - state?: any; -} - -export const IAideChatWidgetHistoryService = createDecorator('IAideChatWidgetHistoryService'); -export interface IAideChatWidgetHistoryService { - _serviceBrand: undefined; - - readonly onDidClearHistory: Event; - - clearHistory(): void; - getHistory(location: AideChatAgentLocation): IChatHistoryEntry[]; - saveHistory(location: AideChatAgentLocation, history: IChatHistoryEntry[]): void; -} - -interface IChatHistory { - history: { [providerId: string]: IChatHistoryEntry[] }; -} - -export class ChatWidgetHistoryService implements IAideChatWidgetHistoryService { - _serviceBrand: undefined; - - private memento: Memento; - private viewState: IChatHistory; - - private readonly _onDidClearHistory = new Emitter(); - readonly onDidClearHistory: Event = this._onDidClearHistory.event; - - constructor( - @IStorageService storageService: IStorageService - ) { - this.memento = new Memento('aide-chat-history', storageService); - const loadedState = this.memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE) as IChatHistory; - for (const provider in loadedState.history) { - // Migration from old format - loadedState.history[provider] = loadedState.history[provider].map(entry => typeof entry === 'string' ? { text: entry } : entry); - } - - this.viewState = loadedState; - } - - getHistory(location: AideChatAgentLocation): IChatHistoryEntry[] { - const key = this.getKey(location); - return this.viewState.history?.[key] ?? []; - } - - private getKey(location: AideChatAgentLocation): string { - // Preserve history for panel by continuing to use the same old provider id. Use the location as a key for other chat locations. - return location === AideChatAgentLocation.Panel ? CHAT_PROVIDER_ID : location; - } - - saveHistory(location: AideChatAgentLocation, history: IChatHistoryEntry[]): void { - if (!this.viewState.history) { - this.viewState.history = {}; - } - - const key = this.getKey(location); - this.viewState.history[key] = history; - this.memento.saveMemento(); - } - - clearHistory(): void { - this.viewState.history = {}; - this.memento.saveMemento(); - this._onDidClearHistory.fire(); - } -} diff --git a/src/vs/workbench/contrib/aideChat/common/aideChatWordCounter.ts b/src/vs/workbench/contrib/aideChat/common/aideChatWordCounter.ts deleted file mode 100644 index edd27ddc435..00000000000 --- a/src/vs/workbench/contrib/aideChat/common/aideChatWordCounter.ts +++ /dev/null @@ -1,35 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -export interface IWordCountResult { - value: string; - returnedWordCount: number; - totalWordCount: number; - isFullString: boolean; -} - -export function getNWords(str: string, numWordsToCount: number): IWordCountResult { - // Match words and markdown style links - const allWordMatches = Array.from(str.matchAll(/\[([^\]]+)\]\(([^)]+)\)|\p{sc=Han}|[^\s\|\-|\p{sc=Han}]+/gu)); - - const targetWords = allWordMatches.slice(0, numWordsToCount); - - const endIndex = numWordsToCount > allWordMatches.length - ? str.length // Reached end of string - : targetWords.length ? targetWords.at(-1)!.index + targetWords.at(-1)![0].length : 0; - - const value = str.substring(0, endIndex); - return { - value, - returnedWordCount: targetWords.length === 0 ? (value.length ? 1 : 0) : targetWords.length, - isFullString: endIndex >= str.length, - totalWordCount: allWordMatches.length - }; -} - -export function countWords(str: string): number { - const result = getNWords(str, Number.MAX_SAFE_INTEGER); - return result.returnedWordCount; -} diff --git a/src/vs/workbench/contrib/aideChat/common/annotations.ts b/src/vs/workbench/contrib/aideChat/common/annotations.ts deleted file mode 100644 index 8088675e75f..00000000000 --- a/src/vs/workbench/contrib/aideChat/common/annotations.ts +++ /dev/null @@ -1,107 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import { MarkdownString } from '../../../../base/common/htmlContent.js'; -import { basename } from '../../../../base/common/resources.js'; -import { URI } from '../../../../base/common/uri.js'; -import { IRange } from '../../../../editor/common/core/range.js'; -import { IChatProgressRenderableResponseContent, IAideChatProgressResponseContent, appendMarkdownString, canMergeMarkdownStrings } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { IChatAgentVulnerabilityDetails, IAideChatMarkdownContent } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; - -export const contentRefUrl = 'http://_vscodecontentref_'; // must be lowercase for URI - -export function annotateSpecialMarkdownContent(response: ReadonlyArray): IChatProgressRenderableResponseContent[] { - const result: IChatProgressRenderableResponseContent[] = []; - for (const item of response) { - const previousItem = result[result.length - 1]; - if (item.kind === 'inlineReference') { - const location = 'uri' in item.inlineReference ? item.inlineReference : { uri: item.inlineReference }; - const printUri = URI.parse(contentRefUrl).with({ fragment: JSON.stringify(location) }); - const markdownText = `[${item.name || basename(location.uri)}](${printUri.toString()})`; - if (previousItem?.kind === 'markdownContent') { - const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText)); - result[result.length - 1] = { content: merged, kind: 'markdownContent' }; - } else { - result.push({ content: new MarkdownString(markdownText), kind: 'markdownContent' }); - } - } else if (item.kind === 'markdownContent' && previousItem?.kind === 'markdownContent' && canMergeMarkdownStrings(previousItem.content, item.content)) { - const merged = appendMarkdownString(previousItem.content, item.content); - result[result.length - 1] = { content: merged, kind: 'markdownContent' }; - } else if (item.kind === 'markdownVuln') { - const vulnText = encodeURIComponent(JSON.stringify(item.vulnerabilities)); - const markdownText = `${item.content.value}`; - if (previousItem?.kind === 'markdownContent') { - // Since this is inside a codeblock, it needs to be merged into the previous markdown content. - const merged = appendMarkdownString(previousItem.content, new MarkdownString(markdownText)); - result[result.length - 1] = { content: merged, kind: 'markdownContent' }; - } else { - result.push({ content: new MarkdownString(markdownText), kind: 'markdownContent' }); - } - } else { - result.push(item); - } - } - - return result; -} - -export interface IMarkdownVulnerability { - readonly title: string; - readonly description: string; - readonly range: IRange; -} - -export function annotateVulnerabilitiesInText(response: ReadonlyArray): readonly IAideChatMarkdownContent[] { - const result: IAideChatMarkdownContent[] = []; - for (const item of response) { - const previousItem = result[result.length - 1]; - if (item.kind === 'markdownContent') { - if (previousItem?.kind === 'markdownContent') { - result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + item.content.value, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' }; - } else { - result.push(item); - } - } else if (item.kind === 'markdownVuln') { - const vulnText = encodeURIComponent(JSON.stringify(item.vulnerabilities)); - const markdownText = `${item.content.value}`; - if (previousItem?.kind === 'markdownContent') { - result[result.length - 1] = { content: new MarkdownString(previousItem.content.value + markdownText, { isTrusted: previousItem.content.isTrusted }), kind: 'markdownContent' }; - } else { - result.push({ content: new MarkdownString(markdownText), kind: 'markdownContent' }); - } - } - } - - return result; -} - -export function extractVulnerabilitiesFromText(text: string): { newText: string; vulnerabilities: IMarkdownVulnerability[] } { - const vulnerabilities: IMarkdownVulnerability[] = []; - let newText = text; - let match: RegExpExecArray | null; - while ((match = /(.*?)<\/vscode_annotation>/ms.exec(newText)) !== null) { - const [full, details, content] = match; - const start = match.index; - const textBefore = newText.substring(0, start); - const linesBefore = textBefore.split('\n').length - 1; - const linesInside = content.split('\n').length - 1; - - const previousNewlineIdx = textBefore.lastIndexOf('\n'); - const startColumn = start - (previousNewlineIdx + 1) + 1; - const endPreviousNewlineIdx = (textBefore + content).lastIndexOf('\n'); - const endColumn = start + content.length - (endPreviousNewlineIdx + 1) + 1; - - try { - const vulnDetails: IChatAgentVulnerabilityDetails[] = JSON.parse(decodeURIComponent(details)); - vulnDetails.forEach(({ title, description }) => vulnerabilities.push({ - title, description, range: { startLineNumber: linesBefore + 1, startColumn, endLineNumber: linesBefore + linesInside + 1, endColumn } - })); - } catch (err) { - // Something went wrong with encoding this text, just ignore it - } - newText = newText.substring(0, start) + content + newText.substring(start + full.length); - } - - return { newText, vulnerabilities }; -} diff --git a/src/vs/workbench/contrib/aideChat/common/codeBlockModelCollection.ts b/src/vs/workbench/contrib/aideChat/common/codeBlockModelCollection.ts deleted file mode 100644 index dc11f7ca418..00000000000 --- a/src/vs/workbench/contrib/aideChat/common/codeBlockModelCollection.ts +++ /dev/null @@ -1,149 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Disposable, IReference } from '../../../../base/common/lifecycle.js'; -import { ResourceMap } from '../../../../base/common/map.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { URI } from '../../../../base/common/uri.js'; -import { Range } from '../../../../editor/common/core/range.js'; -import { ILanguageService } from '../../../../editor/common/languages/language.js'; -import { EndOfLinePreference } from '../../../../editor/common/model.js'; -import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js'; -import { IChatRequestViewModel, IChatResponseViewModel, isResponseVM } from '../../../../workbench/contrib/aideChat/common/aideChatViewModel.js'; -import { extractVulnerabilitiesFromText, IMarkdownVulnerability } from './annotations.js'; - - -export class CodeBlockModelCollection extends Disposable { - - private readonly _models = new ResourceMap<{ - readonly model: Promise>; - vulns: readonly IMarkdownVulnerability[]; - }>(); - - constructor( - @ILanguageService private readonly languageService: ILanguageService, - @ITextModelService private readonly textModelService: ITextModelService - ) { - super(); - } - - public override dispose(): void { - super.dispose(); - this.clear(); - } - - get(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): { model: Promise; readonly vulns: readonly IMarkdownVulnerability[] } | undefined { - const uri = this.getUri(sessionId, chat, codeBlockIndex); - const entry = this._models.get(uri); - if (!entry) { - return; - } - return { model: entry.model.then(ref => ref.object), vulns: entry.vulns }; - } - - getOrCreate(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number): { model: Promise; readonly vulns: readonly IMarkdownVulnerability[] } { - const existing = this.get(sessionId, chat, codeBlockIndex); - if (existing) { - return existing; - } - - const uri = this.getUri(sessionId, chat, codeBlockIndex); - const ref = this.textModelService.createModelReference(uri); - this._models.set(uri, { model: ref, vulns: [] }); - return { model: ref.then(ref => ref.object), vulns: [] }; - } - - clear(): void { - this._models.forEach(async entry => (await entry.model).dispose()); - this._models.clear(); - } - - async update(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number, content: { text: string; languageId?: string }) { - const entry = this.getOrCreate(sessionId, chat, codeBlockIndex); - - const extractedVulns = extractVulnerabilitiesFromText(content.text); - const newText = fixCodeText(extractedVulns.newText, content.languageId); - this.setVulns(sessionId, chat, codeBlockIndex, extractedVulns.vulnerabilities); - - const textModel = (await entry.model).textEditorModel; - if (content.languageId) { - const vscodeLanguageId = this.languageService.getLanguageIdByLanguageName(content.languageId); - if (vscodeLanguageId && vscodeLanguageId !== textModel.getLanguageId()) { - textModel.setLanguage(vscodeLanguageId); - } - } - - const currentText = textModel.getValue(EndOfLinePreference.LF); - if (newText === currentText) { - return; - } - - if (newText.startsWith(currentText)) { - const text = newText.slice(currentText.length); - const lastLine = textModel.getLineCount(); - const lastCol = textModel.getLineMaxColumn(lastLine); - textModel.applyEdits([{ range: new Range(lastLine, lastCol, lastLine, lastCol), text }]); - } else { - // console.log(`Failed to optimize setText`); - textModel.setValue(newText); - } - } - - private setVulns(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, codeBlockIndex: number, vulnerabilities: IMarkdownVulnerability[]) { - const uri = this.getUri(sessionId, chat, codeBlockIndex); - const entry = this._models.get(uri); - if (entry) { - entry.vulns = vulnerabilities; - } - } - - private getUri(sessionId: string, chat: IChatRequestViewModel | IChatResponseViewModel, index: number): URI { - const metadata = this.getUriMetaData(chat); - return URI.from({ - scheme: Schemas.vscodeChatCodeBlock, - authority: sessionId, - path: `/${chat.id}/${index}`, - fragment: metadata ? JSON.stringify(metadata) : undefined, - }); - } - - private getUriMetaData(chat: IChatRequestViewModel | IChatResponseViewModel) { - if (!isResponseVM(chat)) { - return undefined; - } - - return { - references: chat.contentReferences.map(ref => { - const uriOrLocation = 'variableName' in ref.reference ? - ref.reference.value : - ref.reference; - if (!uriOrLocation) { - return; - } - - if (URI.isUri(uriOrLocation)) { - return { - uri: uriOrLocation.toJSON() - }; - } - - return { - uri: uriOrLocation.uri.toJSON(), - range: uriOrLocation.range, - }; - }) - }; - } -} - -function fixCodeText(text: string, languageId: string | undefined): string { - if (languageId === 'php') { - if (!text.trim().startsWith('<')) { - return `('ILanguageModelStatsService'); - -export interface ILanguageModelStatsService { - readonly _serviceBrand: undefined; - - update(model: string, extensionId: ExtensionIdentifier, agent: string | undefined, tokenCount: number | undefined): Promise; -} - -interface LanguageModelStats { - extensions: { - extensionId: string; - requestCount: number; - tokenCount: number; - participants: { - id: string; - requestCount: number; - tokenCount: number; - }[]; - }[]; -} - -export class LanguageModelStatsService extends Disposable implements ILanguageModelStatsService { - - private static readonly MODEL_STATS_STORAGE_KEY_PREFIX = 'languageModelStats.'; - private static readonly MODEL_ACCESS_STORAGE_KEY_PREFIX = 'languageModelAccess.'; - - declare _serviceBrand: undefined; - - private readonly _onDidChangeStats = this._register(new Emitter()); - readonly onDidChangeLanguageMoelStats = this._onDidChangeStats.event; - - private readonly sessionStats = new Map(); - - constructor( - @IExtensionFeaturesManagementService private readonly extensionFeaturesManagementService: IExtensionFeaturesManagementService, - @IStorageService private readonly _storageService: IStorageService, - ) { - super(); - this._register(_storageService.onDidChangeValue(StorageScope.APPLICATION, undefined, this._store)(e => { - const model = this.getModel(e.key); - if (model) { - this._onDidChangeStats.fire(model); - } - })); - } - - hasAccessedModel(extensionId: string, model: string): boolean { - return this.getAccessExtensions(model).includes(extensionId.toLowerCase()); - } - - async update(model: string, extensionId: ExtensionIdentifier, agent: string | undefined, tokenCount: number | undefined): Promise { - await this.extensionFeaturesManagementService.getAccess(extensionId, 'languageModels'); - - // update model access - this.addAccess(model, extensionId.value); - - // update session stats - let sessionStats = this.sessionStats.get(model); - if (!sessionStats) { - sessionStats = { extensions: [] }; - this.sessionStats.set(model, sessionStats); - } - this.add(sessionStats, extensionId.value, agent, tokenCount); - - this.write(model, extensionId.value, agent, tokenCount); - this._onDidChangeStats.fire(model); - } - - private addAccess(model: string, extensionId: string): void { - extensionId = extensionId.toLowerCase(); - const extensions = this.getAccessExtensions(model); - if (!extensions.includes(extensionId)) { - extensions.push(extensionId); - this._storageService.store(this.getAccessKey(model), JSON.stringify(extensions), StorageScope.APPLICATION, StorageTarget.USER); - } - } - - private getAccessExtensions(model: string): string[] { - const key = this.getAccessKey(model); - const data = this._storageService.get(key, StorageScope.APPLICATION); - try { - if (data) { - const parsed = JSON.parse(data); - if (Array.isArray(parsed)) { - return parsed; - } - } - } catch (e) { - // ignore - } - return []; - - } - - private async write(model: string, extensionId: string, participant: string | undefined, tokenCount: number | undefined): Promise { - const modelStats = await this.read(model); - this.add(modelStats, extensionId, participant, tokenCount); - this._storageService.store(this.getKey(model), JSON.stringify(modelStats), StorageScope.APPLICATION, StorageTarget.USER); - } - - private add(modelStats: LanguageModelStats, extensionId: string, participant: string | undefined, tokenCount: number | undefined): void { - let extensionStats = modelStats.extensions.find(e => ExtensionIdentifier.equals(e.extensionId, extensionId)); - if (!extensionStats) { - extensionStats = { extensionId, requestCount: 0, tokenCount: 0, participants: [] }; - modelStats.extensions.push(extensionStats); - } - if (participant) { - let participantStats = extensionStats.participants.find(p => p.id === participant); - if (!participantStats) { - participantStats = { id: participant, requestCount: 0, tokenCount: 0 }; - extensionStats.participants.push(participantStats); - } - participantStats.requestCount++; - participantStats.tokenCount += tokenCount ?? 0; - } else { - extensionStats.requestCount++; - extensionStats.tokenCount += tokenCount ?? 0; - } - } - - private async read(model: string): Promise { - try { - const value = this._storageService.get(this.getKey(model), StorageScope.APPLICATION); - if (value) { - return JSON.parse(value); - } - } catch (error) { - // ignore - } - return { extensions: [] }; - } - - private getModel(key: string): string | undefined { - if (key.startsWith(LanguageModelStatsService.MODEL_STATS_STORAGE_KEY_PREFIX)) { - return key.substring(LanguageModelStatsService.MODEL_STATS_STORAGE_KEY_PREFIX.length); - } - return undefined; - } - - private getKey(model: string): string { - return `${LanguageModelStatsService.MODEL_STATS_STORAGE_KEY_PREFIX}${model}`; - } - - private getAccessKey(model: string): string { - return `${LanguageModelStatsService.MODEL_ACCESS_STORAGE_KEY_PREFIX}${model}`; - } -} - -Registry.as(Extensions.ExtensionFeaturesRegistry).registerExtensionFeature({ - id: 'aiModels', - label: localize('Language Models', "Language Models"), - description: localize('aiModels', "Language models usage statistics of this extension."), - access: { - canToggle: false - }, -}); diff --git a/src/vs/workbench/contrib/aideChat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/aideChat/common/languageModelToolsService.ts deleted file mode 100644 index c7fbfc87795..00000000000 --- a/src/vs/workbench/contrib/aideChat/common/languageModelToolsService.ts +++ /dev/null @@ -1,110 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { CancellationToken } from '../../../../base/common/cancellation.js'; -import { IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { Iterable } from '../../../../base/common/iterator.js'; -import { IExtensionService } from '../../../../workbench/services/extensions/common/extensions.js'; - -export interface IToolData { - name: string; - displayName?: string; - description: string; - parametersSchema?: Object; -} - -interface IToolEntry { - data: IToolData; - impl?: IToolImpl; -} - -export interface IToolImpl { - invoke(parameters: any, token: CancellationToken): Promise; -} - -export const ILanguageModelToolsService = createDecorator('ILanguageModelToolsService'); - -export interface IToolDelta { - added?: IToolData; - removed?: string; -} - -export interface ILanguageModelToolsService { - _serviceBrand: undefined; - onDidChangeTools: Event; - registerToolData(toolData: IToolData): IDisposable; - registerToolImplementation(name: string, tool: IToolImpl): IDisposable; - getTools(): Iterable>; - invokeTool(name: string, parameters: any, token: CancellationToken): Promise; -} - -export class LanguageModelToolsService implements ILanguageModelToolsService { - _serviceBrand: undefined; - - private _onDidChangeTools = new Emitter(); - readonly onDidChangeTools = this._onDidChangeTools.event; - - private _tools = new Map(); - - constructor( - @IExtensionService private readonly _extensionService: IExtensionService - ) { } - - registerToolData(toolData: IToolData): IDisposable { - if (this._tools.has(toolData.name)) { - throw new Error(`Tool "${toolData.name}" is already registered.`); - } - - this._tools.set(toolData.name, { data: toolData }); - this._onDidChangeTools.fire({ added: toolData }); - - return toDisposable(() => { - this._tools.delete(toolData.name); - this._onDidChangeTools.fire({ removed: toolData.name }); - }); - - } - - registerToolImplementation(name: string, tool: IToolImpl): IDisposable { - const entry = this._tools.get(name); - if (!entry) { - throw new Error(`Tool "${name}" was not contributed.`); - } - - if (entry.impl) { - throw new Error(`Tool "${name}" already has an implementation.`); - } - - entry.impl = tool; - return toDisposable(() => { - entry.impl = undefined; - }); - } - - getTools(): Iterable> { - return Iterable.map(this._tools.values(), i => i.data); - } - - async invokeTool(name: string, parameters: any, token: CancellationToken): Promise { - let tool = this._tools.get(name); - if (!tool) { - throw new Error(`Tool ${name} was not contributed`); - } - - if (!tool.impl) { - await this._extensionService.activateByEvent(`onLanguageModelTool:${name}`); - - // Extension should activate and register the tool implementation - tool = this._tools.get(name); - if (!tool?.impl) { - throw new Error(`Tool ${name} does not have an implementation registered.`); - } - } - - return tool.impl.invoke(parameters, token); - } -} diff --git a/src/vs/workbench/contrib/aideChat/common/languageModels.ts b/src/vs/workbench/contrib/aideChat/common/languageModels.ts deleted file mode 100644 index a831d4a2b69..00000000000 --- a/src/vs/workbench/contrib/aideChat/common/languageModels.ts +++ /dev/null @@ -1,300 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { Emitter, Event } from '../../../../base/common/event.js'; -import { Iterable } from '../../../../base/common/iterator.js'; -import { IJSONSchema } from '../../../../base/common/jsonSchema.js'; -import { DisposableStore, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { isFalsyOrWhitespace } from '../../../../base/common/strings.js'; -import { localize } from '../../../../nls.js'; -import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { IExtensionService, isProposedApiEnabled } from '../../../../workbench/services/extensions/common/extensions.js'; -import { ExtensionsRegistry } from '../../../../workbench/services/extensions/common/extensionsRegistry.js'; - -export const enum AideChatMessageRole { - System, - User, - Assistant, -} - -export interface IChatMessageTextPart { - type: 'text'; - value: string; -} - -export interface IChatMessageFunctionResultPart { - type: 'function_result'; - name: string; - value: any; - isError?: boolean; -} - -export type IChatMessagePart = IChatMessageTextPart | IChatMessageFunctionResultPart; - -export interface IAideChatMessage { - readonly name?: string | undefined; - readonly role: AideChatMessageRole; - readonly content: IChatMessagePart; -} - -export interface IChatResponseTextPart { - type: 'text'; - value: string; -} - -export interface IChatResponceFunctionUsePart { - type: 'function_use'; - name: string; - parameters: any; -} - -export type IChatResponsePart = IChatResponseTextPart | IChatResponceFunctionUsePart; - -export interface IChatResponseFragment { - index: number; - part: IChatResponsePart; -} - -export interface ILanguageModelChatMetadata { - readonly extension: ExtensionIdentifier; - - readonly name: string; - readonly id: string; - readonly vendor: string; - readonly version: string; - readonly family: string; - readonly maxInputTokens: number; - readonly maxOutputTokens: number; - readonly targetExtensions?: string[]; - - readonly auth?: { - readonly providerLabel: string; - readonly accountLabel?: string; - }; -} - -export interface ILanguageModelChatResponse { - stream: AsyncIterable; - result: Promise; -} - -export interface ILanguageModelChat { - metadata: ILanguageModelChatMetadata; - sendChatRequest(messages: IAideChatMessage[], from: ExtensionIdentifier, options: { [name: string]: any }, token: CancellationToken): Promise; - provideTokenCount(message: string | IAideChatMessage, token: CancellationToken): Promise; -} - -export interface ILanguageModelChatSelector { - readonly name?: string; - readonly identifier?: string; - readonly vendor?: string; - readonly version?: string; - readonly family?: string; - readonly tokens?: number; - readonly extension?: ExtensionIdentifier; -} - -export const ILanguageModelsService = createDecorator('ILanguageModelsService'); - -export interface ILanguageModelsChangeEvent { - added?: { - identifier: string; - metadata: ILanguageModelChatMetadata; - }[]; - removed?: string[]; -} - -export interface ILanguageModelsService { - - readonly _serviceBrand: undefined; - - onDidChangeLanguageModels: Event; - - getLanguageModelIds(): string[]; - - lookupLanguageModel(identifier: string): ILanguageModelChatMetadata | undefined; - - selectLanguageModels(selector: ILanguageModelChatSelector): Promise; - - registerLanguageModelChat(identifier: string, provider: ILanguageModelChat): IDisposable; - - sendChatRequest(identifier: string, from: ExtensionIdentifier, messages: IAideChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise; - - computeTokenLength(identifier: string, message: string | IAideChatMessage, token: CancellationToken): Promise; -} - -const languageModelType: IJSONSchema = { - type: 'object', - properties: { - vendor: { - type: 'string', - description: localize('vscode.extension.contributes.AIModels.vendor', "A globally unique vendor of language models.") - } - } -}; - -interface IUserFriendlyLanguageModel { - vendor: string; -} - -export const languageModelExtensionPoint = ExtensionsRegistry.registerExtensionPoint({ - extensionPoint: 'aiModels', - jsonSchema: { - description: localize('vscode.extension.contributes.aiModels', "Contribute language models of a specific vendor."), - oneOf: [ - languageModelType, - { - type: 'array', - items: languageModelType - } - ] - }, - activationEventsGenerator: (contribs: IUserFriendlyLanguageModel[], result: { push(item: string): void }) => { - for (const contrib of contribs) { - result.push(`onAIModelChat:${contrib.vendor}`); - } - } -}); - -export class LanguageModelsService implements ILanguageModelsService { - - readonly _serviceBrand: undefined; - - private readonly _store = new DisposableStore(); - - private readonly _providers = new Map(); - private readonly _vendors = new Set(); - - private readonly _onDidChangeProviders = this._store.add(new Emitter()); - readonly onDidChangeLanguageModels: Event = this._onDidChangeProviders.event; - - constructor( - @IExtensionService private readonly _extensionService: IExtensionService, - @ILogService private readonly _logService: ILogService, - ) { - - this._store.add(languageModelExtensionPoint.setHandler((extensions) => { - - this._vendors.clear(); - - for (const extension of extensions) { - - if (!isProposedApiEnabled(extension.description, 'chatProvider')) { - extension.collector.error(localize('vscode.extension.contributes.AIModels.chatProviderRequired', "This contribution point requires the 'chatProvider' proposal.")); - continue; - } - - for (const item of Iterable.wrap(extension.value)) { - if (this._vendors.has(item.vendor)) { - extension.collector.error(localize('vscode.extension.contributes.AIModels.vendorAlreadyRegistered', "The vendor '{0}' is already registered and cannot be registered twice", item.vendor)); - continue; - } - if (isFalsyOrWhitespace(item.vendor)) { - extension.collector.error(localize('vscode.extension.contributes.AIModels.emptyVendor', "The vendor field cannot be empty.")); - continue; - } - if (item.vendor.trim() !== item.vendor) { - extension.collector.error(localize('vscode.extension.contributes.AIModels.whitespaceVendor', "The vendor field cannot start or end with whitespace.")); - continue; - } - this._vendors.add(item.vendor); - } - } - - const removed: string[] = []; - for (const [identifier, value] of this._providers) { - if (!this._vendors.has(value.metadata.vendor)) { - this._providers.delete(identifier); - removed.push(identifier); - } - } - if (removed.length > 0) { - this._onDidChangeProviders.fire({ removed }); - } - })); - } - - dispose() { - this._store.dispose(); - this._providers.clear(); - } - - getLanguageModelIds(): string[] { - return Array.from(this._providers.keys()); - } - - lookupLanguageModel(identifier: string): ILanguageModelChatMetadata | undefined { - return this._providers.get(identifier)?.metadata; - } - - async selectLanguageModels(selector: ILanguageModelChatSelector): Promise { - - if (selector.vendor) { - // selective activation - await this._extensionService.activateByEvent(`onLanguageModelChat:${selector.vendor}}`); - } else { - // activate all extensions that do language models - const all = Array.from(this._vendors).map(vendor => this._extensionService.activateByEvent(`onLanguageModelChat:${vendor}`)); - await Promise.all(all); - } - - const result: string[] = []; - - for (const [identifier, model] of this._providers) { - - if ((selector.vendor === undefined || model.metadata.vendor === selector.vendor) - && (selector.family === undefined || model.metadata.family === selector.family) - && (selector.version === undefined || model.metadata.version === selector.version) - && (selector.identifier === undefined || model.metadata.id === selector.identifier) - && (!model.metadata.targetExtensions || model.metadata.targetExtensions.some(candidate => ExtensionIdentifier.equals(candidate, selector.extension))) - ) { - result.push(identifier); - } - } - - this._logService.trace('[LM] selected language models', selector, result); - - return result; - } - - registerLanguageModelChat(identifier: string, provider: ILanguageModelChat): IDisposable { - - this._logService.trace('[LM] registering language model chat', identifier, provider.metadata); - - if (!this._vendors.has(provider.metadata.vendor)) { - throw new Error(`Chat response provider uses UNKNOWN vendor ${provider.metadata.vendor}.`); - } - if (this._providers.has(identifier)) { - throw new Error(`Chat response provider with identifier ${identifier} is already registered.`); - } - this._providers.set(identifier, provider); - this._onDidChangeProviders.fire({ added: [{ identifier, metadata: provider.metadata }] }); - return toDisposable(() => { - if (this._providers.delete(identifier)) { - this._onDidChangeProviders.fire({ removed: [identifier] }); - this._logService.trace('[LM] UNregistered language model chat', identifier, provider.metadata); - } - }); - } - - async sendChatRequest(identifier: string, from: ExtensionIdentifier, messages: IAideChatMessage[], options: { [name: string]: any }, token: CancellationToken): Promise { - const provider = this._providers.get(identifier); - if (!provider) { - throw new Error(`Chat response provider with identifier ${identifier} is not registered.`); - } - return provider.sendChatRequest(messages, from, options, token); - } - - computeTokenLength(identifier: string, message: string | IAideChatMessage, token: CancellationToken): Promise { - const provider = this._providers.get(identifier); - if (!provider) { - throw new Error(`Chat response provider with identifier ${identifier} is not registered.`); - } - return provider.provideTokenCount(message, token); - } -} diff --git a/src/vs/workbench/contrib/aideChat/common/tools/languageModelToolsContribution.ts b/src/vs/workbench/contrib/aideChat/common/tools/languageModelToolsContribution.ts deleted file mode 100644 index 01cdca4c2f7..00000000000 --- a/src/vs/workbench/contrib/aideChat/common/tools/languageModelToolsContribution.ts +++ /dev/null @@ -1,94 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - - -import { IJSONSchema } from '../../../../../base/common/jsonSchema.js'; -import { DisposableMap } from '../../../../../base/common/lifecycle.js'; -import { localize } from '../../../../../nls.js'; -import { ExtensionIdentifier } from '../../../../../platform/extensions/common/extensions.js'; -import { ILogService } from '../../../../../platform/log/common/log.js'; -import { IWorkbenchContribution } from '../../../../../workbench/common/contributions.js'; -import { ILanguageModelToolsService } from '../../../../../workbench/contrib/aideChat/common/languageModelToolsService.js'; -import * as extensionsRegistry from '../../../../../workbench/services/extensions/common/extensionsRegistry.js'; - -interface IRawToolContribution { - name: string; - displayName?: string; - description: string; - parametersSchema?: IJSONSchema; -} - -const languageModelToolsExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint({ - extensionPoint: 'aiModelTools', - activationEventsGenerator: (contributions: IRawToolContribution[], result) => { - for (const contrib of contributions) { - result.push(`onAIModelTool:${contrib.name}`); - } - }, - jsonSchema: { - description: localize('vscode.extension.contributes.tools', 'Contributes a tool that can be invoked by a language model.'), - type: 'array', - items: { - additionalProperties: false, - type: 'object', - defaultSnippets: [{ body: { name: '', description: '' } }], - required: ['name', 'description'], - properties: { - name: { - description: localize('toolname', "A name for this tool which must be unique across all tools."), - type: 'string' - }, - description: { - description: localize('toolDescription', "A description of this tool that may be passed to a language model."), - type: 'string' - }, - displayName: { - description: localize('toolDisplayName', "A human-readable name for this tool that may be used to describe it in the UI."), - type: 'string' - }, - parametersSchema: { - description: localize('parametersSchema', "A JSON schema for the parameters this tool accepts."), - type: 'object', - $ref: 'http://json-schema.org/draft-07/schema#' - } - } - } - } -}); - -function toToolKey(extensionIdentifier: ExtensionIdentifier, toolName: string) { - return `${extensionIdentifier.value}/${toolName}`; -} - -export class LanguageModelToolsExtensionPointHandler implements IWorkbenchContribution { - static readonly ID = 'workbench.contrib.aideToolsExtensionPointHandler'; - - private _registrationDisposables = new DisposableMap(); - - constructor( - @ILanguageModelToolsService languageModelToolsService: ILanguageModelToolsService, - @ILogService logService: ILogService, - ) { - languageModelToolsExtensionPoint.setHandler((extensions, delta) => { - for (const extension of delta.added) { - for (const tool of extension.value) { - if (!tool.name || !tool.description) { - logService.warn(`Invalid tool contribution from ${extension.description.identifier.value}: ${JSON.stringify(tool)}`); - continue; - } - - const disposable = languageModelToolsService.registerToolData(tool); - this._registrationDisposables.set(toToolKey(extension.description.identifier, tool.name), disposable); - } - } - - for (const extension of delta.removed) { - for (const tool of extension.value) { - this._registrationDisposables.deleteAndDispose(toToolKey(extension.description.identifier, tool.name)); - } - } - }); - } -} diff --git a/src/vs/workbench/contrib/aideChat/common/voiceChatService.ts b/src/vs/workbench/contrib/aideChat/common/voiceChatService.ts deleted file mode 100644 index 0a419bdea66..00000000000 --- a/src/vs/workbench/contrib/aideChat/common/voiceChatService.ts +++ /dev/null @@ -1,249 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { localize } from '../../../../nls.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js'; -import { rtrim } from '../../../../base/common/strings.js'; -import { IContextKeyService, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IAideChatAgentService } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { IChatModel } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { chatAgentLeader, chatSubcommandLeader } from '../../../../workbench/contrib/aideChat/common/aideChatParserTypes.js'; -import { ISpeechService, ISpeechToTextEvent, SpeechToTextStatus } from '../../../../workbench/contrib/speech/common/speechService.js'; - -export const IVoiceChatService = createDecorator('voiceChatService'); - -export interface IVoiceChatSessionOptions { - readonly usesAgents?: boolean; - readonly model?: IChatModel; -} - -export interface IVoiceChatService { - - readonly _serviceBrand: undefined; - - /** - * Similar to `ISpeechService.createSpeechToTextSession`, but with - * support for agent prefixes and command prefixes. For example, - * if the user says "at workspace slash fix this problem", the result - * will be "@workspace /fix this problem". - */ - createVoiceChatSession(token: CancellationToken, options: IVoiceChatSessionOptions): Promise; -} - -export interface IVoiceChatTextEvent extends ISpeechToTextEvent { - - /** - * This property will be `true` when the text recognized - * so far only consists of agent prefixes (`@workspace`) - * and/or command prefixes (`@workspace /fix`). - */ - readonly waitingForInput?: boolean; -} - -export interface IVoiceChatSession { - readonly onDidChange: Event; -} - -interface IPhraseValue { - readonly agent: string; - readonly command?: string; -} - -enum PhraseTextType { - AGENT = 1, - COMMAND = 2, - AGENT_AND_COMMAND = 3 -} - -export const VoiceChatInProgress = new RawContextKey('voiceChatInProgress', false, { type: 'boolean', description: localize('voiceChatInProgress', "A speech-to-text session is in progress for chat.") }); - -export class VoiceChatService extends Disposable implements IVoiceChatService { - - readonly _serviceBrand: undefined; - - private static readonly AGENT_PREFIX = chatAgentLeader; - private static readonly COMMAND_PREFIX = chatSubcommandLeader; - - private static readonly PHRASES_LOWER = { - [this.AGENT_PREFIX]: 'at', - [this.COMMAND_PREFIX]: 'slash' - }; - - private static readonly PHRASES_UPPER = { - [this.AGENT_PREFIX]: 'At', - [this.COMMAND_PREFIX]: 'Slash' - }; - - private static readonly CHAT_AGENT_ALIAS = new Map([['vscode', 'code']]); - - private readonly voiceChatInProgress = VoiceChatInProgress.bindTo(this.contextKeyService); - private activeVoiceChatSessions = 0; - - constructor( - @ISpeechService private readonly speechService: ISpeechService, - @IAideChatAgentService private readonly chatAgentService: IAideChatAgentService, - @IContextKeyService private readonly contextKeyService: IContextKeyService - ) { - super(); - } - - private createPhrases(model?: IChatModel): Map { - const phrases = new Map(); - - for (const agent of this.chatAgentService.getActivatedAgents()) { - const agentPhrase = `${VoiceChatService.PHRASES_LOWER[VoiceChatService.AGENT_PREFIX]} ${VoiceChatService.CHAT_AGENT_ALIAS.get(agent.name) ?? agent.name}`.toLowerCase(); - phrases.set(agentPhrase, { agent: agent.name }); - - for (const slashCommand of agent.slashCommands) { - const slashCommandPhrase = `${VoiceChatService.PHRASES_LOWER[VoiceChatService.COMMAND_PREFIX]} ${slashCommand.name}`.toLowerCase(); - phrases.set(slashCommandPhrase, { agent: agent.name, command: slashCommand.name }); - - const agentSlashCommandPhrase = `${agentPhrase} ${slashCommandPhrase}`.toLowerCase(); - phrases.set(agentSlashCommandPhrase, { agent: agent.name, command: slashCommand.name }); - } - } - - return phrases; - } - - private toText(value: IPhraseValue, type: PhraseTextType): string { - switch (type) { - case PhraseTextType.AGENT: - return `${VoiceChatService.AGENT_PREFIX}${value.agent}`; - case PhraseTextType.COMMAND: - return `${VoiceChatService.COMMAND_PREFIX}${value.command}`; - case PhraseTextType.AGENT_AND_COMMAND: - return `${VoiceChatService.AGENT_PREFIX}${value.agent} ${VoiceChatService.COMMAND_PREFIX}${value.command}`; - } - } - - async createVoiceChatSession(token: CancellationToken, options: IVoiceChatSessionOptions): Promise { - const disposables = new DisposableStore(); - - const onSessionStoppedOrCanceled = (dispose: boolean) => { - this.activeVoiceChatSessions = Math.max(0, this.activeVoiceChatSessions - 1); - if (this.activeVoiceChatSessions === 0) { - this.voiceChatInProgress.reset(); - } - - if (dispose) { - disposables.dispose(); - } - }; - - disposables.add(token.onCancellationRequested(() => onSessionStoppedOrCanceled(true))); - - let detectedAgent = false; - let detectedSlashCommand = false; - - const emitter = disposables.add(new Emitter()); - const session = await this.speechService.createSpeechToTextSession(token, 'chat'); - - if (token.isCancellationRequested) { - onSessionStoppedOrCanceled(true); - } - - const phrases = this.createPhrases(options.model); - disposables.add(session.onDidChange(e => { - switch (e.status) { - case SpeechToTextStatus.Recognizing: - case SpeechToTextStatus.Recognized: { - let massagedEvent: IVoiceChatTextEvent = e; - if (e.text) { - const startsWithAgent = e.text.startsWith(VoiceChatService.PHRASES_UPPER[VoiceChatService.AGENT_PREFIX]) || e.text.startsWith(VoiceChatService.PHRASES_LOWER[VoiceChatService.AGENT_PREFIX]); - const startsWithSlashCommand = e.text.startsWith(VoiceChatService.PHRASES_UPPER[VoiceChatService.COMMAND_PREFIX]) || e.text.startsWith(VoiceChatService.PHRASES_LOWER[VoiceChatService.COMMAND_PREFIX]); - if (startsWithAgent || startsWithSlashCommand) { - const originalWords = e.text.split(' '); - let transformedWords: string[] | undefined; - - let waitingForInput = false; - - // Check for agent + slash command - if (options.usesAgents && startsWithAgent && !detectedAgent && !detectedSlashCommand && originalWords.length >= 4) { - const phrase = phrases.get(originalWords.slice(0, 4).map(word => this.normalizeWord(word)).join(' ')); - if (phrase) { - transformedWords = [this.toText(phrase, PhraseTextType.AGENT_AND_COMMAND), ...originalWords.slice(4)]; - - waitingForInput = originalWords.length === 4; - - if (e.status === SpeechToTextStatus.Recognized) { - detectedAgent = true; - detectedSlashCommand = true; - } - } - } - - // Check for agent (if not done already) - if (options.usesAgents && startsWithAgent && !detectedAgent && !transformedWords && originalWords.length >= 2) { - const phrase = phrases.get(originalWords.slice(0, 2).map(word => this.normalizeWord(word)).join(' ')); - if (phrase) { - transformedWords = [this.toText(phrase, PhraseTextType.AGENT), ...originalWords.slice(2)]; - - waitingForInput = originalWords.length === 2; - - if (e.status === SpeechToTextStatus.Recognized) { - detectedAgent = true; - } - } - } - - // Check for slash command (if not done already) - if (startsWithSlashCommand && !detectedSlashCommand && !transformedWords && originalWords.length >= 2) { - const phrase = phrases.get(originalWords.slice(0, 2).map(word => this.normalizeWord(word)).join(' ')); - if (phrase) { - transformedWords = [this.toText(phrase, options.usesAgents && !detectedAgent ? - PhraseTextType.AGENT_AND_COMMAND : // rewrite `/fix` to `@workspace /foo` in this case - PhraseTextType.COMMAND // when we have not yet detected an agent before - ), ...originalWords.slice(2)]; - - waitingForInput = originalWords.length === 2; - - if (e.status === SpeechToTextStatus.Recognized) { - detectedSlashCommand = true; - } - } - } - - massagedEvent = { - status: e.status, - text: (transformedWords ?? originalWords).join(' '), - waitingForInput - }; - } - } - emitter.fire(massagedEvent); - break; - } - case SpeechToTextStatus.Started: - this.activeVoiceChatSessions++; - this.voiceChatInProgress.set(true); - emitter.fire(e); - break; - case SpeechToTextStatus.Stopped: - onSessionStoppedOrCanceled(false); - emitter.fire(e); - break; - case SpeechToTextStatus.Error: - emitter.fire(e); - break; - } - })); - - return { - onDidChange: emitter.event - }; - } - - private normalizeWord(word: string): string { - word = rtrim(word, '.'); - word = rtrim(word, ','); - word = rtrim(word, '?'); - - return word.toLowerCase(); - } -} diff --git a/src/vs/workbench/contrib/aideProbe/browser/aideContextActions.ts b/src/vs/workbench/contrib/aideProbe/browser/aideContextActions.ts index 0e0a906023f..387a1a36107 100644 --- a/src/vs/workbench/contrib/aideProbe/browser/aideContextActions.ts +++ b/src/vs/workbench/contrib/aideProbe/browser/aideContextActions.ts @@ -27,7 +27,8 @@ import { getWorkbenchContribution } from '../../../../workbench/common/contribut import { ContextPicker, IContextPicker } from '../../../../workbench/contrib/aideProbe/browser/aideContextPicker.js'; import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IAideControlsService } from '../../../../workbench/contrib/aideProbe/browser/aideControls.js'; -import { IAideChatVariablesService } from '../../../../workbench/contrib/aideChat/common/aideChatVariables.js'; +import { IChatVariablesService } from '../../chat/common/chatVariables.js'; +import { ChatAgentLocation } from '../../chat/common/chatAgents.js'; const AIDE_CONTEXT_CATEGORY = localize2('chat.category', 'Chat'); @@ -185,14 +186,14 @@ export class AttachContextAction extends Action2 { override async run(accessor: ServicesAccessor, ...args: any[]): Promise { const quickInputService = accessor.get(IQuickInputService); - const chatVariablesService = accessor.get(IAideChatVariablesService); + const chatVariablesService = accessor.get(IChatVariablesService); const commandService = accessor.get(ICommandService); const contextKeyService = accessor.get(IContextKeyService); CONTEXT_PROBE_CONTEXT_TYPE.bindTo(contextKeyService).set('specific'); const quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[] = []; - for (const variable of chatVariablesService.getVariables()) { + for (const variable of chatVariablesService.getVariables(ChatAgentLocation.Panel)) { if (variable.fullName && !variable.isSlow) { quickPickItems.push({ label: `${variable.icon ? `$(${variable.icon.id}) ` : ''}${variable.fullName}`, diff --git a/src/vs/workbench/contrib/aideProbe/browser/aideControls.ts b/src/vs/workbench/contrib/aideProbe/browser/aideControls.ts index f9cbd8f850f..cf3f5eb246d 100644 --- a/src/vs/workbench/contrib/aideProbe/browser/aideControls.ts +++ b/src/vs/workbench/contrib/aideProbe/browser/aideControls.ts @@ -49,12 +49,12 @@ import { AideProbeModel, IVariableEntry } from '../../../../workbench/contrib/ai import { IAideProbeService } from '../../../../workbench/contrib/aideProbe/browser/aideProbeService.js'; import { AideProbeMode, AideProbeStatus, AnchorEditingSelection, IAideProbeMode, IAideProbeStatus } from '../../../../workbench/contrib/aideProbe/common/aideProbe.js'; import { IParsedChatRequest } from '../../../../workbench/contrib/aideProbe/common/aideProbeParserTypes.js'; -import { ChatRequestParser } from '../../../../workbench/contrib/aideProbe/common/aideProbeRequestParser.js'; import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions } from '../../../../workbench/contrib/codeEditor/browser/simpleEditorOptions.js'; import { IAideControlsPartService } from '../../../../workbench/services/aideControlsPart/browser/aideControlsPartService.js'; import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; import { IOutline, IOutlineService, OutlineTarget } from '../../../../workbench/services/outline/browser/outline.js'; import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; +import { ChatRequestParser } from '../../chat/common/chatRequestParser.js'; import './media/aideControls.css'; const $ = dom.$; @@ -171,7 +171,7 @@ export class AideControls extends Themable implements IAideControls { private parsedChatRequest: IParsedChatRequest | undefined; get parsedInput() { if (this.parsedChatRequest === undefined) { - this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest(this.inputEditor.getValue()); + this.parsedChatRequest = this.instantiationService.createInstance(ChatRequestParser).parseChatRequest('', this.inputEditor.getValue()); } return this.parsedChatRequest; diff --git a/src/vs/workbench/contrib/aideProbe/browser/aideFollowupReferencesContentPart.ts b/src/vs/workbench/contrib/aideProbe/browser/aideFollowupReferencesContentPart.ts index e5f985b5bff..7271dda375b 100644 --- a/src/vs/workbench/contrib/aideProbe/browser/aideFollowupReferencesContentPart.ts +++ b/src/vs/workbench/contrib/aideProbe/browser/aideFollowupReferencesContentPart.ts @@ -9,34 +9,29 @@ import { IListRenderer, IListVirtualDelegate } from '../../../../base/browser/ui import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; -import { matchesSomeScheme, Schemas } from '../../../../base/common/network.js'; import { basename } from '../../../../base/common/path.js'; -import { basenameOrAuthority } from '../../../../base/common/resources.js'; import { ThemeIcon } from '../../../../base/common/themables.js'; import { isDefined } from '../../../../base/common/types.js'; import { URI } from '../../../../base/common/uri.js'; import { localize } from '../../../../nls.js'; -import { FileKind } from '../../../../platform/files/common/files.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { WorkbenchList } from '../../../../platform/list/browser/listService.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IThemeService } from '../../../../platform/theme/common/themeService.js'; import { Heroicon } from '../../../../workbench/browser/heroicon.js'; import { IResourceLabel, ResourceLabels } from '../../../../workbench/browser/labels.js'; -import { ColorScheme } from '../../../../workbench/browser/web.api.js'; -import { IAideChatContentReference, IAideChatWarningMessage } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { IAideChatVariablesService } from '../../../../workbench/contrib/aideChat/common/aideChatVariables.js'; import { IFollowupState } from '../../../../workbench/contrib/aideProbe/browser/aideProbeViewModel.js'; import { createFileIconThemableTreeContainerScope } from '../../../../workbench/contrib/files/browser/views/explorerView.js'; +import { IChatContentReference, IChatWarningMessage } from '../../chat/common/chatService.js'; const $ = dom.$; -export interface IAideFollowupContentReference extends Omit { +export interface IAideFollowupContentReference extends Omit { kind: 'followup-reference'; state: IFollowupState; } -export interface IAideReferenceFoundContentReference extends Omit { +export interface IAideReferenceFoundContentReference extends Omit { kind: 'found-reference'; occurencies: number; } @@ -51,7 +46,7 @@ export class AideReferencesContentPart extends Disposable { private collapseButtonElement: HTMLElement; constructor( - data: ReadonlyArray, + data: ReadonlyArray, label: string, expanded: boolean, private readonly onDidChangeVisibility: Event, @@ -102,7 +97,7 @@ export class AideReferencesContentPart extends Disposable { const renderer = this.instantiationService.createInstance(ContentReferencesListRenderer, resourceLabels); const list = this.instantiationService.createInstance( - WorkbenchList, + WorkbenchList, 'ChatListRenderer', container, new ContentReferencesListDelegate(), @@ -110,35 +105,31 @@ export class AideReferencesContentPart extends Disposable { { alwaysConsumeMouseWheel: false, accessibilityProvider: { - getAriaLabel: (element: IAideFollowupContentReference | IAideReferenceFoundContentReference | IAideChatWarningMessage) => { + getAriaLabel: (element: IAideFollowupContentReference | IAideReferenceFoundContentReference | IChatWarningMessage) => { if (element.kind === 'warning') { return element.content.value; } const reference = element.reference; - if ('variableName' in reference) { - return reference.variableName; - } else if (URI.isUri(reference)) { + if (URI.isUri(reference)) { return basename(reference.path); - } else { - return basename(reference.uri.path); } + + return null; }, getWidgetAriaLabel: () => localize('usedReferences', "Used References") }, dnd: { - getDragURI: (element: IAideFollowupContentReference | IAideReferenceFoundContentReference | IAideChatWarningMessage) => { + getDragURI: (element: IAideFollowupContentReference | IAideReferenceFoundContentReference | IChatWarningMessage) => { if (element.kind === 'warning') { return null; } const { reference } = element; - if ('variableName' in reference) { - return null; - } else if (URI.isUri(reference)) { + if (URI.isUri(reference)) { return reference.toString(); - } else { - return reference.uri.toString(); } + + return null; }, dispose: () => { }, onDragOver: () => false, @@ -157,7 +148,7 @@ export class AideReferencesContentPart extends Disposable { if (e.browserEvent) { e.browserEvent.preventDefault(); } - if (e.element && 'reference' in e.element) { + /* if (e.element && 'reference' in e.element) { const uriOrLocation = 'variableName' in e.element.reference ? e.element.reference.value : e.element.reference; const uri = URI.isUri(uriOrLocation) ? uriOrLocation : uriOrLocation?.uri; @@ -174,7 +165,7 @@ export class AideReferencesContentPart extends Disposable { } }); } - } + } */ })); this._register(list.onContextMenu((e) => { e.browserEvent.preventDefault(); @@ -215,7 +206,7 @@ export class AideReferencesContentPart extends Disposable { } -class ContentReferencesListDelegate implements IListVirtualDelegate { +class ContentReferencesListDelegate implements IListVirtualDelegate { getHeight(element: IAideFollowupContentReference): number { return 22; } @@ -231,14 +222,12 @@ interface IChatContentReferenceListTemplate { templateDisposables: DisposableStore; } -class ContentReferencesListRenderer implements IListRenderer { +class ContentReferencesListRenderer implements IListRenderer { static TEMPLATE_ID = 'contentReferencesListRenderer'; readonly templateId: string = ContentReferencesListRenderer.TEMPLATE_ID; constructor( private labels: ResourceLabels, - @IThemeService private readonly themeService: IThemeService, - @IAideChatVariablesService private readonly chatVariablesService: IAideChatVariablesService, @IInstantiationService private readonly instantiationService: IInstantiationService ) { } @@ -249,6 +238,7 @@ class ContentReferencesListRenderer implements IListRenderer('IAideProbeExplanationService'); diff --git a/src/vs/workbench/contrib/aideProbe/browser/aideProbeModel.ts b/src/vs/workbench/contrib/aideProbe/browser/aideProbeModel.ts index c9237e830ff..11657f916da 100644 --- a/src/vs/workbench/contrib/aideProbe/browser/aideProbeModel.ts +++ b/src/vs/workbench/contrib/aideProbe/browser/aideProbeModel.ts @@ -24,12 +24,12 @@ import { ITextModelService } from '../../../../editor/common/services/resolverSe import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; import { IUndoRedoService } from '../../../../platform/undoRedo/common/undoRedo.js'; -import { IChatRequestVariableData, IChatTextEditGroupState } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; import { CONTEXT_PROBE_REQUEST_STATUS } from '../../../../workbench/contrib/aideProbe/browser/aideProbeContextKeys.js'; import { AideProbeStatus, IAideFollowupInformation, IAideProbeBreakdownContent, IAideProbeGoToDefinition, IAideProbeInitialSymbolInformation, IAideProbeInitialSymbols, IAideProbeMode, IAideProbeProgress, IAideProbeRequestModel, IAideProbeResponseEvent, IAideProbeStatus, IAideProbeTextEdit, IAideRelevantReference, IAideRelevantReferenceInformation } from '../../../../workbench/contrib/aideProbe/common/aideProbe.js'; import { HunkData } from '../../../../workbench/contrib/inlineChat/browser/inlineChatSession.js'; import { ITextFileService } from '../../../../workbench/services/textfile/common/textfiles.js'; +import { IChatRequestVariableData, IChatTextEditGroupState } from '../../chat/common/chatModel.js'; export interface IContentVariableReference { variableName: string; diff --git a/src/vs/workbench/contrib/aideProbe/browser/aideProbeView.ts b/src/vs/workbench/contrib/aideProbe/browser/aideProbeView.ts index 433be1ff890..1120a5e32c1 100644 --- a/src/vs/workbench/contrib/aideProbe/browser/aideProbeView.ts +++ b/src/vs/workbench/contrib/aideProbe/browser/aideProbeView.ts @@ -36,7 +36,6 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/ import { Heroicon } from '../../../../workbench/browser/heroicon.js'; import { IViewPaneOptions, ViewPane } from '../../../../workbench/browser/parts/views/viewPane.js'; import { IViewDescriptorService } from '../../../../workbench/common/views.js'; -import { ChatMarkdownRenderer } from '../../../../workbench/contrib/aideChat/browser/aideChatMarkdownRenderer.js'; import { AideReferencesContentPart, IAideFollowupContentReference, IAideReferenceFoundContentReference } from '../../../../workbench/contrib/aideProbe/browser/aideFollowupReferencesContentPart.js'; import { CONTEXT_PROBE_MODE } from '../../../../workbench/contrib/aideProbe/browser/aideProbeContextKeys.js'; import { IAideProbeExplanationService } from '../../../../workbench/contrib/aideProbe/browser/aideProbeExplanations.js'; @@ -46,6 +45,7 @@ import { IAideProbeListItem, AideProbeViewModel, IAideProbeBreakdownViewModel, I import { AideProbeMode, AideProbeStatus } from '../../../../workbench/contrib/aideProbe/common/aideProbe.js'; import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; import { defaultButtonStyles } from '../../../../platform/theme/browser/defaultStyles.js'; +import { ChatMarkdownRenderer } from '../../chat/browser/chatMarkdownRenderer.js'; const $ = dom.$; diff --git a/src/vs/workbench/contrib/aideProbe/browser/contrib/aideControlsDynamicVariables.ts b/src/vs/workbench/contrib/aideProbe/browser/contrib/aideControlsDynamicVariables.ts index 23a206ca0fe..a2bbdd7e9a1 100644 --- a/src/vs/workbench/contrib/aideProbe/browser/contrib/aideControlsDynamicVariables.ts +++ b/src/vs/workbench/contrib/aideProbe/browser/contrib/aideControlsDynamicVariables.ts @@ -20,9 +20,9 @@ import { ServicesAccessor } from '../../../../../platform/instantiation/common/i import { ILabelService } from '../../../../../platform/label/common/label.js'; import { ILogService } from '../../../../../platform/log/common/log.js'; import { chatVariableLeader } from '../../../../../workbench/contrib/aideProbe/common/aideProbeParserTypes.js'; -import { IDynamicVariable } from '../../../../../workbench/contrib/aideChat/common/aideChatVariables.js'; import { AideControls, IAideControlsContrib } from '../../../../../workbench/contrib/aideProbe/browser/aideControls.js'; import { ISymbolQuickPickItem } from '../../../../../workbench/contrib/search/browser/symbolsQuickAccess.js'; +import { IDynamicVariable } from '../../../chat/common/chatVariables.js'; export const dynamicVariableDecorationType = 'chat-dynamic-variable'; diff --git a/src/vs/workbench/contrib/aideProbe/common/aideProbe.ts b/src/vs/workbench/contrib/aideProbe/common/aideProbe.ts index 99308e857c7..0cf4e1bcd32 100644 --- a/src/vs/workbench/contrib/aideProbe/common/aideProbe.ts +++ b/src/vs/workbench/contrib/aideProbe/common/aideProbe.ts @@ -5,13 +5,13 @@ import { IMarkdownString } from '../../../../base/common/htmlContent.js'; import { URI } from '../../../../base/common/uri.js'; -import { Selection } from '../../../../editor/common/core/selection.js'; import { Range } from '../../../../editor/common/core/range.js'; +import { Selection } from '../../../../editor/common/core/selection.js'; import { DocumentSymbol, TextEdit, WorkspaceEdit } from '../../../../editor/common/languages.js'; import { IValidEditOperation } from '../../../../editor/common/model.js'; import { IModelContentChange } from '../../../../editor/common/textModelEvents.js'; -import { IChatRequestVariableData } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { IAideChatMarkdownContent } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; +import { IChatRequestVariableData } from '../../chat/common/chatModel.js'; +import { IChatMarkdownContent } from '../../chat/common/chatService.js'; export interface IAideProbeData { id: string; @@ -132,7 +132,7 @@ export interface IAideFollowups { } export type IAideProbeProgress = - | IAideChatMarkdownContent + | IChatMarkdownContent | IAideProbeBreakdownContent | IAideProbeGoToDefinition | IAideProbeTextEdit diff --git a/src/vs/workbench/contrib/aideProbe/common/aideProbeParserTypes.ts b/src/vs/workbench/contrib/aideProbe/common/aideProbeParserTypes.ts index 0ed698cb44f..6fb1a5502d8 100644 --- a/src/vs/workbench/contrib/aideProbe/common/aideProbeParserTypes.ts +++ b/src/vs/workbench/contrib/aideProbe/common/aideProbeParserTypes.ts @@ -6,7 +6,7 @@ import { revive } from '../../../../base/common/marshalling.js'; import { IOffsetRange, OffsetRange } from '../../../../editor/common/core/offsetRange.js'; import { IRange } from '../../../../editor/common/core/range.js'; -import { IAideChatRequestVariableValue } from '../../../../workbench/contrib/aideChat/common/aideChatVariables.js'; +import { IChatRequestVariableValue } from '../../chat/common/chatVariables.js'; // These are in a separate file to avoid circular dependencies with the dependencies of the parser @@ -69,7 +69,7 @@ export class ChatRequestVariablePart implements IParsedChatRequestPart { export class ChatRequestDynamicVariablePart implements IParsedChatRequestPart { static readonly Kind = 'dynamic'; readonly kind = ChatRequestDynamicVariablePart.Kind; - constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly text: string, readonly id: string, readonly modelDescription: string | undefined, readonly data: IAideChatRequestVariableValue) { } + constructor(readonly range: OffsetRange, readonly editorRange: IRange, readonly text: string, readonly id: string, readonly modelDescription: string | undefined, readonly data: IChatRequestVariableValue) { } get referenceText(): string { return this.text.replace(chatVariableLeader, ''); diff --git a/src/vs/workbench/contrib/aideProbe/common/aideProbeRequestParser.ts b/src/vs/workbench/contrib/aideProbe/common/aideProbeRequestParser.ts deleted file mode 100644 index 3141ab9e97d..00000000000 --- a/src/vs/workbench/contrib/aideProbe/common/aideProbeRequestParser.ts +++ /dev/null @@ -1,94 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { OffsetRange } from '../../../../editor/common/core/offsetRange.js'; -import { IPosition, Position } from '../../../../editor/common/core/position.js'; -import { Range } from '../../../../editor/common/core/range.js'; -import { IAideChatVariablesService } from '../../../../workbench/contrib/aideChat/common/aideChatVariables.js'; -import { ChatRequestTextPart, ChatRequestVariablePart, IParsedChatRequest, IParsedChatRequestPart, chatVariableLeader } from '../../../../workbench/contrib/aideProbe/common/aideProbeParserTypes.js'; - -const variableReg = /^#([\w_\-]+)(:\d+)?(?=(\s|$|\b))/i; // A #-variable with an optional numeric : arg (@response:2) - -export class ChatRequestParser { - constructor( - @IAideChatVariablesService private readonly variableService: IAideChatVariablesService, - ) { } - - parseChatRequest(message: string): IParsedChatRequest { - const parts: IParsedChatRequestPart[] = []; - - let lineNumber = 1; - let column = 1; - for (let i = 0; i < message.length; i++) { - const previousChar = message.charAt(i - 1); - const char = message.charAt(i); - let newPart: IParsedChatRequestPart | undefined; - if (previousChar.match(/\s/) || i === 0) { - if (char === chatVariableLeader) { - newPart = this.tryToParseVariable(message.slice(i), i, new Position(lineNumber, column), parts); - } - } - - if (newPart) { - if (i !== 0) { - // Insert a part for all the text we passed over, then insert the new parsed part - const previousPart = parts.at(-1); - const previousPartEnd = previousPart?.range.endExclusive ?? 0; - const previousPartEditorRangeEndLine = previousPart?.editorRange.endLineNumber ?? 1; - const previousPartEditorRangeEndCol = previousPart?.editorRange.endColumn ?? 1; - parts.push(new ChatRequestTextPart( - new OffsetRange(previousPartEnd, i), - new Range(previousPartEditorRangeEndLine, previousPartEditorRangeEndCol, lineNumber, column), - message.slice(previousPartEnd, i))); - } - - parts.push(newPart); - } - - if (char === '\n') { - lineNumber++; - column = 1; - } else { - column++; - } - } - - const lastPart = parts.at(-1); - const lastPartEnd = lastPart?.range.endExclusive ?? 0; - if (lastPartEnd < message.length) { - parts.push(new ChatRequestTextPart( - new OffsetRange(lastPartEnd, message.length), - new Range(lastPart?.editorRange.endLineNumber ?? 1, lastPart?.editorRange.endColumn ?? 1, lineNumber, column), - message.slice(lastPartEnd, message.length))); - } - - return { - parts, - text: message, - }; - } - - private tryToParseVariable(message: string, offset: number, position: IPosition, parts: ReadonlyArray): ChatRequestVariablePart | undefined { - const nextVariableMatch = message.match(variableReg); - if (!nextVariableMatch) { - return; - } - - const [full, name] = nextVariableMatch; - const variableArg = nextVariableMatch[2] ?? ''; - const varRange = new OffsetRange(offset, offset + full.length); - const varEditorRange = new Range(position.lineNumber, position.column, position.lineNumber, position.column + full.length); - - - - // TODO - not really handling duplicate variables names yet - const variable = this.variableService.getVariable(name); - if (variable && (!variable.isSlow)) { - return new ChatRequestVariablePart(varRange, varEditorRange, name, variableArg, variable.id); - } - - return; - } -} diff --git a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChat.contribution.ts b/src/vs/workbench/contrib/inlineAideChat/browser/inlineChat.contribution.ts deleted file mode 100644 index f3028b5b03d..00000000000 --- a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChat.contribution.ts +++ /dev/null @@ -1,133 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { EditorContributionInstantiation, registerEditorContribution } from '../../../../editor/browser/editorExtensions.js'; -import { IMenuItem, isIMenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; -import { InlineChatController } from '../../../../workbench/contrib/inlineAideChat/browser/inlineChatController.js'; -import * as InlineChatActions from '../../../../workbench/contrib/inlineAideChat/browser/inlineChatActions.js'; -import { CTX_INLINE_CHAT_CONFIG_TXT_BTNS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, INLINE_CHAT_ID, InlineChatConfigKeys, MENU_INLINE_CHAT_CONTENT_STATUS, MENU_INLINE_CHAT_EXECUTE, MENU_INLINE_CHAT_WIDGET_STATUS } from '../../../../workbench/contrib/inlineAideChat/common/inlineChat.js'; -import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { Registry } from '../../../../platform/registry/common/platform.js'; -import { LifecyclePhase } from '../../../../workbench/services/lifecycle/common/lifecycle.js'; -import { InlineChatNotebookContribution } from '../../../../workbench/contrib/inlineAideChat/browser/inlineChatNotebook.js'; -import { IWorkbenchContributionsRegistry, registerWorkbenchContribution2, Extensions as WorkbenchExtensions, WorkbenchPhase } from '../../../../workbench/common/contributions.js'; -import { InlineChatSavingServiceImpl } from '../../../../workbench/contrib/inlineAideChat/browser/inlineChatSavingServiceImpl.js'; -import { IInlineAideChatSavingService } from '../../../../workbench/contrib/inlineAideChat/browser/inlineChatSavingService.js'; -import { IInlineAideChatSessionService } from '../../../../workbench/contrib/inlineAideChat/browser/inlineChatSessionService.js'; -import { InlineChatEnabler, InlineChatSessionServiceImpl } from '../../../../workbench/contrib/inlineAideChat/browser/inlineChatSessionServiceImpl.js'; -import { CancelAction, SubmitAction } from '../../../../workbench/contrib/aideChat/browser/actions/aideChatExecuteActions.js'; -import { localize } from '../../../../nls.js'; -import { CONTEXT_CHAT_INPUT_HAS_TEXT } from '../../../../workbench/contrib/aideChat/common/aideChatContextKeys.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; - - -// --- browser - -registerSingleton(IInlineAideChatSessionService, InlineChatSessionServiceImpl, InstantiationType.Delayed); -registerSingleton(IInlineAideChatSavingService, InlineChatSavingServiceImpl, InstantiationType.Delayed); - -registerEditorContribution(INLINE_CHAT_ID, InlineChatController, EditorContributionInstantiation.Eager); // EAGER because of notebook dispose/create of editors - -// --- MENU special --- - -const sendActionMenuItem: IMenuItem = { - group: '0_main', - order: 0, - command: { - id: SubmitAction.ID, - title: localize('edit', "Send"), - }, - when: ContextKeyExpr.and( - CONTEXT_CHAT_INPUT_HAS_TEXT, - CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.toNegated(), - CTX_INLINE_CHAT_CONFIG_TXT_BTNS - ), -}; - -MenuRegistry.appendMenuItem(MENU_INLINE_CHAT_CONTENT_STATUS, sendActionMenuItem); -MenuRegistry.appendMenuItem(MENU_INLINE_CHAT_WIDGET_STATUS, sendActionMenuItem); - -const cancelActionMenuItem: IMenuItem = { - group: '0_main', - order: 0, - command: { - id: CancelAction.ID, - title: localize('cancel', "Cancel Request"), - shortTitle: localize('cancelShort', "Cancel"), - }, - when: ContextKeyExpr.and( - CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, - ), -}; - -MenuRegistry.appendMenuItem(MENU_INLINE_CHAT_WIDGET_STATUS, cancelActionMenuItem); - -// --- actions --- - -registerAction2(InlineChatActions.StartSessionAction); -registerAction2(InlineChatActions.CloseAction); -registerAction2(InlineChatActions.ConfigureInlineChatAction); -registerAction2(InlineChatActions.UnstashSessionAction); -registerAction2(InlineChatActions.DiscardHunkAction); -registerAction2(InlineChatActions.DiscardAction); -registerAction2(InlineChatActions.RerunAction); -registerAction2(InlineChatActions.MoveToNextHunk); -registerAction2(InlineChatActions.MoveToPreviousHunk); - -registerAction2(InlineChatActions.ArrowOutUpAction); -registerAction2(InlineChatActions.ArrowOutDownAction); -registerAction2(InlineChatActions.FocusInlineChat); -registerAction2(InlineChatActions.ViewInChatAction); - -registerAction2(InlineChatActions.ToggleDiffForChange); -registerAction2(InlineChatActions.AcceptChanges); - -registerAction2(InlineChatActions.CopyRecordings); - -const workbenchContributionsRegistry = Registry.as(WorkbenchExtensions.Workbench); -workbenchContributionsRegistry.registerWorkbenchContribution(InlineChatNotebookContribution, LifecyclePhase.Restored); - -registerWorkbenchContribution2(InlineChatEnabler.Id, InlineChatEnabler, WorkbenchPhase.AfterRestored); - -// MARK - Menu Copier -// menu copier that we use for text-button mode. -// When active it filters out the send and cancel actions from the chat menu -class MenuCopier implements IDisposable { - - static Id = 'inlineAideChat.menuCopier'; - - readonly dispose: () => void; - - constructor(@IConfigurationService configService: IConfigurationService,) { - - const store = new DisposableStore(); - function updateMenu() { - if (configService.getValue(InlineChatConfigKeys.ExpTextButtons)) { - store.clear(); - for (const item of MenuRegistry.getMenuItems(MenuId.AideChatExecute)) { - if (isIMenuItem(item) && (item.command.id === SubmitAction.ID || item.command.id === CancelAction.ID)) { - continue; - } - store.add(MenuRegistry.appendMenuItem(MENU_INLINE_CHAT_EXECUTE, item)); - } - } - } - updateMenu(); - const listener = MenuRegistry.onDidChangeMenu(e => { - if (e.has(MenuId.AideChatExecute)) { - updateMenu(); - } - }); - - this.dispose = () => { - listener.dispose(); - store.dispose(); - }; - } -} - -registerWorkbenchContribution2(MenuCopier.Id, MenuCopier, WorkbenchPhase.AfterRestored); diff --git a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatActions.ts b/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatActions.ts deleted file mode 100644 index 841c44165ff..00000000000 --- a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatActions.ts +++ /dev/null @@ -1,538 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Codicon } from '../../../../base/common/codicons.js'; -import { KeyChord, KeyCode, KeyMod } from '../../../../base/common/keyCodes.js'; -import { ICodeEditor, isCodeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js'; -import { EditorAction2 } from '../../../../editor/browser/editorExtensions.js'; -import { EmbeddedDiffEditorWidget } from '../../../../editor/browser/widget/diffEditor/embeddedDiffEditorWidget.js'; -import { EmbeddedCodeEditorWidget } from '../../../../editor/browser/widget/codeEditor/embeddedCodeEditorWidget.js'; -import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; -import { InlineChatController, InlineChatRunOptions } from '../../../../workbench/contrib/inlineAideChat/browser/inlineChatController.js'; -import { ACTION_ACCEPT_CHANGES, CTX_INLINE_CHAT_HAS_AGENT, CTX_INLINE_CHAT_HAS_STASHED_SESSION, CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, CTX_INLINE_CHAT_INNER_CURSOR_LAST, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_DOCUMENT_CHANGED, CTX_INLINE_CHAT_EDIT_MODE, EditMode, MENU_INLINE_CHAT_WIDGET_STATUS, CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatResponseType, ACTION_REGENERATE_RESPONSE, MENU_INLINE_CHAT_CONTENT_STATUS, ACTION_VIEW_IN_CHAT, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF } from '../../../../workbench/contrib/inlineAideChat/common/inlineChat.js'; -import { localize, localize2 } from '../../../../nls.js'; -import { Action2, IAction2Options } from '../../../../platform/actions/common/actions.js'; -import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js'; -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; -import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js'; -import { IQuickInputService, IQuickPickItem } from '../../../../platform/quickinput/common/quickInput.js'; -import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; -import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js'; -import { fromNow } from '../../../../base/common/date.js'; -import { IInlineAideChatSessionService, Recording } from './inlineChatSessionService.js'; -import { CONTEXT_ACCESSIBILITY_MODE_ENABLED } from '../../../../platform/accessibility/common/accessibility.js'; -import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; -import { registerIcon } from '../../../../platform/theme/common/iconRegistry.js'; -import { IPreferencesService } from '../../../../workbench/services/preferences/common/preferences.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { IAideChatService } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; - -CommandsRegistry.registerCommandAlias('interactiveEditor.start', 'inlineAideChat.start'); -CommandsRegistry.registerCommandAlias('interactive.acceptChanges', ACTION_ACCEPT_CHANGES); - -export const LOCALIZED_START_INLINE_CHAT_STRING = localize2('run', 'Start in Editor'); -export const START_INLINE_CHAT = registerIcon('start-inline-chat', Codicon.sparkle, localize('startInlineChat', 'Icon which spawns the inline chat from the editor toolbar.')); - -// some gymnastics to enable hold for speech without moving the StartSessionAction into the electron-layer - -export interface IHoldForSpeech { - (accessor: ServicesAccessor, controller: InlineChatController, source: Action2): void; -} -let _holdForSpeech: IHoldForSpeech | undefined = undefined; -export function setHoldForSpeech(holdForSpeech: IHoldForSpeech) { - _holdForSpeech = holdForSpeech; -} - -export class StartSessionAction extends EditorAction2 { - - constructor() { - super({ - id: 'inlineAideChat.start', - title: LOCALIZED_START_INLINE_CHAT_STRING, - category: AbstractInlineChatAction.category, - f1: true, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_AGENT, EditorContextKeys.writable), - keybinding: { - when: EditorContextKeys.focus, - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyCode.KeyI, - secondary: [KeyChord(KeyMod.CtrlCmd | KeyCode.KeyK, KeyCode.KeyI)], - }, - icon: START_INLINE_CHAT - }); - } - - - override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ..._args: any[]) { - - const ctrl = InlineChatController.get(editor); - if (!ctrl) { - return; - } - - if (_holdForSpeech) { - accessor.get(IInstantiationService).invokeFunction(_holdForSpeech, ctrl, this); - } - - let options: InlineChatRunOptions | undefined; - const arg = _args[0]; - if (arg && InlineChatRunOptions.isInlineChatRunOptions(arg)) { - options = arg; - } - InlineChatController.get(editor)?.run({ ...options }); - } -} - -export class UnstashSessionAction extends EditorAction2 { - constructor() { - super({ - id: 'inlineAideChat.unstash', - title: localize2('unstash', "Resume Last Dismissed Inline Chat"), - category: AbstractInlineChatAction.category, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_STASHED_SESSION, EditorContextKeys.writable), - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyCode.KeyZ, - } - }); - } - - override async runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, ..._args: any[]) { - const ctrl = InlineChatController.get(editor); - if (ctrl) { - const session = ctrl.unstashLastSession(); - if (session) { - ctrl.run({ - existingSession: session, - isUnstashed: true - }); - } - } - } -} - -export abstract class AbstractInlineChatAction extends EditorAction2 { - - static readonly category = localize2('cat', "Inline Chat"); - - constructor(desc: IAction2Options) { - super({ - ...desc, - category: AbstractInlineChatAction.category, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_HAS_AGENT, desc.precondition) - }); - } - - override runEditorCommand(accessor: ServicesAccessor, editor: ICodeEditor, ..._args: any[]) { - const editorService = accessor.get(IEditorService); - const logService = accessor.get(ILogService); - - let ctrl = InlineChatController.get(editor); - if (!ctrl) { - const { activeTextEditorControl } = editorService; - if (isCodeEditor(activeTextEditorControl)) { - editor = activeTextEditorControl; - } else if (isDiffEditor(activeTextEditorControl)) { - editor = activeTextEditorControl.getModifiedEditor(); - } - ctrl = InlineChatController.get(editor); - } - - if (!ctrl) { - logService.warn('[IE] NO controller found for action', this.desc.id, editor.getModel()?.uri); - return; - } - - if (editor instanceof EmbeddedCodeEditorWidget) { - editor = editor.getParentEditor(); - } - if (!ctrl) { - for (const diffEditor of accessor.get(ICodeEditorService).listDiffEditors()) { - if (diffEditor.getOriginalEditor() === editor || diffEditor.getModifiedEditor() === editor) { - if (diffEditor instanceof EmbeddedDiffEditorWidget) { - this.runEditorCommand(accessor, diffEditor.getParentEditor(), ..._args); - } - } - } - return; - } - this.runInlineChatCommand(accessor, ctrl, editor, ..._args); - } - - abstract runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor, ...args: any[]): void; -} - -export class ArrowOutUpAction extends AbstractInlineChatAction { - constructor() { - super({ - id: 'inlineAideChat.arrowOutUp', - title: localize('arrowUp', 'Cursor Up'), - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_FIRST, EditorContextKeys.isEmbeddedDiffEditor.negate(), CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), - keybinding: { - weight: KeybindingWeight.EditorCore, - primary: KeyMod.CtrlCmd | KeyCode.UpArrow - } - }); - } - - runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): void { - ctrl.arrowOut(true); - } -} - -export class ArrowOutDownAction extends AbstractInlineChatAction { - constructor() { - super({ - id: 'inlineAideChat.arrowOutDown', - title: localize('arrowDown', 'Cursor Down'), - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_INNER_CURSOR_LAST, EditorContextKeys.isEmbeddedDiffEditor.negate(), CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), - keybinding: { - weight: KeybindingWeight.EditorCore, - primary: KeyMod.CtrlCmd | KeyCode.DownArrow - } - }); - } - - runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): void { - ctrl.arrowOut(false); - } -} - -export class FocusInlineChat extends EditorAction2 { - - constructor() { - super({ - id: 'inlineAideChat.focus', - title: localize2('focus', "Focus Input"), - f1: true, - category: AbstractInlineChatAction.category, - precondition: ContextKeyExpr.and(EditorContextKeys.editorTextFocus, CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_FOCUSED.negate(), CONTEXT_ACCESSIBILITY_MODE_ENABLED.negate()), - keybinding: [{ - weight: KeybindingWeight.EditorCore + 10, // win against core_command - when: ContextKeyExpr.and(CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.isEqualTo('above'), EditorContextKeys.isEmbeddedDiffEditor.negate()), - primary: KeyMod.CtrlCmd | KeyCode.DownArrow, - }, { - weight: KeybindingWeight.EditorCore + 10, // win against core_command - when: ContextKeyExpr.and(CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.isEqualTo('below'), EditorContextKeys.isEmbeddedDiffEditor.negate()), - primary: KeyMod.CtrlCmd | KeyCode.UpArrow, - }] - }); - } - - override runEditorCommand(_accessor: ServicesAccessor, editor: ICodeEditor, ..._args: any[]) { - InlineChatController.get(editor)?.focus(); - } -} - - -export class DiscardAction extends AbstractInlineChatAction { - - constructor() { - super({ - id: 'inlineAideChat.discard', - title: localize('discard', 'Discard'), - icon: Codicon.discard, - precondition: CTX_INLINE_CHAT_VISIBLE, - keybinding: { - weight: KeybindingWeight.EditorContrib - 1, - primary: KeyCode.Escape, - when: CTX_INLINE_CHAT_USER_DID_EDIT.negate() - } - }); - } - - async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): Promise { - await ctrl.cancelSession(); - } -} - -export class AcceptChanges extends AbstractInlineChatAction { - - constructor() { - super({ - id: ACTION_ACCEPT_CHANGES, - title: localize2('apply1', "Accept Changes"), - shortTitle: localize('apply2', 'Accept'), - icon: Codicon.check, - f1: true, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, ContextKeyExpr.or(CTX_INLINE_CHAT_DOCUMENT_CHANGED.toNegated(), CTX_INLINE_CHAT_EDIT_MODE.notEqualsTo(EditMode.Preview))), - keybinding: [{ - weight: KeybindingWeight.WorkbenchContrib + 10, - primary: KeyMod.CtrlCmd | KeyCode.Enter, - }], - menu: { - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: '0_main', - order: 1, - when: ContextKeyExpr.and( - CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.toNegated(), - CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.MessagesAndEdits) - ), - } - }); - } - - override async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController): Promise { - ctrl.acceptHunk(); - } -} - -export class DiscardHunkAction extends AbstractInlineChatAction { - - constructor() { - super({ - id: 'inlineAideChat.discardHunkChange', - title: localize('discard', 'Discard'), - icon: Codicon.clearAll, - precondition: CTX_INLINE_CHAT_VISIBLE, - menu: { - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: '0_main', - order: 2, - when: ContextKeyExpr.and( - CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate(), - CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.MessagesAndEdits), - CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live) - ), - }, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyCode.Escape, - when: CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.MessagesAndEdits) - } - }); - } - - async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): Promise { - return ctrl.discardHunk(); - } -} - -export class RerunAction extends AbstractInlineChatAction { - constructor() { - super({ - id: ACTION_REGENERATE_RESPONSE, - title: localize2('chat.rerun.label', "Rerun Request"), - shortTitle: localize('rerun', 'Rerun'), - f1: false, - icon: Codicon.refresh, - precondition: CTX_INLINE_CHAT_VISIBLE, - menu: { - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: '0_main', - order: 5, - when: ContextKeyExpr.and( - CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate(), - CTX_INLINE_CHAT_RESPONSE_TYPE.notEqualsTo(InlineChatResponseType.None) - ) - }, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyCode.KeyR - } - }); - } - - override async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): Promise { - const chatService = accessor.get(IAideChatService); - const model = ctrl.chatWidget.viewModel?.model; - - const lastRequest = model?.getRequests().at(-1); - if (lastRequest) { - await chatService.resendRequest(lastRequest, { noCommandDetection: false, attempt: lastRequest.attempt + 1, location: ctrl.chatWidget.location }); - } - } -} - -export class CloseAction extends AbstractInlineChatAction { - - constructor() { - super({ - id: 'inlineAideChat.close', - title: localize('close', 'Close'), - icon: Codicon.close, - precondition: CTX_INLINE_CHAT_VISIBLE, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyCode.Escape, - }, - menu: [{ - id: MENU_INLINE_CHAT_CONTENT_STATUS, - group: '0_main', - order: 10, - }, { - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: '0_main', - order: 1, - when: ContextKeyExpr.and( - CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate(), - ContextKeyExpr.or( - CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.Messages), - CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Preview) - ) - ), - }] - }); - } - - async runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): Promise { - ctrl.cancelSession(); - } -} - -export class ConfigureInlineChatAction extends AbstractInlineChatAction { - constructor() { - super({ - id: 'inlineAideChat.configure', - title: localize2('configure', 'Configure Inline Chat'), - icon: Codicon.settingsGear, - precondition: CTX_INLINE_CHAT_VISIBLE, - f1: true, - menu: { - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: 'zzz', - order: 5 - } - }); - } - - async runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]): Promise { - accessor.get(IPreferencesService).openSettings({ query: 'inlineChat' }); - } -} - -export class MoveToNextHunk extends AbstractInlineChatAction { - - constructor() { - super({ - id: 'inlineAideChat.moveToNextHunk', - title: localize2('moveToNextHunk', 'Move to Next Change'), - precondition: CTX_INLINE_CHAT_VISIBLE, - f1: true, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyCode.F7 - } - }); - } - - override runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor, ...args: any[]): void { - ctrl.moveHunk(true); - } -} - -export class MoveToPreviousHunk extends AbstractInlineChatAction { - - constructor() { - super({ - id: 'inlineAideChat.moveToPreviousHunk', - title: localize2('moveToPreviousHunk', 'Move to Previous Change'), - f1: true, - precondition: CTX_INLINE_CHAT_VISIBLE, - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.Shift | KeyCode.F7 - } - }); - } - - override runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController, editor: ICodeEditor, ...args: any[]): void { - ctrl.moveHunk(false); - } -} - -export class CopyRecordings extends AbstractInlineChatAction { - - constructor() { - super({ - id: 'inlineAideChat.copyRecordings', - f1: true, - title: localize2('copyRecordings', "(Developer) Write Exchange to Clipboard") - }); - } - - override async runInlineChatCommand(accessor: ServicesAccessor): Promise { - - const clipboardService = accessor.get(IClipboardService); - const quickPickService = accessor.get(IQuickInputService); - const ieSessionService = accessor.get(IInlineAideChatSessionService); - - const recordings = ieSessionService.recordings().filter(r => r.exchanges.length > 0); - if (recordings.length === 0) { - return; - } - - const picks: (IQuickPickItem & { rec: Recording })[] = recordings.map(rec => { - return { - rec, - label: localize('label', "'{0}' and {1} follow ups ({2})", rec.exchanges[0].prompt, rec.exchanges.length - 1, fromNow(rec.when, true)), - tooltip: rec.exchanges.map(ex => ex.prompt).join('\n'), - }; - }); - - const pick = await quickPickService.pick(picks, { canPickMany: false }); - if (pick) { - clipboardService.writeText(JSON.stringify(pick.rec, undefined, 2)); - } - } -} - -export class ViewInChatAction extends AbstractInlineChatAction { - constructor() { - super({ - id: ACTION_VIEW_IN_CHAT, - title: localize('viewInChat', 'View in Chat'), - icon: Codicon.commentDiscussion, - precondition: CTX_INLINE_CHAT_VISIBLE, - menu: [{ - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: 'more', - order: 1, - when: CTX_INLINE_CHAT_RESPONSE_TYPE.notEqualsTo(InlineChatResponseType.Messages) - }, { - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: '0_main', - order: 1, - when: ContextKeyExpr.and( - CTX_INLINE_CHAT_RESPONSE_TYPE.isEqualTo(InlineChatResponseType.Messages), - CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.negate() - ) - }], - keybinding: { - weight: KeybindingWeight.WorkbenchContrib, - primary: KeyMod.CtrlCmd | KeyCode.DownArrow, - } - }); - } - override runInlineChatCommand(_accessor: ServicesAccessor, ctrl: InlineChatController, _editor: ICodeEditor, ..._args: any[]) { - return ctrl.viewInChat(); - } -} - -export class ToggleDiffForChange extends AbstractInlineChatAction { - - constructor() { - super({ - id: ACTION_TOGGLE_DIFF, - precondition: ContextKeyExpr.and(CTX_INLINE_CHAT_VISIBLE, CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live), CTX_INLINE_CHAT_CHANGE_HAS_DIFF), - title: localize2('showChanges', 'Toggle Changes'), - icon: Codicon.diffSingle, - toggled: { - condition: CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, - }, - menu: { - id: MENU_INLINE_CHAT_WIDGET_STATUS, - group: 'more', - when: ContextKeyExpr.and(CTX_INLINE_CHAT_EDIT_MODE.isEqualTo(EditMode.Live), CTX_INLINE_CHAT_CHANGE_HAS_DIFF), - order: 10, - } - }); - } - - override runInlineChatCommand(accessor: ServicesAccessor, ctrl: InlineChatController): void { - ctrl.toggleDiff(); - } -} diff --git a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatContentWidget.ts b/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatContentWidget.ts deleted file mode 100644 index 1bfd257b523..00000000000 --- a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatContentWidget.ts +++ /dev/null @@ -1,212 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import './media/inlineChatContentWidget.css'; -import { ContentWidgetPositionPreference, ICodeEditor, IContentWidget, IContentWidgetPosition } from '../../../../editor/browser/editorBrowser.js'; -import * as dom from '../../../../base/browser/dom.js'; -import { IDimension } from '../../../../editor/common/core/dimension.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { IPosition, Position } from '../../../../editor/common/core/position.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { inlineChatBackground, InlineChatConfigKeys, MENU_INLINE_CHAT_CONTENT_STATUS, MENU_INLINE_CHAT_EXECUTE } from '../../../../workbench/contrib/inlineAideChat/common/inlineChat.js'; -import { Session } from '../../../../workbench/contrib/inlineAideChat/browser/inlineChatSession.js'; -import { ChatWidget } from '../../../../workbench/contrib/aideChat/browser/aideChatWidget.js'; -import { AideChatAgentLocation } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { editorBackground, editorForeground, inputBackground } from '../../../../platform/theme/common/colorRegistry.js'; -import { ChatModel } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { Range } from '../../../../editor/common/core/range.js'; -import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; -import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { EditorOption } from '../../../../editor/common/config/editorOptions.js'; -import { ScrollType } from '../../../../editor/common/editorCommon.js'; -import { MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; -import { MenuItemAction } from '../../../../platform/actions/common/actions.js'; -import { TextOnlyMenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; - -export class InlineChatContentWidget implements IContentWidget { - - readonly suppressMouseDown = false; - readonly allowEditorOverflow = true; - - private readonly _store = new DisposableStore(); - private readonly _domNode = document.createElement('div'); - private readonly _inputContainer = document.createElement('div'); - private readonly _toolbarContainer = document.createElement('div'); - - private _position?: IPosition; - - private readonly _onDidBlur = this._store.add(new Emitter()); - readonly onDidBlur: Event = this._onDidBlur.event; - - private _visible: boolean = false; - private _focusNext: boolean = false; - - private readonly _defaultChatModel: ChatModel; - private readonly _widget: ChatWidget; - - constructor( - location: AideChatAgentLocation, - private readonly _editor: ICodeEditor, - @IInstantiationService instaService: IInstantiationService, - @IContextKeyService contextKeyService: IContextKeyService, - @IConfigurationService configurationService: IConfigurationService - ) { - - this._defaultChatModel = this._store.add(instaService.createInstance(ChatModel, undefined, AideChatAgentLocation.Editor)); - - const scopedInstaService = instaService.createChild( - new ServiceCollection([ - IContextKeyService, - this._store.add(contextKeyService.createScoped(this._domNode)) - ]), - this._store - ); - - this._widget = scopedInstaService.createInstance( - ChatWidget, - location, - { resource: true }, - { - defaultElementHeight: 32, - editorOverflowWidgetsDomNode: _editor.getOverflowWidgetsDomNode(), - renderStyle: 'minimal', - renderInputOnTop: true, - renderFollowups: true, - supportsFileReferences: false, - menus: { - telemetrySource: 'inlineChat-content', - executeToolbar: MENU_INLINE_CHAT_EXECUTE, - }, - filter: _item => false - }, - { - listForeground: editorForeground, - listBackground: inlineChatBackground, - inputEditorBackground: inputBackground, - resultEditorBackground: editorBackground - } - ); - this._store.add(this._widget); - this._widget.render(this._inputContainer); - this._widget.setModel(this._defaultChatModel, {}); - this._store.add(this._widget.onDidChangeContentHeight(() => _editor.layoutContentWidget(this))); - - this._domNode.tabIndex = -1; - this._domNode.className = 'inline-chat-content-widget cschat-session'; - - this._domNode.appendChild(this._inputContainer); - - this._toolbarContainer.classList.add('toolbar'); - if (!configurationService.getValue(InlineChatConfigKeys.ExpTextButtons)) { - this._toolbarContainer.style.display = 'none'; - this._domNode.style.paddingBottom = '6px'; - } - this._domNode.appendChild(this._toolbarContainer); - - this._store.add(scopedInstaService.createInstance(MenuWorkbenchToolBar, this._toolbarContainer, MENU_INLINE_CHAT_CONTENT_STATUS, { - actionViewItemProvider: action => action instanceof MenuItemAction ? instaService.createInstance(TextOnlyMenuEntryActionViewItem, action, { conversational: true }) : undefined, - toolbarOptions: { primaryGroup: '0_main' }, - icon: false, - label: true, - })); - - const tracker = dom.trackFocus(this._domNode); - this._store.add(tracker.onDidBlur(() => { - if (this._visible && this._widget.inputEditor.getModel()?.getValueLength() === 0 - // && !"ON" - ) { - this._onDidBlur.fire(); - } - })); - this._store.add(tracker); - } - - dispose(): void { - this._store.dispose(); - } - - getId(): string { - return 'inline-chat-content-widget'; - } - - getDomNode(): HTMLElement { - return this._domNode; - } - - getPosition(): IContentWidgetPosition | null { - if (!this._position) { - return null; - } - return { - position: this._position, - preference: [ContentWidgetPositionPreference.ABOVE] - }; - } - - beforeRender(): IDimension | null { - - const maxHeight = this._widget.input.inputEditor.getOption(EditorOption.lineHeight) * 5; - const inputEditorHeight = this._widget.contentHeight; - - this._widget.layout(Math.min(maxHeight, inputEditorHeight), 390); - - // const actualHeight = this._widget.inputPartHeight; - // return new dom.Dimension(width, actualHeight); - return null; - } - - afterRender(): void { - if (this._focusNext) { - this._focusNext = false; - this._widget.focusInput(); - } - } - - // --- - - get chatWidget(): ChatWidget { - return this._widget; - } - - get isVisible(): boolean { - return this._visible; - } - - get value(): string { - return this._widget.inputEditor.getValue(); - } - - show(position: IPosition) { - if (!this._visible) { - this._visible = true; - this._focusNext = true; - - this._editor.revealRangeNearTopIfOutsideViewport(Range.fromPositions(position), ScrollType.Immediate); - this._widget.inputEditor.setValue(''); - - const wordInfo = this._editor.getModel()?.getWordAtPosition(position); - - this._position = wordInfo ? new Position(position.lineNumber, wordInfo.startColumn) : position; - this._editor.addContentWidget(this); - this._widget.setVisible(true); - } - } - - hide() { - if (this._visible) { - this._visible = false; - this._editor.removeContentWidget(this); - this._widget.saveState(); - this._widget.setVisible(false); - } - } - - setSession(session: Session): void { - this._widget.setModel(session.chatModel, {}); - this._widget.setInputPlaceholder(session.agent.description ?? ''); - } -} diff --git a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatController.ts b/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatController.ts deleted file mode 100644 index d3154859c4b..00000000000 --- a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatController.ts +++ /dev/null @@ -1,1147 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as aria from '../../../../base/browser/ui/aria/aria.js'; -import { Barrier, DeferredPromise, Queue } from '../../../../base/common/async.js'; -import { CancellationTokenSource } from '../../../../base/common/cancellation.js'; -import { toErrorMessage } from '../../../../base/common/errorMessage.js'; -import { onUnexpectedError } from '../../../../base/common/errors.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { Lazy } from '../../../../base/common/lazy.js'; -import { DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { MovingAverage } from '../../../../base/common/numbers.js'; -import { StopWatch } from '../../../../base/common/stopwatch.js'; -import { assertType } from '../../../../base/common/types.js'; -import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { IPosition, Position } from '../../../../editor/common/core/position.js'; -import { IRange, Range } from '../../../../editor/common/core/range.js'; -import { ISelection, Selection } from '../../../../editor/common/core/selection.js'; -import { IEditorContribution, IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js'; -import { CompletionItemKind, CompletionList, TextEdit } from '../../../../editor/common/languages.js'; -import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js'; -import { InlineCompletionsController } from '../../../../editor/contrib/inlineCompletions/browser/controller/inlineCompletionsController.js'; -import { localize } from '../../../../nls.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js'; -import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { IAideChatWidgetService, showChatView } from '../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { IAideChatService } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { IInlineAideChatSavingService } from './inlineChatSavingService.js'; -import { EmptyResponse, ErrorResponse, ReplyResponse, Session, SessionPrompt } from '../../../../workbench/contrib/inlineAideChat/browser/inlineChatSession.js'; -import { IInlineAideChatSessionService } from './inlineChatSessionService.js'; -import { EditModeStrategy, IEditObserver, LiveStrategy, PreviewStrategy, ProgressingEditsOptions } from '../../../../workbench/contrib/inlineAideChat/browser/inlineChatStrategies.js'; -import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; -import { CTX_INLINE_CHAT_REQUEST_IN_PROGRESS, CTX_INLINE_CHAT_RESPONSE_TYPE, CTX_INLINE_CHAT_USER_DID_EDIT, CTX_INLINE_CHAT_VISIBLE, EditMode, INLINE_CHAT_ID, InlineChatConfigKeys, InlineChatResponseType } from '../../../../workbench/contrib/inlineAideChat/common/inlineChat.js'; -import { StashedSession } from './inlineChatSession.js'; -import { IModelDeltaDecoration, ITextModel, IValidEditOperation } from '../../../../editor/common/model.js'; -import { InlineChatContentWidget } from '../../../../workbench/contrib/inlineAideChat/browser/inlineChatContentWidget.js'; -import { MessageController } from '../../../../editor/contrib/message/browser/messageController.js'; -import { ChatModel, ChatRequestRemovalReason, IChatRequestModel, IChatTextEditGroup, IChatTextEditGroupState, IResponse } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { InlineChatError } from '../../../../workbench/contrib/inlineAideChat/browser/inlineChatSessionServiceImpl.js'; -import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; -import { ChatInputPart } from '../../../../workbench/contrib/aideChat/browser/aideChatInputPart.js'; -import { IViewsService } from '../../../../workbench/services/views/common/viewsService.js'; -import { DefaultModelSHA1Computer } from '../../../../editor/common/services/modelService.js'; -import { generateUuid } from '../../../../base/common/uuid.js'; -import { isEqual } from '../../../../base/common/resources.js'; -import { AideChatAgentLocation } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { INotebookEditorService } from '../../../../workbench/contrib/notebook/browser/services/notebookEditorService.js'; - -export const enum State { - CREATE_SESSION = 'CREATE_SESSION', - INIT_UI = 'INIT_UI', - WAIT_FOR_INPUT = 'WAIT_FOR_INPUT', - SHOW_REQUEST = 'SHOW_REQUEST', - SHOW_RESPONSE = 'SHOW_RESPONSE', - PAUSE = 'PAUSE', - CANCEL = 'CANCEL', - ACCEPT = 'DONE', -} - -const enum Message { - NONE = 0, - ACCEPT_SESSION = 1 << 0, - CANCEL_SESSION = 1 << 1, - PAUSE_SESSION = 1 << 2, - CANCEL_REQUEST = 1 << 3, - CANCEL_INPUT = 1 << 4, - ACCEPT_INPUT = 1 << 5, -} - -export abstract class InlineChatRunOptions { - initialSelection?: ISelection; - initialRange?: IRange; - message?: string; - autoSend?: boolean; - existingSession?: Session; - isUnstashed?: boolean; - position?: IPosition; - withIntentDetection?: boolean; - - static isInlineChatRunOptions(options: any): options is InlineChatRunOptions { - const { initialSelection, initialRange, message, autoSend, position, existingSession } = options; - if ( - typeof message !== 'undefined' && typeof message !== 'string' - || typeof autoSend !== 'undefined' && typeof autoSend !== 'boolean' - || typeof initialRange !== 'undefined' && !Range.isIRange(initialRange) - || typeof initialSelection !== 'undefined' && !Selection.isISelection(initialSelection) - || typeof position !== 'undefined' && !Position.isIPosition(position) - || typeof existingSession !== 'undefined' && !(existingSession instanceof Session) - ) { - return false; - } - return true; - } -} - -export class InlineChatController implements IEditorContribution { - - static get(editor: ICodeEditor) { - return editor.getContribution(INLINE_CHAT_ID); - } - - private _isDisposed: boolean = false; - private readonly _store = new DisposableStore(); - - private readonly _ui: Lazy<{ content: InlineChatContentWidget; zone: InlineChatZoneWidget }>; - - private readonly _ctxVisible: IContextKey; - private readonly _ctxResponseType: IContextKey; - private readonly _ctxUserDidEdit: IContextKey; - private readonly _ctxRequestInProgress: IContextKey; - - private _messages = this._store.add(new Emitter()); - - private readonly _onWillStartSession = this._store.add(new Emitter()); - readonly onWillStartSession = this._onWillStartSession.event; - - get chatWidget() { - if (this._ui.value.content.isVisible) { - return this._ui.value.content.chatWidget; - } else { - return this._ui.value.zone.widget.chatWidget; - } - } - - private readonly _sessionStore = this._store.add(new DisposableStore()); - private readonly _stashedSession = this._store.add(new MutableDisposable()); - private _session?: Session; - private _strategy?: EditModeStrategy; - - constructor( - private readonly _editor: ICodeEditor, - @IInstantiationService private readonly _instaService: IInstantiationService, - @IInlineAideChatSessionService private readonly _inlineChatSessionService: IInlineAideChatSessionService, - @IInlineAideChatSavingService private readonly _inlineChatSavingService: IInlineAideChatSavingService, - @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, - @ILogService private readonly _logService: ILogService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IDialogService private readonly _dialogService: IDialogService, - @IContextKeyService contextKeyService: IContextKeyService, - @IAideChatService private readonly _chatService: IAideChatService, - @ILanguageFeaturesService private readonly _languageFeatureService: ILanguageFeaturesService, - @IAideChatWidgetService private readonly _chatWidgetService: IAideChatWidgetService, - @INotebookEditorService notebookEditorService: INotebookEditorService, - ) { - this._ctxVisible = CTX_INLINE_CHAT_VISIBLE.bindTo(contextKeyService); - this._ctxUserDidEdit = CTX_INLINE_CHAT_USER_DID_EDIT.bindTo(contextKeyService); - this._ctxResponseType = CTX_INLINE_CHAT_RESPONSE_TYPE.bindTo(contextKeyService); - this._ctxRequestInProgress = CTX_INLINE_CHAT_REQUEST_IN_PROGRESS.bindTo(contextKeyService); - - this._ui = new Lazy(() => { - let location = AideChatAgentLocation.Editor; - - // inline chat in notebooks - // check if this editor is part of a notebook editor - // and iff so, use the notebook location - for (const notebookEditor of notebookEditorService.listNotebookEditors()) { - for (const [, codeEditor] of notebookEditor.codeEditors) { - if (codeEditor === this._editor) { - location = AideChatAgentLocation.Notebook; - break; - } - } - } - const content = this._store.add(_instaService.createInstance(InlineChatContentWidget, location, this._editor)); - const zone = this._store.add(_instaService.createInstance(InlineChatZoneWidget, location, this._editor)); - return { content, zone }; - }); - - this._store.add(this._editor.onDidChangeModel(async e => { - if (this._session || !e.newModelUrl) { - return; - } - - const existingSession = this._inlineChatSessionService.getSession(this._editor, e.newModelUrl); - if (!existingSession) { - return; - } - - this._log('session RESUMING after model change', e); - await this.run({ existingSession }); - })); - - this._store.add(this._inlineChatSessionService.onDidEndSession(e => { - if (e.session === this._session && e.endedByExternalCause) { - this._log('session ENDED by external cause'); - this._session = undefined; - this._strategy?.cancel(); - this._resetWidget(); - this.cancelSession(); - } - })); - - this._store.add(this._inlineChatSessionService.onDidMoveSession(async e => { - if (e.editor === this._editor) { - this._log('session RESUMING after move', e); - await this.run({ existingSession: e.session }); - } - })); - - this._log('NEW controller'); - } - - dispose(): void { - if (this._currentRun) { - this._messages.fire(this._session?.chatModel.hasRequests - ? Message.PAUSE_SESSION - : Message.CANCEL_SESSION); - } - this._store.dispose(); - this._isDisposed = true; - this._log('DISPOSED controller'); - } - - private _log(message: string | Error, ...more: any[]): void { - if (message instanceof Error) { - this._logService.error(message, ...more); - } else { - this._logService.trace(`[IE] (editor:${this._editor.getId()})${message}`, ...more); - } - } - - getMessage(): string | undefined { - return this._ui.value.zone.widget.responseContent; - } - - getId(): string { - return INLINE_CHAT_ID; - } - - private _getMode(): EditMode { - return this._configurationService.getValue(InlineChatConfigKeys.Mode); - } - - getWidgetPosition(): Position | undefined { - return this._ui.value.zone.position; - } - - private _currentRun?: Promise; - - async run(options: InlineChatRunOptions | undefined = {}): Promise { - try { - this.finishExistingSession(); - if (this._currentRun) { - await this._currentRun; - } - if (options.initialSelection) { - this._editor.setSelection(options.initialSelection); - } - this._stashedSession.clear(); - this._onWillStartSession.fire(); - this._currentRun = this._nextState(State.CREATE_SESSION, options); - await this._currentRun; - - } catch (error) { - // this should not happen but when it does make sure to tear down the UI and everything - onUnexpectedError(error); - if (this._session) { - this._inlineChatSessionService.releaseSession(this._session); - } - this[State.PAUSE](); - - } finally { - this._currentRun = undefined; - } - } - - // ---- state machine - - protected async _nextState(state: State, options: InlineChatRunOptions): Promise { - let nextState: State | void = state; - while (nextState && !this._isDisposed) { - this._log('setState to ', nextState); - nextState = await this[nextState](options); - } - } - - private async [State.CREATE_SESSION](options: InlineChatRunOptions): Promise { - assertType(this._session === undefined); - assertType(this._editor.hasModel()); - - let session: Session | undefined = options.existingSession; - - - let initPosition: Position | undefined; - if (options.position) { - initPosition = Position.lift(options.position).delta(-1); - delete options.position; - } - - const widgetPosition = this._showWidget(true, initPosition); - - // this._updatePlaceholder(); - let errorMessage = localize('create.fail', "Failed to start editor chat"); - - if (!session) { - const createSessionCts = new CancellationTokenSource(); - const msgListener = Event.once(this._messages.event)(m => { - this._log('state=_createSession) message received', m); - if (m === Message.ACCEPT_INPUT) { - // user accepted the input before having a session - options.autoSend = true; - this._ui.value.zone.widget.updateInfo(localize('welcome.2', "Getting ready...")); - } else { - createSessionCts.cancel(); - } - }); - - try { - session = await this._inlineChatSessionService.createSession( - this._editor, - { editMode: this._getMode(), wholeRange: options.initialRange }, - createSessionCts.token - ); - } catch (error) { - // Inline chat errors are from the provider and have their error messages shown to the user - if (error instanceof InlineChatError || error?.name === InlineChatError.code) { - errorMessage = error.message; - } - } - - createSessionCts.dispose(); - msgListener.dispose(); - - if (createSessionCts.token.isCancellationRequested) { - if (session) { - this._inlineChatSessionService.releaseSession(session); - } - return State.CANCEL; - } - } - - delete options.initialRange; - delete options.existingSession; - - if (!session) { - MessageController.get(this._editor)?.showMessage(errorMessage, widgetPosition); - this._log('Failed to start editor chat'); - return State.CANCEL; - } - - await session.chatModel.waitForInitialization(); - - // create a new strategy - switch (session.editMode) { - case EditMode.Preview: - this._strategy = this._instaService.createInstance(PreviewStrategy, session, this._editor, this._ui.value.zone); - break; - case EditMode.Live: - default: - this._strategy = this._instaService.createInstance(LiveStrategy, session, this._editor, this._ui.value.zone); - break; - } - - this._session = session; - return State.INIT_UI; - } - - private async [State.INIT_UI](options: InlineChatRunOptions): Promise { - assertType(this._session); - assertType(this._strategy); - - // hide/cancel inline completions when invoking IE - InlineCompletionsController.get(this._editor)?.hide(); - - this._sessionStore.clear(); - - const wholeRangeDecoration = this._editor.createDecorationsCollection(); - const updateWholeRangeDecoration = () => { - const newDecorations = this._strategy?.getWholeRangeDecoration() ?? []; - wholeRangeDecoration.set(newDecorations); - }; - this._sessionStore.add(toDisposable(() => wholeRangeDecoration.clear())); - this._sessionStore.add(this._session.wholeRange.onDidChange(updateWholeRangeDecoration)); - updateWholeRangeDecoration(); - - this._sessionStore.add(this._ui.value.content.onDidBlur(() => this.cancelSession())); - - this._ui.value.content.setSession(this._session); - // this._ui.value.zone.widget.updateSlashCommands(this._session.session.slashCommands ?? []); - this._updatePlaceholder(); - - this._showWidget(!this._session.chatModel.hasRequests); - this._ui.value.zone.widget.updateToolbar(true); - - this._sessionStore.add(this._editor.onDidChangeModel((e) => { - const msg = this._session?.chatModel.hasRequests - ? Message.PAUSE_SESSION // pause when switching models/tabs and when having a previous exchange - : Message.CANCEL_SESSION; - this._log('model changed, pause or cancel session', msg, e); - this._messages.fire(msg); - })); - - const altVersionNow = this._editor.getModel()?.getAlternativeVersionId(); - - this._sessionStore.add(this._editor.onDidChangeModelContent(e => { - - if (!this._session?.hunkData.ignoreTextModelNChanges) { - this._ctxUserDidEdit.set(altVersionNow !== this._editor.getModel()?.getAlternativeVersionId()); - } - - if (this._session?.hunkData.ignoreTextModelNChanges || this._strategy?.hasFocus()) { - return; - } - - const wholeRange = this._session!.wholeRange; - let shouldFinishSession = false; - if (this._configurationService.getValue(InlineChatConfigKeys.FinishOnType)) { - for (const { range } of e.changes) { - shouldFinishSession = !Range.areIntersectingOrTouching(range, wholeRange.value); - } - } - - this._session!.recordExternalEditOccurred(shouldFinishSession); - - if (shouldFinishSession) { - this._log('text changed outside of whole range, FINISH session'); - this.finishExistingSession(); - } - })); - - this._sessionStore.add(this._session.chatModel.onDidChange(async e => { - if (e.kind === 'removeRequest') { - // TODO@jrieken there is still some work left for when a request "in the middle" - // is removed. We will undo all changes till that point but not remove those - // later request - const exchange = this._session!.exchanges.find(candidate => candidate.prompt.request.id === e.requestId); - if (exchange && this._editor.hasModel()) { - // undo till this point - this._session!.hunkData.ignoreTextModelNChanges = true; - try { - - const model = this._editor.getModel(); - const targetAltVersion = exchange.prompt.modelAltVersionId; - while (targetAltVersion < model.getAlternativeVersionId() && model.canUndo()) { - await model.undo(); - } - } finally { - this._session!.hunkData.ignoreTextModelNChanges = false; - } - } - } - })); - - // #region DEBT - // DEBT@jrieken - // REMOVE when agents are adopted - this._sessionStore.add(this._languageFeatureService.completionProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, hasAccessToAllModels: true }, { - _debugDisplayName: 'inline chat commands', - triggerCharacters: ['/'], - provideCompletionItems: (model, position, context, token) => { - if (position.lineNumber !== 1) { - return undefined; - } - if (!this._session || !this._session.agent.slashCommands) { - return undefined; - } - const widget = this._chatWidgetService.getWidgetByInputUri(model.uri); - if (widget !== this._ui.value.zone.widget.chatWidget && widget !== this._ui.value.content.chatWidget) { - return undefined; - } - - const result: CompletionList = { suggestions: [], incomplete: false }; - for (const command of this._session.agent.slashCommands) { - const withSlash = `/${command.name}`; - result.suggestions.push({ - label: { label: withSlash, description: command.description ?? '' }, - kind: CompletionItemKind.Text, - insertText: withSlash, - range: Range.fromPositions(new Position(1, 1), position), - }); - } - - return result; - } - })); - - const updateSlashDecorations = (collection: IEditorDecorationsCollection, model: ITextModel) => { - - const newDecorations: IModelDeltaDecoration[] = []; - for (const command of (this._session?.agent.slashCommands ?? []).sort((a, b) => b.name.length - a.name.length)) { - const withSlash = `/${command.name}`; - const firstLine = model.getLineContent(1); - if (firstLine.startsWith(withSlash)) { - newDecorations.push({ - range: new Range(1, 1, 1, withSlash.length + 1), - options: { - description: 'inline-chat-slash-command', - inlineClassName: 'inline-chat-slash-command', - after: { - // Force some space between slash command and placeholder - content: ' ' - } - } - }); - - // inject detail when otherwise empty - if (firstLine.trim() === `/${command.name}`) { - newDecorations.push({ - range: new Range(1, withSlash.length, 1, withSlash.length), - options: { - description: 'inline-chat-slash-command-detail', - after: { - content: `${command.description}`, - inlineClassName: 'inline-chat-slash-command-detail' - } - } - }); - } - break; - } - } - collection.set(newDecorations); - }; - const inputInputEditor = this._ui.value.content.chatWidget.inputEditor; - const zoneInputEditor = this._ui.value.zone.widget.chatWidget.inputEditor; - const inputDecorations = inputInputEditor.createDecorationsCollection(); - const zoneDecorations = zoneInputEditor.createDecorationsCollection(); - this._sessionStore.add(inputInputEditor.onDidChangeModelContent(() => updateSlashDecorations(inputDecorations, inputInputEditor.getModel()!))); - this._sessionStore.add(zoneInputEditor.onDidChangeModelContent(() => updateSlashDecorations(zoneDecorations, zoneInputEditor.getModel()!))); - this._sessionStore.add(toDisposable(() => { - inputDecorations.clear(); - zoneDecorations.clear(); - })); - - //#endregion ------- DEBT - - if (!this._session.chatModel.hasRequests) { - return State.WAIT_FOR_INPUT; - } else if (options.isUnstashed) { - delete options.isUnstashed; - return State.SHOW_RESPONSE; - } else { - return State.SHOW_RESPONSE; - } - } - - private async [State.WAIT_FOR_INPUT](options: InlineChatRunOptions): Promise { - assertType(this._session); - assertType(this._strategy); - - this._updatePlaceholder(); - - if (options.message) { - this.updateInput(options.message); - aria.alert(options.message); - delete options.message; - this._showWidget(false); - } - - let message = Message.NONE; - let request: IChatRequestModel | undefined; - - const barrier = new Barrier(); - const store = new DisposableStore(); - store.add(this._session.chatModel.onDidChange(e => { - if (e.kind === 'addRequest') { - request = e.request; - message = Message.ACCEPT_INPUT; - barrier.open(); - } - })); - store.add(this._strategy.onDidAccept(() => this.acceptSession())); - store.add(this._strategy.onDidDiscard(() => this.cancelSession())); - store.add(Event.once(this._messages.event)(m => { - this._log('state=_waitForInput) message received', m); - message = m; - barrier.open(); - })); - - if (options.autoSend) { - delete options.autoSend; - this._showWidget(false); - this._ui.value.zone.widget.chatWidget.acceptInput(); - } - - await barrier.wait(); - store.dispose(); - - - if (message & (Message.CANCEL_INPUT | Message.CANCEL_SESSION)) { - return State.CANCEL; - } - - if (message & Message.PAUSE_SESSION) { - return State.PAUSE; - } - - if (message & Message.ACCEPT_SESSION) { - this._ui.value.zone.widget.selectAll(false); - return State.ACCEPT; - } - - if (!request?.message.text) { - return State.WAIT_FOR_INPUT; - } - - this._session.addInput(new SessionPrompt(request, this._editor.getModel()!.getAlternativeVersionId())); - - return State.SHOW_REQUEST; - } - - - private async [State.SHOW_REQUEST](): Promise { - assertType(this._session); - assertType(this._session.chatModel.requestInProgress); - - this._ctxRequestInProgress.set(true); - - const { chatModel } = this._session; - const request: IChatRequestModel | undefined = chatModel.getRequests().at(-1); - - assertType(request); - assertType(request.response); - - this._showWidget(false); - // this._ui.value.zone.widget.value = request.message.text; - this._ui.value.zone.widget.selectAll(false); - this._ui.value.zone.widget.updateInfo(''); - - const { response } = request; - const responsePromise = new DeferredPromise(); - - const store = new DisposableStore(); - - const progressiveEditsCts = store.add(new CancellationTokenSource()); - const progressiveEditsAvgDuration = new MovingAverage(); - const progressiveEditsClock = StopWatch.create(); - const progressiveEditsQueue = new Queue(); - - let next: State.SHOW_RESPONSE | State.SHOW_REQUEST | State.CANCEL | State.PAUSE | State.ACCEPT | State.WAIT_FOR_INPUT = State.SHOW_RESPONSE; - store.add(Event.once(this._messages.event)(message => { - this._log('state=_makeRequest) message received', message); - this._chatService.cancelCurrentRequestForSession(chatModel.sessionId); - if (message & Message.CANCEL_SESSION) { - next = State.CANCEL; - } else if (message & Message.PAUSE_SESSION) { - next = State.PAUSE; - } else if (message & Message.ACCEPT_SESSION) { - next = State.ACCEPT; - } - })); - - store.add(chatModel.onDidChange(e => { - if (e.kind === 'removeRequest' && e.requestId === request.id) { - progressiveEditsCts.cancel(); - responsePromise.complete(); - if (e.reason === ChatRequestRemovalReason.Resend) { - next = State.SHOW_REQUEST; - } else { - next = State.CANCEL; - } - } - })); - - // cancel the request when the user types - store.add(this._ui.value.zone.widget.chatWidget.inputEditor.onDidChangeModelContent(() => { - this._chatService.cancelCurrentRequestForSession(chatModel.sessionId); - })); - - let lastLength = 0; - let isFirstChange = true; - - const sha1 = new DefaultModelSHA1Computer(); - const textModel0Sha1 = sha1.canComputeSHA1(this._session.textModel0) - ? sha1.computeSHA1(this._session.textModel0) - : generateUuid(); - const editState: IChatTextEditGroupState = { sha1: textModel0Sha1, applied: 0 }; - let localEditGroup: IChatTextEditGroup | undefined; - - // apply edits - const handleResponse = () => { - - this._updateCtxResponseType(); - - if (!localEditGroup) { - localEditGroup = response.response.value.find(part => part.kind === 'textEditGroup' && isEqual(part.uri, this._session?.textModelN.uri)); - } - - if (localEditGroup) { - - localEditGroup.state ??= editState; - - const edits = localEditGroup.edits; - const newEdits = edits.slice(lastLength); - if (newEdits.length > 0) { - // NEW changes - lastLength = edits.length; - progressiveEditsAvgDuration.update(progressiveEditsClock.elapsed()); - progressiveEditsClock.reset(); - - progressiveEditsQueue.queue(async () => { - - const startThen = this._session!.wholeRange.value.getStartPosition(); - - // making changes goes into a queue because otherwise the async-progress time will - // influence the time it takes to receive the changes and progressive typing will - // become infinitely fast - for (const edits of newEdits) { - await this._makeChanges(edits, { - duration: progressiveEditsAvgDuration.value, - token: progressiveEditsCts.token - }, isFirstChange); - - isFirstChange = false; - } - - // reshow the widget if the start position changed or shows at the wrong position - const startNow = this._session!.wholeRange.value.getStartPosition(); - if (!startNow.equals(startThen) || !this._ui.value.zone.position?.equals(startNow)) { - this._showWidget(false, startNow.delta(-1)); - } - }); - } - } - - if (response.isCanceled) { - progressiveEditsCts.cancel(); - responsePromise.complete(); - - } else if (response.isComplete) { - responsePromise.complete(); - } - }; - store.add(response.onDidChange(handleResponse)); - handleResponse(); - - // (1) we must wait for the request to finish - // (2) we must wait for all edits that came in via progress to complete - await responsePromise.p; - await progressiveEditsQueue.whenIdle(); - - store.dispose(); - - const diff = await this._editorWorkerService.computeDiff(this._session.textModel0.uri, this._session.textModelN.uri, { computeMoves: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, ignoreTrimWhitespace: false }, 'advanced'); - this._session.wholeRange.fixup(diff?.changes ?? []); - await this._session.hunkData.recompute(editState, diff); - - this._ctxRequestInProgress.set(false); - - return next; - } - - private async[State.SHOW_RESPONSE](): Promise { - assertType(this._session); - assertType(this._strategy); - - const { response } = this._session.lastExchange!; - - - let newPosition: Position | undefined; - - if (response instanceof EmptyResponse) { - // show status message - const status = localize('empty', "No results, please refine your input and try again"); - this._ui.value.zone.widget.updateStatus(status, { classes: ['warn'] }); - return State.WAIT_FOR_INPUT; - - } else if (response instanceof ErrorResponse) { - // show error - if (!response.isCancellation) { - this._ui.value.zone.widget.updateStatus(response.message, { classes: ['error'] }); - this._strategy?.cancel(); - } - - } else if (response instanceof ReplyResponse) { - // real response -> complex... - this._ui.value.zone.widget.updateStatus(''); - - const position = await this._strategy.renderChanges(response); - if (position) { - // if the selection doesn't start far off we keep the widget at its current position - // because it makes reading this nicer - const selection = this._editor.getSelection(); - if (selection?.containsPosition(position)) { - if (position.lineNumber - selection.startLineNumber > 8) { - newPosition = position; - } - } else { - newPosition = position; - } - } - } - this._showWidget(false, newPosition); - - return State.WAIT_FOR_INPUT; - } - - private async[State.PAUSE]() { - - this._resetWidget(); - - this._strategy?.dispose?.(); - this._session = undefined; - } - - private async[State.ACCEPT]() { - assertType(this._session); - assertType(this._strategy); - this._sessionStore.clear(); - - try { - await this._strategy.apply(); - } catch (err) { - this._dialogService.error(localize('err.apply', "Failed to apply changes.", toErrorMessage(err))); - this._log('FAILED to apply changes'); - this._log(err); - } - - this._inlineChatSessionService.releaseSession(this._session); - - this._resetWidget(); - - this._strategy?.dispose(); - this._strategy = undefined; - this._session = undefined; - } - - private async[State.CANCEL]() { - if (this._session) { - // assertType(this._session); - assertType(this._strategy); - this._sessionStore.clear(); - - // only stash sessions that were not unstashed, not "empty", and not interacted with - const shouldStash = !this._session.isUnstashed && !!this._session.lastExchange && this._session.hunkData.size === this._session.hunkData.pending; - let undoCancelEdits: IValidEditOperation[] = []; - try { - undoCancelEdits = this._strategy.cancel(); - } catch (err) { - this._dialogService.error(localize('err.discard', "Failed to discard changes.", toErrorMessage(err))); - this._log('FAILED to discard changes'); - this._log(err); - } - - this._stashedSession.clear(); - if (shouldStash) { - this._stashedSession.value = this._inlineChatSessionService.stashSession(this._session, this._editor, undoCancelEdits); - } else { - this._inlineChatSessionService.releaseSession(this._session); - } - } - - this._resetWidget(); - - this._strategy?.dispose(); - this._strategy = undefined; - this._session = undefined; - } - - // ---- - - private _showWidget(initialRender: boolean = false, position?: Position) { - assertType(this._editor.hasModel()); - this._ctxVisible.set(true); - - let widgetPosition: Position; - if (position) { - // explicit position wins - widgetPosition = position; - } else if (this._ui.rawValue?.zone?.position) { - // already showing - special case of line 1 - if (this._ui.rawValue?.zone.position.lineNumber === 1) { - widgetPosition = this._ui.rawValue?.zone.position.delta(-1); - } else { - widgetPosition = this._ui.rawValue?.zone.position; - } - } else { - // default to ABOVE the selection - widgetPosition = this._editor.getSelection().getStartPosition().delta(-1); - } - - if (this._session && !position && (this._session.hasChangedText || this._session.lastExchange)) { - widgetPosition = this._session.wholeRange.value.getStartPosition().delta(-1); - } - - if (this._ui.rawValue?.zone?.position) { - this._ui.value.zone.updatePositionAndHeight(widgetPosition); - - } else if (initialRender) { - const selection = this._editor.getSelection(); - widgetPosition = selection.getStartPosition(); - this._ui.value.content.show(widgetPosition); - - } else { - this._ui.value.content.hide(); - this._ui.value.zone.show(widgetPosition); - if (this._session) { - this._ui.value.zone.widget.setChatModel(this._session.chatModel); - } - } - - return widgetPosition; - } - - private _resetWidget() { - this._sessionStore.clear(); - this._ctxVisible.reset(); - this._ctxUserDidEdit.reset(); - - this._ui.rawValue?.content.hide(); - this._ui.rawValue?.zone?.hide(); - - // Return focus to the editor only if the current focus is within the editor widget - if (this._editor.hasWidgetFocus()) { - this._editor.focus(); - } - } - - private _updateCtxResponseType(): void { - - if (!this._session) { - this._ctxResponseType.set(InlineChatResponseType.None); - return; - } - - const hasLocalEdit = (response: IResponse): boolean => { - return response.value.some(part => part.kind === 'textEditGroup' && isEqual(part.uri, this._session?.textModelN.uri)); - }; - - let responseType = InlineChatResponseType.None; - for (const request of this._session.chatModel.getRequests()) { - if (!request.response) { - continue; - } - responseType = InlineChatResponseType.Messages; - if (hasLocalEdit(request.response.response)) { - responseType = InlineChatResponseType.MessagesAndEdits; - break; // no need to check further - } - } - this._ctxResponseType.set(responseType); - } - - private async _makeChanges(edits: TextEdit[], opts: ProgressingEditsOptions | undefined, undoStopBefore: boolean) { - assertType(this._session); - assertType(this._strategy); - - const moreMinimalEdits = await this._editorWorkerService.computeMoreMinimalEdits(this._session.textModelN.uri, edits); - this._log('edits from PROVIDER and after making them MORE MINIMAL', this._session.agent.extensionId, edits, moreMinimalEdits); - - if (moreMinimalEdits?.length === 0) { - // nothing left to do - return; - } - - const actualEdits = !opts && moreMinimalEdits ? moreMinimalEdits : edits; - const editOperations = actualEdits.map(TextEdit.asEditOperation); - - const editsObserver: IEditObserver = { - start: () => this._session!.hunkData.ignoreTextModelNChanges = true, - stop: () => this._session!.hunkData.ignoreTextModelNChanges = false, - }; - - this._inlineChatSavingService.markChanged(this._session); - if (opts) { - await this._strategy.makeProgressiveChanges(editOperations, editsObserver, opts, undoStopBefore); - } else { - await this._strategy.makeChanges(editOperations, editsObserver, undoStopBefore); - } - } - - private _forcedPlaceholder: string | undefined = undefined; - - private _updatePlaceholder(): void { - this._ui.value.zone.widget.placeholder = this._getPlaceholderText(); - } - - private _getPlaceholderText(): string { - return this._forcedPlaceholder ?? this._session?.agent.description ?? ''; - } - - // ---- controller API - - showSaveHint(): void { - const status = localize('savehint', "Accept or discard changes to continue saving"); - this._ui.value.zone.widget.updateStatus(status, { classes: ['warn'] }); - } - - acceptInput() { - return this.chatWidget.acceptInput(); - } - - updateInput(text: string, selectAll = true): void { - - this._ui.value.content.chatWidget.setInput(text); - this._ui.value.zone.widget.chatWidget.setInput(text); - if (selectAll) { - const newSelection = new Selection(1, 1, Number.MAX_SAFE_INTEGER, 1); - this._ui.value.content.chatWidget.inputEditor.setSelection(newSelection); - this._ui.value.zone.widget.chatWidget.inputEditor.setSelection(newSelection); - } - } - - cancelCurrentRequest(): void { - this._messages.fire(Message.CANCEL_INPUT | Message.CANCEL_REQUEST); - } - - arrowOut(up: boolean): void { - if (this._ui.value.zone.position && this._editor.hasModel()) { - const { column } = this._editor.getPosition(); - const { lineNumber } = this._ui.value.zone.position; - const newLine = up ? lineNumber : lineNumber + 1; - this._editor.setPosition({ lineNumber: newLine, column }); - this._editor.focus(); - } - } - - focus(): void { - this._ui.value.zone.widget.focus(); - } - - hasFocus(): boolean { - return this._ui.value.zone.widget.hasFocus(); - } - - moveHunk(next: boolean) { - this.focus(); - this._strategy?.move?.(next); - } - - async viewInChat() { - if (!this._strategy || !this._session) { - return; - } - - let someApplied = false; - let lastEdit: IChatTextEditGroup | undefined; - - const uri = this._editor.getModel()?.uri; - const requests = this._session.chatModel.getRequests(); - for (const request of requests) { - if (!request.response) { - continue; - } - for (const part of request.response.response.value) { - if (part.kind === 'textEditGroup' && isEqual(part.uri, uri)) { - // fully or partially applied edits - someApplied = someApplied || Boolean(part.state?.applied); - lastEdit = part; - } - } - } - - const doEdits = this._strategy.cancel(); - - if (someApplied) { - assertType(lastEdit); - lastEdit.edits = [doEdits]; - } - - await this._instaService.invokeFunction(moveToPanelChat, this._session?.chatModel); - - this.cancelSession(); - } - - toggleDiff() { - this._strategy?.toggleDiff?.(); - } - - acceptSession(): void { - if (this._session?.lastExchange?.response instanceof ReplyResponse && this._session?.lastExchange?.response.chatResponse) { - const response = this._session?.lastExchange?.response.chatResponse; - this._chatService.notifyUserAction({ - sessionId: this._session.chatModel.sessionId, - requestId: response.requestId, - agentId: response.agent?.id, - result: response.result, - action: { - kind: 'inlineChat', - action: 'accepted' - } - }); - } - this._messages.fire(Message.ACCEPT_SESSION); - } - - acceptHunk() { - return this._strategy?.acceptHunk(); - } - - discardHunk() { - return this._strategy?.discardHunk(); - } - - async cancelSession() { - - if (this._session?.lastExchange?.response instanceof ReplyResponse && this._session?.lastExchange?.response.chatResponse) { - const response = this._session?.lastExchange?.response.chatResponse; - this._chatService.notifyUserAction({ - sessionId: this._session.chatModel.sessionId, - requestId: response.requestId, - agentId: response.agent?.id, - result: response.result, - action: { - kind: 'inlineChat', - action: 'discarded' - } - }); - } - - this._messages.fire(Message.CANCEL_SESSION); - } - - finishExistingSession(): void { - if (this._session) { - if (this._session.editMode === EditMode.Preview) { - this._log('finishing existing session, using CANCEL', this._session.editMode); - this.cancelSession(); - } else { - this._log('finishing existing session, using APPLY', this._session.editMode); - this.acceptSession(); - } - } - } - - unstashLastSession(): Session | undefined { - const result = this._stashedSession.value?.unstash(); - if (result) { - this._inlineChatSavingService.markChanged(result); - } - return result; - } - - joinCurrentRun(): Promise | undefined { - return this._currentRun; - } -} - -async function moveToPanelChat(accessor: ServicesAccessor, model: ChatModel | undefined) { - - const viewsService = accessor.get(IViewsService); - const chatService = accessor.get(IAideChatService); - - const widget = await showChatView(viewsService); - - if (widget && widget.viewModel && model) { - for (const request of model.getRequests().slice()) { - await chatService.adoptRequest(widget.viewModel.model.sessionId, request); - } - widget.focusLastMessage(); - } -} diff --git a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatNotebook.ts b/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatNotebook.ts deleted file mode 100644 index 32720bb1a49..00000000000 --- a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatNotebook.ts +++ /dev/null @@ -1,96 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { illegalState } from '../../../../base/common/errors.js'; -import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { isEqual } from '../../../../base/common/resources.js'; -import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { InlineChatController } from '../../../../workbench/contrib/inlineAideChat/browser/inlineChatController.js'; -import { IInlineAideChatSessionService } from './inlineChatSessionService.js'; -import { INotebookEditorService } from '../../../../workbench/contrib/notebook/browser/services/notebookEditorService.js'; -import { CellUri } from '../../../../workbench/contrib/notebook/common/notebookCommon.js'; - -export class InlineChatNotebookContribution { - - private readonly _store = new DisposableStore(); - - constructor( - @IInlineAideChatSessionService sessionService: IInlineAideChatSessionService, - @INotebookEditorService notebookEditorService: INotebookEditorService, - ) { - - this._store.add(sessionService.registerSessionKeyComputer(Schemas.vscodeNotebookCell, { - getComparisonKey: (editor, uri) => { - const data = CellUri.parse(uri); - if (!data) { - throw illegalState('Expected notebook cell uri'); - } - let fallback: string | undefined; - for (const notebookEditor of notebookEditorService.listNotebookEditors()) { - if (notebookEditor.hasModel() && isEqual(notebookEditor.textModel.uri, data.notebook)) { - - const candidate = `${notebookEditor.getId()}#${uri}`; - - if (!fallback) { - fallback = candidate; - } - - // find the code editor in the list of cell-code editors - if (notebookEditor.codeEditors.find((tuple) => tuple[1] === editor)) { - return candidate; - } - - // // reveal cell and try to find code editor again - // const cell = notebookEditor.getCellByHandle(data.handle); - // if (cell) { - // notebookEditor.revealInViewAtTop(cell); - // if (notebookEditor.codeEditors.find((tuple) => tuple[1] === editor)) { - // return candidate; - // } - // } - } - } - - if (fallback) { - return fallback; - } - - throw illegalState('Expected notebook editor'); - } - })); - - this._store.add(sessionService.onWillStartSession(newSessionEditor => { - const candidate = CellUri.parse(newSessionEditor.getModel().uri); - if (!candidate) { - return; - } - for (const notebookEditor of notebookEditorService.listNotebookEditors()) { - if (isEqual(notebookEditor.textModel?.uri, candidate.notebook)) { - let found = false; - const editors: ICodeEditor[] = []; - for (const [, codeEditor] of notebookEditor.codeEditors) { - editors.push(codeEditor); - found = codeEditor === newSessionEditor || found; - } - if (found) { - // found the this editor in the outer notebook editor -> make sure to - // cancel all sibling sessions - for (const editor of editors) { - if (editor !== newSessionEditor) { - InlineChatController.get(editor)?.finishExistingSession(); - } - } - break; - } - } - } - })); - } - - dispose(): void { - this._store.dispose(); - } -} diff --git a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatSavingService.ts b/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatSavingService.ts deleted file mode 100644 index f22454d96d3..00000000000 --- a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatSavingService.ts +++ /dev/null @@ -1,16 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { Session } from '../../../../workbench/contrib/inlineAideChat/browser/inlineChatSession.js'; - - -export const IInlineAideChatSavingService = createDecorator('IInlineAideChatSavingService'); - -export interface IInlineAideChatSavingService { - _serviceBrand: undefined; - - markChanged(session: Session): void; - -} diff --git a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatSavingServiceImpl.ts b/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatSavingServiceImpl.ts deleted file mode 100644 index 2b217e9670c..00000000000 --- a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatSavingServiceImpl.ts +++ /dev/null @@ -1,285 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Queue, raceCancellation } from '../../../../base/common/async.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { DisposableStore, IDisposable, MutableDisposable, combinedDisposable, dispose } from '../../../../base/common/lifecycle.js'; -import { ICodeEditor, isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { localize } from '../../../../nls.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IProgress, IProgressStep } from '../../../../platform/progress/common/progress.js'; -import { SaveReason } from '../../../../workbench/common/editor.js'; -import { Session } from '../../../../workbench/contrib/inlineAideChat/browser/inlineChatSession.js'; -import { IInlineAideChatSessionService } from './inlineChatSessionService.js'; -import { InlineChatConfigKeys } from '../../../../workbench/contrib/inlineAideChat/common/inlineChat.js'; -import { GroupsOrder, IEditorGroup, IEditorGroupsService } from '../../../../workbench/services/editor/common/editorGroupsService.js'; -import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; -import { IFilesConfigurationService } from '../../../../workbench/services/filesConfiguration/common/filesConfigurationService.js'; -import { ITextFileService } from '../../../../workbench/services/textfile/common/textfiles.js'; -import { IInlineAideChatSavingService } from './inlineChatSavingService.js'; -import { Iterable } from '../../../../base/common/iterator.js'; -import { IResourceEditorInput } from '../../../../platform/editor/common/editor.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { CellUri } from '../../../../workbench/contrib/notebook/common/notebookCommon.js'; -import { getNotebookEditorFromEditorPane } from '../../../../workbench/contrib/notebook/browser/notebookBrowser.js'; -import { compare } from '../../../../base/common/strings.js'; -import { IWorkingCopyFileService } from '../../../../workbench/services/workingCopy/common/workingCopyFileService.js'; -import { URI } from '../../../../base/common/uri.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { Event } from '../../../../base/common/event.js'; -import { InlineChatController } from '../../../../workbench/contrib/inlineAideChat/browser/inlineChatController.js'; - -interface SessionData { - readonly resourceUri: URI; - readonly dispose: () => void; - readonly session: Session; - readonly groupCandidate: IEditorGroup; -} - -export class InlineChatSavingServiceImpl implements IInlineAideChatSavingService { - - declare readonly _serviceBrand: undefined; - - private readonly _store = new DisposableStore(); - private readonly _saveParticipant = this._store.add(new MutableDisposable()); - private readonly _sessionData = new Map(); - - constructor( - @IFilesConfigurationService private readonly _fileConfigService: IFilesConfigurationService, - @IEditorGroupsService private readonly _editorGroupService: IEditorGroupsService, - @ITextFileService private readonly _textFileService: ITextFileService, - @IEditorService private readonly _editorService: IEditorService, - @IInlineAideChatSessionService private readonly _inlineChatSessionService: IInlineAideChatSessionService, - @IConfigurationService private readonly _configService: IConfigurationService, - @IWorkingCopyFileService private readonly _workingCopyFileService: IWorkingCopyFileService, - @ILogService private readonly _logService: ILogService, - ) { - this._store.add(Event.any(_inlineChatSessionService.onDidEndSession, _inlineChatSessionService.onDidStashSession)(e => { - this._sessionData.get(e.session)?.dispose(); - })); - } - - dispose(): void { - this._store.dispose(); - dispose(this._sessionData.values()); - } - - markChanged(session: Session): void { - if (!this._sessionData.has(session)) { - - let uri = session.targetUri; - - // notebooks: use the notebook-uri because saving happens on the notebook-level - if (uri.scheme === Schemas.vscodeNotebookCell) { - const data = CellUri.parse(uri); - if (!data) { - return; - } - uri = data?.notebook; - } - - if (this._sessionData.size === 0) { - this._installSaveParticpant(); - } - - const saveConfigOverride = this._fileConfigService.disableAutoSave(uri); - this._sessionData.set(session, { - resourceUri: uri, - groupCandidate: this._editorGroupService.activeGroup, - session, - dispose: () => { - saveConfigOverride.dispose(); - this._sessionData.delete(session); - if (this._sessionData.size === 0) { - this._saveParticipant.clear(); - } - } - }); - } - } - - private _installSaveParticpant(): void { - - const queue = new Queue(); - - const d1 = this._textFileService.files.addSaveParticipant({ - participate: (model, ctx, progress, token) => { - return queue.queue(() => this._participate(ctx.savedFrom ?? model.textEditorModel?.uri, ctx.reason, progress, token)); - } - }); - const d2 = this._workingCopyFileService.addSaveParticipant({ - participate: (workingCopy, ctx, progress, token) => { - return queue.queue(() => this._participate(ctx.savedFrom ?? workingCopy.resource, ctx.reason, progress, token)); - } - }); - this._saveParticipant.value = combinedDisposable(d1, d2, queue); - } - - private async _participate(uri: URI | undefined, reason: SaveReason, progress: IProgress, token: CancellationToken): Promise { - - if (reason !== SaveReason.EXPLICIT) { - // all saves that we are concerned about are explicit - // because we have disabled auto-save for them - return; - } - - if (!this._configService.getValue(InlineChatConfigKeys.AcceptedOrDiscardBeforeSave)) { - // disabled - return; - } - - const sessions = new Map(); - for (const [session, data] of this._sessionData) { - if (uri?.toString() === data.resourceUri.toString()) { - sessions.set(session, data); - } - } - - if (sessions.size === 0) { - return; - } - - progress.report({ - message: sessions.size === 1 - ? localize('inlineChat', "Waiting for Inline Chat changes to be Accepted or Discarded...") - : localize('inlineChat.N', "Waiting for Inline Chat changes in {0} editors to be Accepted or Discarded...", sessions.size) - }); - - // reveal all sessions in order and also show dangling sessions - const { groups, orphans } = this._getGroupsAndOrphans(sessions.values()); - const editorsOpenedAndSessionsEnded = this._openAndWait(groups, token).then(() => { - if (token.isCancellationRequested) { - return; - } - return this._openAndWait(Iterable.map(orphans, s => [this._editorGroupService.activeGroup, s]), token); - }); - - // fallback: resolve when all sessions for this model have been resolved. this is independent of the editor opening - const allSessionsEnded = this._whenSessionsEnded(Iterable.concat(groups.map(tuple => tuple[1]), orphans), token); - - await Promise.race([allSessionsEnded, editorsOpenedAndSessionsEnded]); - } - - private _getGroupsAndOrphans(sessions: Iterable) { - - const groupByEditor = new Map(); - for (const group of this._editorGroupService.getGroups(GroupsOrder.MOST_RECENTLY_ACTIVE)) { - const candidate = group.activeEditorPane?.getControl(); - if (isCodeEditor(candidate)) { - groupByEditor.set(candidate, group); - } - } - - const groups: [IEditorGroup, SessionData][] = []; - const orphans = new Set(); - - for (const data of sessions) { - - const editor = this._inlineChatSessionService.getCodeEditor(data.session); - const group = groupByEditor.get(editor); - if (group) { - // there is only one session per group because all sessions have the same model - // because we save one file. - groups.push([group, data]); - } else if (this._editorGroupService.groups.includes(data.groupCandidate)) { - // the group candidate is still there. use it - groups.push([data.groupCandidate, data]); - } else { - orphans.add(data); - } - } - return { groups, orphans }; - } - - private async _openAndWait(groups: Iterable<[IEditorGroup, SessionData]>, token: CancellationToken) { - - const dataByGroup = new Map(); - for (const [group, data] of groups) { - let array = dataByGroup.get(group); - if (!array) { - array = []; - dataByGroup.set(group, array); - } - array.push(data); - } - - for (const [group, array] of dataByGroup) { - - if (token.isCancellationRequested) { - break; - } - - array.sort((a, b) => compare(a.session.targetUri.toString(), b.session.targetUri.toString())); - - - for (const data of array) { - - const input: IResourceEditorInput = { resource: data.resourceUri }; - const pane = await this._editorService.openEditor(input, group); - let editor: ICodeEditor | undefined; - if (data.session.targetUri.scheme === Schemas.vscodeNotebookCell) { - const notebookEditor = getNotebookEditorFromEditorPane(pane); - const uriData = CellUri.parse(data.session.targetUri); - if (notebookEditor && notebookEditor.hasModel() && uriData) { - const cell = notebookEditor.getCellByHandle(uriData.handle); - if (cell) { - await notebookEditor.revealRangeInCenterIfOutsideViewportAsync(cell, data.session.wholeRange.value); - } - const tuple = notebookEditor.codeEditors.find(tuple => tuple[1].getModel()?.uri.toString() === data.session.targetUri.toString()); - editor = tuple?.[1]; - } - - } else { - if (isCodeEditor(pane?.getControl())) { - editor = pane.getControl(); - } - } - - if (!editor) { - // PANIC - break; - } - this._inlineChatSessionService.moveSession(data.session, editor); - InlineChatController.get(editor)?.showSaveHint(); - this._logService.info('WAIT for session to end', editor.getId(), data.session.targetUri.toString()); - await this._whenSessionsEnded(Iterable.single(data), token); - } - } - } - - private async _whenSessionsEnded(iterable: Iterable, token: CancellationToken) { - - const sessions = new Map(); - for (const item of iterable) { - sessions.set(item.session, item); - } - - if (sessions.size === 0) { - // nothing to do - return; - } - - let listener: IDisposable | undefined; - - const whenEnded = new Promise(resolve => { - listener = Event.any(this._inlineChatSessionService.onDidEndSession, this._inlineChatSessionService.onDidStashSession)(e => { - const data = sessions.get(e.session); - if (data) { - data.dispose(); - sessions.delete(e.session); - if (sessions.size === 0) { - resolve(); // DONE, release waiting - } - } - }); - }); - - try { - await raceCancellation(whenEnded, token); - } finally { - listener?.dispose(); - } - } -} diff --git a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatSession.ts b/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatSession.ts deleted file mode 100644 index 40a6bc66720..00000000000 --- a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatSession.ts +++ /dev/null @@ -1,745 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { URI } from '../../../../base/common/uri.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { TextEdit } from '../../../../editor/common/languages.js'; -import { IIdentifiedSingleEditOperation, IModelDecorationOptions, IModelDeltaDecoration, ITextModel, IValidEditOperation, TrackedRangeStickiness } from '../../../../editor/common/model.js'; -import { EditMode, CTX_INLINE_CHAT_HAS_STASHED_SESSION } from '../../../../workbench/contrib/inlineAideChat/common/inlineChat.js'; -import { IRange, Range } from '../../../../editor/common/core/range.js'; -import { ModelDecorationOptions } from '../../../../editor/common/model/textModel.js'; -import { toErrorMessage } from '../../../../base/common/errorMessage.js'; -import { isCancellationError } from '../../../../base/common/errors.js'; -import { EditOperation, ISingleEditOperation } from '../../../../editor/common/core/editOperation.js'; -import { DetailedLineRangeMapping, LineRangeMapping, RangeMapping } from '../../../../editor/common/diff/rangeMapping.js'; -import { IUntitledTextEditorModel } from '../../../../workbench/services/untitled/common/untitledTextEditorModel.js'; -import { ITextFileService } from '../../../../workbench/services/textfile/common/textfiles.js'; -import { ILanguageService } from '../../../../editor/common/languages/language.js'; -import { ResourceMap } from '../../../../base/common/map.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { isEqual } from '../../../../base/common/resources.js'; -import { IInlineAideChatSessionService, Recording } from './inlineChatSessionService.js'; -import { LineRange } from '../../../../editor/common/core/lineRange.js'; -import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js'; -import { coalesceInPlace } from '../../../../base/common/arrays.js'; -import { Iterable } from '../../../../base/common/iterator.js'; -import { IModelContentChangedEvent } from '../../../../editor/common/textModelEvents.js'; -import { DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js'; -import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { ChatModel, IChatRequestModel, IChatResponseModel, IChatTextEditGroupState } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { ExtensionIdentifier } from '../../../../platform/extensions/common/extensions.js'; -import { IChatAgent } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { IDocumentDiff } from '../../../../editor/common/diff/documentDiffProvider.js'; - - -export type TelemetryData = { - extension: string; - rounds: string; - undos: string; - unstashed: number; - edits: number; - finishedByEdit: boolean; - startTime: string; - endTime: string; - editMode: string; - acceptedHunks: number; - discardedHunks: number; - responseTypes: string; -}; - -export type TelemetryDataClassification = { - owner: 'jrieken'; - comment: 'Data about an interaction editor session'; - extension: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The extension providing the data' }; - rounds: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of request that were made' }; - undos: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Requests that have been undone' }; - edits: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Did edits happen while the session was active' }; - unstashed: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'How often did this session become stashed and resumed' }; - finishedByEdit: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Did edits cause the session to terminate' }; - startTime: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'When the session started' }; - endTime: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'When the session ended' }; - editMode: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'What edit mode was choosen: live, livePreview, preview' }; - acceptedHunks: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of accepted hunks' }; - discardedHunks: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of discarded hunks' }; - responseTypes: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Comma separated list of response types like edits, message, mixed' }; -}; - - -export class SessionWholeRange { - - private static readonly _options: IModelDecorationOptions = ModelDecorationOptions.register({ description: 'inlineChat/session/wholeRange' }); - - private readonly _onDidChange = new Emitter(); - readonly onDidChange: Event = this._onDidChange.event; - - private _decorationIds: string[] = []; - - constructor(private readonly _textModel: ITextModel, wholeRange: IRange) { - this._decorationIds = _textModel.deltaDecorations([], [{ range: wholeRange, options: SessionWholeRange._options }]); - } - - dispose() { - this._onDidChange.dispose(); - if (!this._textModel.isDisposed()) { - this._textModel.deltaDecorations(this._decorationIds, []); - } - } - - fixup(changes: readonly DetailedLineRangeMapping[]): void { - - const newDeco: IModelDeltaDecoration[] = []; - for (const { modified } of changes) { - const modifiedRange = modified.isEmpty - ? new Range(modified.startLineNumber, 1, modified.startLineNumber, this._textModel.getLineLength(modified.startLineNumber)) - : new Range(modified.startLineNumber, 1, modified.endLineNumberExclusive - 1, this._textModel.getLineLength(modified.endLineNumberExclusive - 1)); - - newDeco.push({ range: modifiedRange, options: SessionWholeRange._options }); - } - const [first, ...rest] = this._decorationIds; // first is the original whole range - const newIds = this._textModel.deltaDecorations(rest, newDeco); - this._decorationIds = [first].concat(newIds); - this._onDidChange.fire(this); - } - - get trackedInitialRange(): Range { - const [first] = this._decorationIds; - return this._textModel.getDecorationRange(first) ?? new Range(1, 1, 1, 1); - } - - get value(): Range { - let result: Range | undefined; - for (const id of this._decorationIds) { - const range = this._textModel.getDecorationRange(id); - if (range) { - if (!result) { - result = range; - } else { - result = Range.plusRange(result, range); - } - } - } - return result!; - } -} - -export class Session { - - private _lastInput: SessionPrompt | undefined; - private _isUnstashed: boolean = false; - private readonly _exchange: SessionExchange[] = []; - private readonly _startTime = new Date(); - private readonly _teldata: TelemetryData; - - readonly textModelNAltVersion: number; - - constructor( - readonly editMode: EditMode, - /** - * The URI of the document which is being EditorEdit - */ - readonly targetUri: URI, - /** - * A copy of the document at the time the session was started - */ - readonly textModel0: ITextModel, - /** - * The document into which AI edits went, when live this is `targetUri` otherwise it is a temporary document - */ - readonly textModelN: ITextModel, - readonly agent: IChatAgent, - readonly wholeRange: SessionWholeRange, - readonly hunkData: HunkData, - readonly chatModel: ChatModel, - ) { - this.textModelNAltVersion = textModelN.getAlternativeVersionId(); - this._teldata = { - extension: ExtensionIdentifier.toKey(agent.extensionId), - startTime: this._startTime.toISOString(), - endTime: this._startTime.toISOString(), - edits: 0, - finishedByEdit: false, - rounds: '', - undos: '', - editMode, - unstashed: 0, - acceptedHunks: 0, - discardedHunks: 0, - responseTypes: '' - }; - } - - addInput(input: SessionPrompt): void { - this._lastInput = input; - } - - get lastInput() { - return this._lastInput; - } - - get isUnstashed(): boolean { - return this._isUnstashed; - } - - markUnstashed() { - this._teldata.unstashed! += 1; - this._isUnstashed = true; - } - - addExchange(exchange: SessionExchange): void { - this._isUnstashed = false; - const newLen = this._exchange.push(exchange); - this._teldata.rounds += `${newLen}|`; - // this._teldata.responseTypes += `${exchange.response instanceof ReplyResponse ? exchange.response.responseType : InlineChatResponseTypes.Empty}|`; - } - - get exchanges(): readonly SessionExchange[] { - return this._exchange; - } - - get lastExchange(): SessionExchange | undefined { - return this._exchange[this._exchange.length - 1]; - } - - get hasChangedText(): boolean { - return !this.textModel0.equalsTextBuffer(this.textModelN.getTextBuffer()); - } - - asChangedText(changes: readonly LineRangeMapping[]): string | undefined { - if (changes.length === 0) { - return undefined; - } - - let startLine = Number.MAX_VALUE; - let endLine = Number.MIN_VALUE; - for (const change of changes) { - startLine = Math.min(startLine, change.modified.startLineNumber); - endLine = Math.max(endLine, change.modified.endLineNumberExclusive); - } - - return this.textModelN.getValueInRange(new Range(startLine, 1, endLine, Number.MAX_VALUE)); - } - - recordExternalEditOccurred(didFinish: boolean) { - this._teldata.edits += 1; - this._teldata.finishedByEdit = didFinish; - } - - asTelemetryData(): TelemetryData { - - for (const item of this.hunkData.getInfo()) { - switch (item.getState()) { - case HunkState.Accepted: - this._teldata.acceptedHunks += 1; - break; - case HunkState.Rejected: - this._teldata.discardedHunks += 1; - break; - } - } - - this._teldata.endTime = new Date().toISOString(); - return this._teldata; - } - - asRecording(): Recording { - const result: Recording = { - session: this.chatModel.sessionId, - when: this._startTime, - exchanges: [] - }; - for (const exchange of this._exchange) { - const response = exchange.response; - if (response instanceof ReplyResponse) { - result.exchanges.push({ prompt: exchange.prompt.value, res: response.chatResponse }); - } - } - return result; - } -} - - -export class SessionPrompt { - - readonly value: string; - - constructor( - readonly request: IChatRequestModel, - readonly modelAltVersionId: number, - ) { - this.value = request.message.text; - } -} - -export class SessionExchange { - - constructor( - readonly prompt: SessionPrompt, - readonly response: ReplyResponse | EmptyResponse | ErrorResponse - ) { } -} - -export class EmptyResponse { - -} - -export class ErrorResponse { - - readonly message: string; - readonly isCancellation: boolean; - - constructor( - readonly error: any - ) { - this.message = toErrorMessage(error, false); - this.isCancellation = isCancellationError(error); - } -} - -export class ReplyResponse { - - readonly untitledTextModel: IUntitledTextEditorModel | undefined; - - constructor( - localUri: URI, - readonly chatRequest: IChatRequestModel, - readonly chatResponse: IChatResponseModel, - @ITextFileService private readonly _textFileService: ITextFileService, - @ILanguageService private readonly _languageService: ILanguageService, - ) { - - const editsMap = new ResourceMap(); - - for (const item of chatResponse.response.value) { - if (item.kind === 'textEditGroup') { - const array = editsMap.get(item.uri); - for (const group of item.edits) { - if (array) { - array.push(group); - } else { - editsMap.set(item.uri, [group]); - } - } - } - } - - for (const [uri, edits] of editsMap) { - - const flatEdits = edits.flat(); - if (flatEdits.length === 0) { - editsMap.delete(uri); - continue; - } - - const isLocalUri = isEqual(uri, localUri); - if (uri.scheme === Schemas.untitled && !isLocalUri && !this.untitledTextModel) { //TODO@jrieken the first untitled model WINS - const langSelection = this._languageService.createByFilepathOrFirstLine(uri, undefined); - const untitledTextModel = this._textFileService.untitled.create({ - associatedResource: uri, - languageId: langSelection.languageId - }); - this.untitledTextModel = untitledTextModel; - untitledTextModel.resolve(); - } - } - } -} - -export class StashedSession { - - private readonly _listener: IDisposable; - private readonly _ctxHasStashedSession: IContextKey; - private _session: Session | undefined; - - constructor( - editor: ICodeEditor, - session: Session, - private readonly _undoCancelEdits: IValidEditOperation[], - @IContextKeyService contextKeyService: IContextKeyService, - @IInlineAideChatSessionService private readonly _sessionService: IInlineAideChatSessionService, - @ILogService private readonly _logService: ILogService - ) { - this._ctxHasStashedSession = CTX_INLINE_CHAT_HAS_STASHED_SESSION.bindTo(contextKeyService); - - // keep session for a little bit, only release when user continues to work (type, move cursor, etc.) - this._session = session; - this._ctxHasStashedSession.set(true); - this._listener = Event.once(Event.any(editor.onDidChangeCursorSelection, editor.onDidChangeModelContent, editor.onDidChangeModel))(() => { - this._session = undefined; - this._sessionService.releaseSession(session); - this._ctxHasStashedSession.reset(); - }); - } - - dispose() { - this._listener.dispose(); - this._ctxHasStashedSession.reset(); - if (this._session) { - this._sessionService.releaseSession(this._session); - } - } - - unstash(): Session | undefined { - if (!this._session) { - return undefined; - } - this._listener.dispose(); - const result = this._session; - result.markUnstashed(); - result.hunkData.ignoreTextModelNChanges = true; - result.textModelN.pushEditOperations(null, this._undoCancelEdits, () => null); - result.hunkData.ignoreTextModelNChanges = false; - this._session = undefined; - this._logService.debug('[IE] Unstashed session'); - return result; - } -} - -// --- - -function lineRangeAsRange(lineRange: LineRange, model: ITextModel): Range { - return lineRange.isEmpty - ? new Range(lineRange.startLineNumber, 1, lineRange.startLineNumber, model.getLineLength(lineRange.startLineNumber)) - : new Range(lineRange.startLineNumber, 1, lineRange.endLineNumberExclusive - 1, model.getLineLength(lineRange.endLineNumberExclusive - 1)); -} - -export class HunkData { - - private static readonly _HUNK_TRACKED_RANGE = ModelDecorationOptions.register({ - description: 'inline-chat-hunk-tracked-range', - stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges - }); - - private static readonly _HUNK_THRESHOLD = 8; - - private readonly _store = new DisposableStore(); - private readonly _data = new Map(); - private _ignoreChanges: boolean = false; - - constructor( - @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, - private readonly _textModel0: ITextModel, - private readonly _textModelN: ITextModel, - ) { - - this._store.add(_textModelN.onDidChangeContent(e => { - if (!this._ignoreChanges) { - this._mirrorChanges(e); - } - })); - } - - dispose(): void { - if (!this._textModelN.isDisposed()) { - this._textModelN.changeDecorations(accessor => { - for (const { textModelNDecorations } of this._data.values()) { - textModelNDecorations.forEach(accessor.removeDecoration, accessor); - } - }); - } - if (!this._textModel0.isDisposed()) { - this._textModel0.changeDecorations(accessor => { - for (const { textModel0Decorations } of this._data.values()) { - textModel0Decorations.forEach(accessor.removeDecoration, accessor); - } - }); - } - this._data.clear(); - this._store.dispose(); - } - - set ignoreTextModelNChanges(value: boolean) { - this._ignoreChanges = value; - } - - get ignoreTextModelNChanges(): boolean { - return this._ignoreChanges; - } - - private _mirrorChanges(event: IModelContentChangedEvent) { - - // mirror textModelN changes to textModel0 execept for those that - // overlap with a hunk - - type HunkRangePair = { rangeN: Range; range0: Range }; - const hunkRanges: HunkRangePair[] = []; - - const ranges0: Range[] = []; - - for (const { textModelNDecorations, textModel0Decorations, state } of this._data.values()) { - - if (state === HunkState.Pending) { - // pending means the hunk's changes aren't "sync'd" yet - for (let i = 1; i < textModelNDecorations.length; i++) { - const rangeN = this._textModelN.getDecorationRange(textModelNDecorations[i]); - const range0 = this._textModel0.getDecorationRange(textModel0Decorations[i]); - if (rangeN && range0) { - hunkRanges.push({ rangeN, range0 }); - } - } - - } else if (state === HunkState.Accepted) { - // accepted means the hunk's changes are also in textModel0 - for (let i = 1; i < textModel0Decorations.length; i++) { - const range = this._textModel0.getDecorationRange(textModel0Decorations[i]); - if (range) { - ranges0.push(range); - } - } - } - } - - hunkRanges.sort((a, b) => Range.compareRangesUsingStarts(a.rangeN, b.rangeN)); - ranges0.sort(Range.compareRangesUsingStarts); - - const edits: IIdentifiedSingleEditOperation[] = []; - - for (const change of event.changes) { - - let isOverlapping = false; - - let pendingChangesLen = 0; - - for (const { rangeN, range0 } of hunkRanges) { - if (rangeN.getEndPosition().isBefore(Range.getStartPosition(change.range))) { - // pending hunk _before_ this change. When projecting into textModel0 we need to - // subtract that. Because diffing is relaxed it might include changes that are not - // actual insertions/deletions. Therefore we need to take the length of the original - // range into account. - pendingChangesLen += this._textModelN.getValueLengthInRange(rangeN); - pendingChangesLen -= this._textModel0.getValueLengthInRange(range0); - - } else if (Range.areIntersectingOrTouching(rangeN, change.range)) { - isOverlapping = true; - break; - - } else { - // hunks past this change aren't relevant - break; - } - } - - if (isOverlapping) { - // hunk overlaps, it grew - continue; - } - - const offset0 = change.rangeOffset - pendingChangesLen; - const start0 = this._textModel0.getPositionAt(offset0); - - let acceptedChangesLen = 0; - for (const range of ranges0) { - if (range.getEndPosition().isBefore(start0)) { - // accepted hunk _before_ this projected change. When projecting into textModel0 - // we need to add that - acceptedChangesLen += this._textModel0.getValueLengthInRange(range); - } - } - - const start = this._textModel0.getPositionAt(offset0 + acceptedChangesLen); - const end = this._textModel0.getPositionAt(offset0 + acceptedChangesLen + change.rangeLength); - edits.push(EditOperation.replace(Range.fromPositions(start, end), change.text)); - } - - this._textModel0.pushEditOperations(null, edits, () => null); - } - - async recompute(editState: IChatTextEditGroupState, diff?: IDocumentDiff | null) { - - diff ??= await this._editorWorkerService.computeDiff(this._textModel0.uri, this._textModelN.uri, { ignoreTrimWhitespace: false, maxComputationTimeMs: Number.MAX_SAFE_INTEGER, computeMoves: false }, 'advanced'); - - if (!diff || diff.changes.length === 0) { - // return new HunkData([], session); - return; - } - - // merge changes neighboring changes - const mergedChanges = [diff.changes[0]]; - for (let i = 1; i < diff.changes.length; i++) { - const lastChange = mergedChanges[mergedChanges.length - 1]; - const thisChange = diff.changes[i]; - if (thisChange.modified.startLineNumber - lastChange.modified.endLineNumberExclusive <= HunkData._HUNK_THRESHOLD) { - mergedChanges[mergedChanges.length - 1] = new DetailedLineRangeMapping( - lastChange.original.join(thisChange.original), - lastChange.modified.join(thisChange.modified), - (lastChange.innerChanges ?? []).concat(thisChange.innerChanges ?? []) - ); - } else { - mergedChanges.push(thisChange); - } - } - - const hunks = mergedChanges.map(change => new RawHunk(change.original, change.modified, change.innerChanges ?? [])); - - this._textModelN.changeDecorations(accessorN => { - - this._textModel0.changeDecorations(accessor0 => { - - // clean up old decorations - for (const { textModelNDecorations, textModel0Decorations } of this._data.values()) { - textModelNDecorations.forEach(accessorN.removeDecoration, accessorN); - textModel0Decorations.forEach(accessor0.removeDecoration, accessor0); - } - - this._data.clear(); - - // add new decorations - for (const hunk of hunks) { - - const textModelNDecorations: string[] = []; - const textModel0Decorations: string[] = []; - - textModelNDecorations.push(accessorN.addDecoration(lineRangeAsRange(hunk.modified, this._textModelN), HunkData._HUNK_TRACKED_RANGE)); - textModel0Decorations.push(accessor0.addDecoration(lineRangeAsRange(hunk.original, this._textModel0), HunkData._HUNK_TRACKED_RANGE)); - - for (const change of hunk.changes) { - textModelNDecorations.push(accessorN.addDecoration(change.modifiedRange, HunkData._HUNK_TRACKED_RANGE)); - textModel0Decorations.push(accessor0.addDecoration(change.originalRange, HunkData._HUNK_TRACKED_RANGE)); - } - - this._data.set(hunk, { - editState, - textModelNDecorations, - textModel0Decorations, - state: HunkState.Pending - }); - } - }); - }); - } - - get size(): number { - return this._data.size; - } - - get pending(): number { - return Iterable.reduce(this._data.values(), (r, { state }) => r + (state === HunkState.Pending ? 1 : 0), 0); - } - - private _discardEdits(item: HunkInformation): ISingleEditOperation[] { - const edits: ISingleEditOperation[] = []; - const rangesN = item.getRangesN(); - const ranges0 = item.getRanges0(); - for (let i = 1; i < rangesN.length; i++) { - const modifiedRange = rangesN[i]; - - const originalValue = this._textModel0.getValueInRange(ranges0[i]); - edits.push(EditOperation.replace(modifiedRange, originalValue)); - } - return edits; - } - - discardAll() { - const edits: ISingleEditOperation[][] = []; - for (const item of this.getInfo()) { - if (item.getState() === HunkState.Pending) { - edits.push(this._discardEdits(item)); - } - } - const undoEdits: IValidEditOperation[][] = []; - this._textModelN.pushEditOperations(null, edits.flat(), (_undoEdits) => { - undoEdits.push(_undoEdits); - return null; - }); - return undoEdits.flat(); - } - - getInfo(): HunkInformation[] { - - const result: HunkInformation[] = []; - - for (const [hunk, data] of this._data.entries()) { - const item: HunkInformation = { - getState: () => { - return data.state; - }, - isInsertion: () => { - return hunk.original.isEmpty; - }, - getRangesN: () => { - const ranges = data.textModelNDecorations.map(id => this._textModelN.getDecorationRange(id)); - coalesceInPlace(ranges); - return ranges; - }, - getRanges0: () => { - const ranges = data.textModel0Decorations.map(id => this._textModel0.getDecorationRange(id)); - coalesceInPlace(ranges); - return ranges; - }, - discardChanges: () => { - // DISCARD: replace modified range with original value. The modified range is retrieved from a decoration - // which was created above so that typing in the editor keeps discard working. - if (data.state === HunkState.Pending) { - const edits = this._discardEdits(item); - this._textModelN.pushEditOperations(null, edits, () => null); - data.state = HunkState.Rejected; - } - }, - acceptChanges: () => { - // ACCEPT: replace original range with modified value. The modified value is retrieved from the model via - // its decoration and the original range is retrieved from the hunk. - if (data.state === HunkState.Pending) { - const edits: ISingleEditOperation[] = []; - const rangesN = item.getRangesN(); - const ranges0 = item.getRanges0(); - for (let i = 1; i < ranges0.length; i++) { - const originalRange = ranges0[i]; - const modifiedValue = this._textModelN.getValueInRange(rangesN[i]); - edits.push(EditOperation.replace(originalRange, modifiedValue)); - } - this._textModel0.pushEditOperations(null, edits, () => null); - data.state = HunkState.Accepted; - data.editState.applied += 1; - } - } - }; - result.push(item); - } - - return result; - } -} - -class RawHunk { - constructor( - readonly original: LineRange, - readonly modified: LineRange, - readonly changes: RangeMapping[] - ) { } -} - -type RawHunkData = { - textModelNDecorations: string[]; - textModel0Decorations: string[]; - state: HunkState; - editState: IChatTextEditGroupState; -}; - -export const enum HunkState { - Pending = 0, - Accepted = 1, - Rejected = 2 -} - -export interface HunkInformation { - /** - * The first element [0] is the whole modified range and subsequent elements are word-level changes - */ - getRangesN(): Range[]; - - getRanges0(): Range[]; - - isInsertion(): boolean; - - discardChanges(): void; - - /** - * Accept the hunk. Applies the corresponding edits into textModel0 - */ - acceptChanges(): void; - - getState(): HunkState; -} diff --git a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatSessionService.ts b/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatSessionService.ts deleted file mode 100644 index d3be74d2296..00000000000 --- a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatSessionService.ts +++ /dev/null @@ -1,65 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import { URI } from '../../../../base/common/uri.js'; -import { Event } from '../../../../base/common/event.js'; -import { EditMode } from '../../../../workbench/contrib/inlineAideChat/common/inlineChat.js'; -import { IRange } from '../../../../editor/common/core/range.js'; -import { IActiveCodeEditor, ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -import { IDisposable } from '../../../../base/common/lifecycle.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Session, StashedSession } from './inlineChatSession.js'; -import { IValidEditOperation } from '../../../../editor/common/model.js'; -import { IChatResponseModel } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; - - -export type Recording = { - when: Date; - session: string; - exchanges: { prompt: string; res: IChatResponseModel }[]; -}; - -export interface ISessionKeyComputer { - getComparisonKey(editor: ICodeEditor, uri: URI): string; -} - -export const IInlineAideChatSessionService = createDecorator('IInlineAideChatSessionService'); - -export interface IInlineChatSessionEvent { - readonly editor: ICodeEditor; - readonly session: Session; -} - -export interface IInlineChatSessionEndEvent extends IInlineChatSessionEvent { - readonly endedByExternalCause: boolean; -} - -export interface IInlineAideChatSessionService { - _serviceBrand: undefined; - - onWillStartSession: Event; - onDidMoveSession: Event; - onDidStashSession: Event; - onDidEndSession: Event; - - createSession(editor: IActiveCodeEditor, options: { editMode: EditMode; wholeRange?: IRange }, token: CancellationToken): Promise; - - moveSession(session: Session, newEditor: ICodeEditor): void; - - getCodeEditor(session: Session): ICodeEditor; - - getSession(editor: ICodeEditor, uri: URI): Session | undefined; - - releaseSession(session: Session): void; - - stashSession(session: Session, editor: ICodeEditor, undoCancelEdits: IValidEditOperation[]): StashedSession; - - registerSessionKeyComputer(scheme: string, value: ISessionKeyComputer): IDisposable; - - // - recordings(): readonly Recording[]; - - dispose(): void; -} diff --git a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatSessionServiceImpl.ts b/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatSessionServiceImpl.ts deleted file mode 100644 index 5925c0049c2..00000000000 --- a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatSessionServiceImpl.ts +++ /dev/null @@ -1,403 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 { CancellationError } from '../../../../base/common/errors.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { URI } from '../../../../base/common/uri.js'; -import { generateUuid } from '../../../../base/common/uuid.js'; -import { IActiveCodeEditor, ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { IRange, Range } from '../../../../editor/common/core/range.js'; -import { IValidEditOperation } from '../../../../editor/common/model.js'; -import { createTextBufferFactoryFromSnapshot } from '../../../../editor/common/model/textModel.js'; -import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; -import { ITextModelService } from '../../../../editor/common/services/resolverService.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; -import { ITelemetryService } from '../../../../platform/telemetry/common/telemetry.js'; -import { DEFAULT_EDITOR_ASSOCIATION } from '../../../../workbench/common/editor.js'; -import { AideChatAgentLocation, IAideChatAgentService } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { IAideChatService } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { CTX_INLINE_CHAT_HAS_AGENT, EditMode } from '../../../../workbench/contrib/inlineAideChat/common/inlineChat.js'; -import { IEditorService } from '../../../../workbench/services/editor/common/editorService.js'; -import { UntitledTextEditorInput } from '../../../../workbench/services/untitled/common/untitledTextEditorInput.js'; -import { EmptyResponse, ErrorResponse, HunkData, ReplyResponse, Session, SessionExchange, SessionWholeRange, StashedSession, TelemetryData, TelemetryDataClassification } from './inlineChatSession.js'; -import { IInlineChatSessionEndEvent, IInlineChatSessionEvent, IInlineAideChatSessionService, ISessionKeyComputer, Recording } from './inlineChatSessionService.js'; -import { IAideChatVariablesService } from '../../../../workbench/contrib/aideChat/common/aideChatVariables.js'; -import { ISelection } from '../../../../editor/common/core/selection.js'; -import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; - - -type SessionData = { - editor: ICodeEditor; - session: Session; - store: IDisposable; -}; - -export class InlineChatError extends Error { - static readonly code = 'InlineChatError'; - constructor(message: string) { - super(message); - this.name = InlineChatError.code; - } -} - -const _inlineChatContext = '_inlineChatContext'; -const _inlineChatDocument = '_inlineChatDocument'; - -class InlineChatContext { - - static readonly variableName = '_inlineChatContext'; - - constructor( - readonly uri: URI, - readonly selection: ISelection, - readonly wholeRange: IRange, - ) { } -} - -export class InlineChatSessionServiceImpl implements IInlineAideChatSessionService { - - declare _serviceBrand: undefined; - - private readonly _store = new DisposableStore(); - - private readonly _onWillStartSession = this._store.add(new Emitter()); - readonly onWillStartSession: Event = this._onWillStartSession.event; - - private readonly _onDidMoveSession = this._store.add(new Emitter()); - readonly onDidMoveSession: Event = this._onDidMoveSession.event; - - private readonly _onDidEndSession = this._store.add(new Emitter()); - readonly onDidEndSession: Event = this._onDidEndSession.event; - - private readonly _onDidStashSession = this._store.add(new Emitter()); - readonly onDidStashSession: Event = this._onDidStashSession.event; - - private readonly _sessions = new Map(); - private readonly _keyComputers = new Map(); - private _recordings: Recording[] = []; - - - constructor( - @ITelemetryService private readonly _telemetryService: ITelemetryService, - @IModelService private readonly _modelService: IModelService, - @ITextModelService private readonly _textModelService: ITextModelService, - @IEditorWorkerService private readonly _editorWorkerService: IEditorWorkerService, - @ILogService private readonly _logService: ILogService, - @IInstantiationService private readonly _instaService: IInstantiationService, - @IEditorService private readonly _editorService: IEditorService, - @IAideChatService private readonly _chatService: IAideChatService, - @IAideChatAgentService private readonly _chatAgentService: IAideChatAgentService, - @IAideChatVariablesService chatVariableService: IAideChatVariablesService, - ) { - - - // MARK: implicit variable for editor selection and (tracked) whole range - - this._store.add(chatVariableService.registerVariable( - { id: _inlineChatContext, name: _inlineChatContext, description: '', hidden: true }, - async (_message, _arg, model) => { - for (const [, data] of this._sessions) { - if (data.session.chatModel === model) { - return JSON.stringify(new InlineChatContext(data.session.textModelN.uri, data.editor.getSelection()!, data.session.wholeRange.trackedInitialRange)); - } - } - return undefined; - } - )); - this._store.add(chatVariableService.registerVariable( - { id: _inlineChatDocument, name: _inlineChatDocument, description: '', hidden: true }, - async (_message, _arg, model) => { - for (const [, data] of this._sessions) { - if (data.session.chatModel === model) { - return data.session.textModelN.uri; - } - } - return undefined; - } - )); - - } - - dispose() { - this._store.dispose(); - this._sessions.forEach(x => x.store.dispose()); - this._sessions.clear(); - } - - async createSession(editor: IActiveCodeEditor, options: { editMode: EditMode; wholeRange?: Range }, token: CancellationToken): Promise { - - const agent = this._chatAgentService.getDefaultAgent(AideChatAgentLocation.Editor); - - if (!agent) { - this._logService.trace('[IE] NO agent found'); - return undefined; - } - - - this._onWillStartSession.fire(editor); - - const textModel = editor.getModel(); - const selection = editor.getSelection(); - - const store = new DisposableStore(); - this._logService.trace(`[IE] creating NEW session for ${editor.getId()}, ${agent.extensionId}`); - - const chatModel = this._chatService.startSession(AideChatAgentLocation.Editor, token); - if (!chatModel) { - this._logService.trace('[IE] NO chatModel found'); - return undefined; - } - - store.add(toDisposable(() => { - this._chatService.clearSession(chatModel.sessionId); - chatModel.dispose(); - })); - - const lastResponseListener = store.add(new MutableDisposable()); - store.add(chatModel.onDidChange(e => { - if (e.kind !== 'addRequest' || !e.request.response) { - return; - } - - const { response } = e.request; - - lastResponseListener.value = response.onDidChange(() => { - - if (!response.isComplete) { - return; - } - - lastResponseListener.clear(); // ONCE - - let inlineResponse: ErrorResponse | EmptyResponse | ReplyResponse; - - // make an response from the ChatResponseModel - if (response.isCanceled) { - // error: cancelled - inlineResponse = new ErrorResponse(new CancellationError()); - } else if (response.result?.errorDetails) { - // error: "real" error - inlineResponse = new ErrorResponse(new Error(response.result.errorDetails.message)); - } else if (response.response.value.length === 0) { - // epmty response - inlineResponse = new EmptyResponse(); - } else { - inlineResponse = this._instaService.createInstance( - ReplyResponse, - session.textModelN.uri, - e.request, - response - ); - } - - session.addExchange(new SessionExchange(session.lastInput!, inlineResponse)); - - if (inlineResponse instanceof ReplyResponse && inlineResponse.untitledTextModel) { - this._textModelService.createModelReference(inlineResponse.untitledTextModel.resource).then(ref => { - store.add(ref); - }); - } - }); - })); - - store.add(this._chatAgentService.onDidChangeAgents(e => { - if (e === undefined && !this._chatAgentService.getAgent(agent.id)) { - this._logService.trace(`[IE] provider GONE for ${editor.getId()}, ${agent.extensionId}`); - this._releaseSession(session, true); - } - })); - - const id = generateUuid(); - const targetUri = textModel.uri; - - // AI edits happen in the actual model, keep a reference but make no copy - store.add((await this._textModelService.createModelReference(textModel.uri))); - const textModelN = textModel; - - // create: keep a snapshot of the "actual" model - const textModel0 = store.add(this._modelService.createModel( - createTextBufferFactoryFromSnapshot(textModel.createSnapshot()), - { languageId: textModel.getLanguageId(), onDidChange: Event.None }, - targetUri.with({ scheme: Schemas.vscode, authority: 'inline-chat', path: '', query: new URLSearchParams({ id, 'textModel0': '' }).toString() }), true - )); - - // untitled documents are special and we are releasing their session when their last editor closes - if (targetUri.scheme === Schemas.untitled) { - store.add(this._editorService.onDidCloseEditor(() => { - if (!this._editorService.isOpened({ resource: targetUri, typeId: UntitledTextEditorInput.ID, editorId: DEFAULT_EDITOR_ASSOCIATION.id })) { - this._releaseSession(session, true); - } - })); - } - - let wholeRange = options.wholeRange; - if (!wholeRange) { - wholeRange = new Range(selection.selectionStartLineNumber, selection.selectionStartColumn, selection.positionLineNumber, selection.positionColumn); - } - - if (token.isCancellationRequested) { - store.dispose(); - return undefined; - } - - const session = new Session( - options.editMode, - targetUri, - textModel0, - textModelN, - agent, - store.add(new SessionWholeRange(textModelN, wholeRange)), - store.add(new HunkData(this._editorWorkerService, textModel0, textModelN)), - chatModel - ); - - // store: key -> session - const key = this._key(editor, session.targetUri); - if (this._sessions.has(key)) { - store.dispose(); - throw new Error(`Session already stored for ${key}`); - } - this._sessions.set(key, { session, editor, store }); - return session; - } - - moveSession(session: Session, target: ICodeEditor): void { - const newKey = this._key(target, session.targetUri); - const existing = this._sessions.get(newKey); - if (existing) { - if (existing.session !== session) { - throw new Error(`Cannot move session because the target editor already/still has one`); - } else { - // noop - return; - } - } - - let found = false; - for (const [oldKey, data] of this._sessions) { - if (data.session === session) { - found = true; - this._sessions.delete(oldKey); - this._sessions.set(newKey, { ...data, editor: target }); - this._logService.trace(`[IE] did MOVE session for ${data.editor.getId()} to NEW EDITOR ${target.getId()}, ${session.agent.extensionId}`); - this._onDidMoveSession.fire({ session, editor: target }); - break; - } - } - if (!found) { - throw new Error(`Cannot move session because it is not stored`); - } - } - - releaseSession(session: Session): void { - this._releaseSession(session, false); - } - - private _releaseSession(session: Session, byServer: boolean): void { - - let tuple: [string, SessionData] | undefined; - - // cleanup - for (const candidate of this._sessions) { - if (candidate[1].session === session) { - // if (value.session === session) { - tuple = candidate; - break; - } - } - - if (!tuple) { - // double remove - return; - } - - this._keepRecording(session); - this._telemetryService.publicLog2('interactiveEditor/session', session.asTelemetryData()); - - const [key, value] = tuple; - this._sessions.delete(key); - this._logService.trace(`[IE] did RELEASED session for ${value.editor.getId()}, ${session.agent.extensionId}`); - - this._onDidEndSession.fire({ editor: value.editor, session, endedByExternalCause: byServer }); - value.store.dispose(); - } - - stashSession(session: Session, editor: ICodeEditor, undoCancelEdits: IValidEditOperation[]): StashedSession { - this._keepRecording(session); - const result = this._instaService.createInstance(StashedSession, editor, session, undoCancelEdits); - this._onDidStashSession.fire({ editor, session }); - this._logService.trace(`[IE] did STASH session for ${editor.getId()}, ${session.agent.extensionId}`); - return result; - } - - getCodeEditor(session: Session): ICodeEditor { - for (const [, data] of this._sessions) { - if (data.session === session) { - return data.editor; - } - } - throw new Error('session not found'); - } - - getSession(editor: ICodeEditor, uri: URI): Session | undefined { - const key = this._key(editor, uri); - return this._sessions.get(key)?.session; - } - - private _key(editor: ICodeEditor, uri: URI): string { - const item = this._keyComputers.get(uri.scheme); - return item - ? item.getComparisonKey(editor, uri) - : `${editor.getId()}@${uri.toString()}`; - - } - - registerSessionKeyComputer(scheme: string, value: ISessionKeyComputer): IDisposable { - this._keyComputers.set(scheme, value); - return toDisposable(() => this._keyComputers.delete(scheme)); - } - - // --- debug - - private _keepRecording(session: Session) { - const newLen = this._recordings.unshift(session.asRecording()); - if (newLen > 5) { - this._recordings.pop(); - } - } - - recordings(): readonly Recording[] { - return this._recordings; - } -} - -export class InlineChatEnabler { - - static Id = 'inlineAideChat.enabler'; - - private readonly _ctxHasProvider: IContextKey; - - private readonly _store = new DisposableStore(); - - constructor( - @IContextKeyService contextKeyService: IContextKeyService, - @IAideChatAgentService chatAgentService: IAideChatAgentService - ) { - this._ctxHasProvider = CTX_INLINE_CHAT_HAS_AGENT.bindTo(contextKeyService); - this._store.add(chatAgentService.onDidChangeAgents(() => { - const hasEditorAgent = Boolean(chatAgentService.getDefaultAgent(AideChatAgentLocation.Editor)); - this._ctxHasProvider.set(hasEditorAgent); - })); - } - - dispose() { - this._ctxHasProvider.reset(); - this._store.dispose(); - } -} diff --git a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatStrategies.ts b/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatStrategies.ts deleted file mode 100644 index 756a86702c0..00000000000 --- a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatStrategies.ts +++ /dev/null @@ -1,612 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { WindowIntervalTimer } from '../../../../base/browser/dom.js'; -import { coalesceInPlace } from '../../../../base/common/arrays.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { DisposableStore } from '../../../../base/common/lifecycle.js'; -import { themeColorFromId } from '../../../../base/common/themables.js'; -import { ICodeEditor, IViewZone, IViewZoneChangeAccessor } from '../../../../editor/browser/editorBrowser.js'; -import { StableEditorScrollState } from '../../../../editor/browser/stableEditorScroll.js'; -import { LineSource, RenderOptions, renderLines } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; -import { ISingleEditOperation } from '../../../../editor/common/core/editOperation.js'; -import { LineRange } from '../../../../editor/common/core/lineRange.js'; -import { Position } from '../../../../editor/common/core/position.js'; -import { Range } from '../../../../editor/common/core/range.js'; -import { IEditorDecorationsCollection } from '../../../../editor/common/editorCommon.js'; -import { IModelDecorationsChangeAccessor, IModelDeltaDecoration, IValidEditOperation, MinimapPosition, OverviewRulerLane, TrackedRangeStickiness } from '../../../../editor/common/model.js'; -import { ModelDecorationOptions } from '../../../../editor/common/model/textModel.js'; -import { IEditorWorkerService } from '../../../../editor/common/services/editorWorker.js'; -import { InlineDecoration, InlineDecorationType } from '../../../../editor/common/viewModel.js'; -import { localize } from '../../../../nls.js'; -import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { Progress } from '../../../../platform/progress/common/progress.js'; -import { SaveReason } from '../../../../workbench/common/editor.js'; -import { countWords } from '../../../../workbench/contrib/aideChat/common/aideChatWordCounter.js'; -import { HunkInformation, ReplyResponse, Session } from '../../../../workbench/contrib/inlineAideChat/browser/inlineChatSession.js'; -import { InlineChatZoneWidget } from './inlineChatZoneWidget.js'; -import { CTX_INLINE_CHAT_CHANGE_HAS_DIFF, CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF, CTX_INLINE_CHAT_DOCUMENT_CHANGED, InlineChatConfigKeys, minimapInlineChatDiffInserted, overviewRulerInlineChatDiffInserted } from '../../../../workbench/contrib/inlineAideChat/common/inlineChat.js'; -import { HunkState } from './inlineChatSession.js'; -import { assertType } from '../../../../base/common/types.js'; -import { IModelService } from '../../../../editor/common/services/model.js'; -import { performAsyncTextEdit, asProgressiveEdit } from './utils.js'; -import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { ITextFileService } from '../../../../workbench/services/textfile/common/textfiles.js'; -import { IUntitledTextEditorModel } from '../../../../workbench/services/untitled/common/untitledTextEditorModel.js'; -import { Schemas } from '../../../../base/common/network.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { DefaultChatTextEditor } from '../../../../workbench/contrib/aideChat/browser/codeBlockPart.js'; -import { isEqual } from '../../../../base/common/resources.js'; - -export interface IEditObserver { - start(): void; - stop(): void; -} - -export abstract class EditModeStrategy { - - protected static _decoBlock = ModelDecorationOptions.register({ - description: 'inline-chat', - showIfCollapsed: false, - isWholeLine: true, - className: 'inline-chat-block-selection', - }); - - protected readonly _store = new DisposableStore(); - protected readonly _onDidAccept = this._store.add(new Emitter()); - protected readonly _onDidDiscard = this._store.add(new Emitter()); - - - readonly onDidAccept: Event = this._onDidAccept.event; - readonly onDidDiscard: Event = this._onDidDiscard.event; - - toggleDiff?: () => any; - - constructor( - protected readonly _session: Session, - protected readonly _editor: ICodeEditor, - protected readonly _zone: InlineChatZoneWidget, - @ITextFileService private readonly _textFileService: ITextFileService, - @IInstantiationService private readonly _instaService: IInstantiationService, - ) { } - - dispose(): void { - this._store.dispose(); - } - - protected async _doApplyChanges(ignoreLocal: boolean): Promise { - - const untitledModels: IUntitledTextEditorModel[] = []; - - const editor = this._instaService.createInstance(DefaultChatTextEditor); - - - for (const request of this._session.chatModel.getRequests()) { - - if (!request.response?.response) { - continue; - } - - for (const item of request.response.response.value) { - if (item.kind !== 'textEditGroup') { - continue; - } - if (ignoreLocal && isEqual(item.uri, this._session.textModelN.uri)) { - continue; - } - - await editor.apply(request.response, item, undefined); - - if (item.uri.scheme === Schemas.untitled) { - const untitled = this._textFileService.untitled.get(item.uri); - if (untitled) { - untitledModels.push(untitled); - } - } - } - } - - for (const untitledModel of untitledModels) { - if (!untitledModel.isDisposed()) { - await untitledModel.resolve(); - await untitledModel.save({ reason: SaveReason.EXPLICIT }); - } - } - } - - abstract apply(): Promise; - - cancel() { - return this._session.hunkData.discardAll(); - } - - async acceptHunk(): Promise { - this._onDidAccept.fire(); - } - - async discardHunk(): Promise { - this._onDidDiscard.fire(); - } - - abstract makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, timings: ProgressingEditsOptions, undoStopBefore: boolean): Promise; - - abstract makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, undoStopBefore: boolean): Promise; - - abstract renderChanges(response: ReplyResponse): Promise; - - move?(next: boolean): void; - - abstract hasFocus(): boolean; - - getWholeRangeDecoration(): IModelDeltaDecoration[] { - const ranges = [this._session.wholeRange.value]; - const newDecorations = ranges.map(range => range.isEmpty() ? undefined : ({ range, options: EditModeStrategy._decoBlock })); - coalesceInPlace(newDecorations); - return newDecorations; - } -} - -export class PreviewStrategy extends EditModeStrategy { - - private readonly _ctxDocumentChanged: IContextKey; - - constructor( - session: Session, - editor: ICodeEditor, - zone: InlineChatZoneWidget, - @IModelService modelService: IModelService, - @IContextKeyService contextKeyService: IContextKeyService, - @ITextFileService textFileService: ITextFileService, - @IInstantiationService instaService: IInstantiationService - ) { - super(session, editor, zone, textFileService, instaService); - - this._ctxDocumentChanged = CTX_INLINE_CHAT_DOCUMENT_CHANGED.bindTo(contextKeyService); - - const baseModel = modelService.getModel(session.targetUri)!; - Event.debounce(baseModel.onDidChangeContent.bind(baseModel), () => { }, 350)(_ => { - if (!baseModel.isDisposed() && !session.textModel0.isDisposed()) { - this._ctxDocumentChanged.set(session.hasChangedText); - } - }, undefined, this._store); - } - - override dispose(): void { - this._ctxDocumentChanged.reset(); - super.dispose(); - } - - override async apply() { - await super._doApplyChanges(false); - } - - override async makeChanges(): Promise { - } - - override async makeProgressiveChanges(): Promise { - } - - override async renderChanges(response: ReplyResponse): Promise { } - - hasFocus(): boolean { - return this._zone.widget.hasFocus(); - } -} - - -export interface ProgressingEditsOptions { - duration: number; - token: CancellationToken; -} - - - -type HunkDisplayData = { - - decorationIds: string[]; - - viewZoneId: string | undefined; - viewZone: IViewZone; - - distance: number; - position: Position; - acceptHunk: () => void; - discardHunk: () => void; - toggleDiff?: () => any; - remove(): void; - move: (next: boolean) => void; - - hunk: HunkInformation; -}; - - -export class LiveStrategy extends EditModeStrategy { - - private readonly _decoInsertedText = ModelDecorationOptions.register({ - description: 'inline-modified-line', - className: 'inline-chat-inserted-range-linehighlight', - isWholeLine: true, - overviewRuler: { - position: OverviewRulerLane.Full, - color: themeColorFromId(overviewRulerInlineChatDiffInserted), - }, - minimap: { - position: MinimapPosition.Inline, - color: themeColorFromId(minimapInlineChatDiffInserted), - } - }); - - private readonly _decoInsertedTextRange = ModelDecorationOptions.register({ - description: 'inline-chat-inserted-range-linehighlight', - className: 'inline-chat-inserted-range', - stickiness: TrackedRangeStickiness.NeverGrowsWhenTypingAtEdges, - }); - - private readonly _ctxCurrentChangeHasDiff: IContextKey; - private readonly _ctxCurrentChangeShowsDiff: IContextKey; - - private readonly _progressiveEditingDecorations: IEditorDecorationsCollection; - private _editCount: number = 0; - - override acceptHunk: () => Promise = () => super.acceptHunk(); - override discardHunk: () => Promise = () => super.discardHunk(); - - constructor( - session: Session, - editor: ICodeEditor, - zone: InlineChatZoneWidget, - @IContextKeyService contextKeyService: IContextKeyService, - @IEditorWorkerService protected readonly _editorWorkerService: IEditorWorkerService, - @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, - @IConfigurationService private readonly _configService: IConfigurationService, - @ITextFileService textFileService: ITextFileService, - @IInstantiationService instaService: IInstantiationService - ) { - super(session, editor, zone, textFileService, instaService); - this._ctxCurrentChangeHasDiff = CTX_INLINE_CHAT_CHANGE_HAS_DIFF.bindTo(contextKeyService); - this._ctxCurrentChangeShowsDiff = CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF.bindTo(contextKeyService); - - this._progressiveEditingDecorations = this._editor.createDecorationsCollection(); - - } - - override dispose(): void { - this._resetDiff(); - super.dispose(); - } - - private _resetDiff(): void { - this._ctxCurrentChangeHasDiff.reset(); - this._ctxCurrentChangeShowsDiff.reset(); - this._zone.widget.updateStatus(''); - this._progressiveEditingDecorations.clear(); - - - for (const data of this._hunkDisplayData.values()) { - data.remove(); - } - } - - override async apply() { - this._resetDiff(); - if (this._editCount > 0) { - this._editor.pushUndoStop(); - } - await super._doApplyChanges(true); - } - - override cancel() { - this._resetDiff(); - return super.cancel(); - } - - override async makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, undoStopBefore: boolean): Promise { - return this._makeChanges(edits, obs, undefined, undefined, undoStopBefore); - } - - override async makeProgressiveChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions, undoStopBefore: boolean): Promise { - - // add decorations once per line that got edited - const progress = new Progress(edits => { - - const newLines = new Set(); - for (const edit of edits) { - LineRange.fromRange(edit.range).forEach(line => newLines.add(line)); - } - const existingRanges = this._progressiveEditingDecorations.getRanges().map(LineRange.fromRange); - for (const existingRange of existingRanges) { - existingRange.forEach(line => newLines.delete(line)); - } - const newDecorations: IModelDeltaDecoration[] = []; - for (const line of newLines) { - newDecorations.push({ range: new Range(line, 1, line, Number.MAX_VALUE), options: this._decoInsertedText }); - } - - this._progressiveEditingDecorations.append(newDecorations); - }); - return this._makeChanges(edits, obs, opts, progress, undoStopBefore); - } - - private async _makeChanges(edits: ISingleEditOperation[], obs: IEditObserver, opts: ProgressingEditsOptions | undefined, progress: Progress | undefined, undoStopBefore: boolean): Promise { - - // push undo stop before first edit - if (undoStopBefore) { - this._editor.pushUndoStop(); - } - - this._editCount++; - - if (opts) { - // ASYNC - const durationInSec = opts.duration / 1000; - for (const edit of edits) { - const wordCount = countWords(edit.text ?? ''); - const speed = wordCount / durationInSec; - // console.log({ durationInSec, wordCount, speed: wordCount / durationInSec }); - const asyncEdit = asProgressiveEdit(new WindowIntervalTimer(this._zone.domNode), edit, speed, opts.token); - await performAsyncTextEdit(this._session.textModelN, asyncEdit, progress, obs); - } - - } else { - // SYNC - obs.start(); - this._session.textModelN.pushEditOperations(null, edits, (undoEdits) => { - progress?.report(undoEdits); - return null; - }); - obs.stop(); - } - } - - private readonly _hunkDisplayData = new Map(); - - override async renderChanges(response: ReplyResponse) { - - this._progressiveEditingDecorations.clear(); - - const renderHunks = () => { - - let widgetData: HunkDisplayData | undefined; - - changeDecorationsAndViewZones(this._editor, (decorationsAccessor, viewZoneAccessor) => { - - const keysNow = new Set(this._hunkDisplayData.keys()); - widgetData = undefined; - - for (const hunkData of this._session.hunkData.getInfo()) { - - keysNow.delete(hunkData); - - const hunkRanges = hunkData.getRangesN(); - let data = this._hunkDisplayData.get(hunkData); - if (!data) { - // first time -> create decoration - const decorationIds: string[] = []; - for (let i = 0; i < hunkRanges.length; i++) { - decorationIds.push(decorationsAccessor.addDecoration(hunkRanges[i], i === 0 - ? this._decoInsertedText - : this._decoInsertedTextRange) - ); - } - - const acceptHunk = () => { - hunkData.acceptChanges(); - renderHunks(); - }; - - const discardHunk = () => { - hunkData.discardChanges(); - renderHunks(); - }; - - // original view zone - const mightContainNonBasicASCII = this._session.textModel0.mightContainNonBasicASCII(); - const mightContainRTL = this._session.textModel0.mightContainRTL(); - const renderOptions = RenderOptions.fromEditor(this._editor); - const originalRange = hunkData.getRanges0()[0]; - const source = new LineSource( - LineRange.fromRangeInclusive(originalRange).mapToLineArray(l => this._session.textModel0.tokenization.getLineTokens(l)), - [], - mightContainNonBasicASCII, - mightContainRTL, - ); - const domNode = document.createElement('div'); - domNode.className = 'inline-chat-original-zone2'; - const result = renderLines(source, renderOptions, [new InlineDecoration(new Range(originalRange.startLineNumber, 1, originalRange.startLineNumber, 1), '', InlineDecorationType.Regular)], domNode); - const viewZoneData: IViewZone = { - afterLineNumber: -1, - heightInLines: result.heightInLines, - domNode, - }; - - const toggleDiff = () => { - const scrollState = StableEditorScrollState.capture(this._editor); - changeDecorationsAndViewZones(this._editor, (_decorationsAccessor, viewZoneAccessor) => { - assertType(data); - if (!data.viewZoneId) { - const [hunkRange] = hunkData.getRangesN(); - viewZoneData.afterLineNumber = hunkRange.startLineNumber - 1; - data.viewZoneId = viewZoneAccessor.addZone(viewZoneData); - } else { - viewZoneAccessor.removeZone(data.viewZoneId!); - data.viewZoneId = undefined; - } - }); - this._ctxCurrentChangeShowsDiff.set(typeof data?.viewZoneId === 'string'); - scrollState.restore(this._editor); - }; - - const remove = () => { - changeDecorationsAndViewZones(this._editor, (decorationsAccessor, viewZoneAccessor) => { - assertType(data); - for (const decorationId of data.decorationIds) { - decorationsAccessor.removeDecoration(decorationId); - } - if (data.viewZoneId) { - viewZoneAccessor.removeZone(data.viewZoneId); - } - data.decorationIds = []; - data.viewZoneId = undefined; - }); - }; - - const move = (next: boolean) => { - assertType(widgetData); - - const candidates: Position[] = []; - for (const item of this._session.hunkData.getInfo()) { - if (item.getState() === HunkState.Pending) { - candidates.push(item.getRangesN()[0].getStartPosition().delta(-1)); - } - } - if (candidates.length < 2) { - return; - } - for (let i = 0; i < candidates.length; i++) { - if (candidates[i].equals(widgetData.position)) { - let newPos: Position; - if (next) { - newPos = candidates[(i + 1) % candidates.length]; - } else { - newPos = candidates[(i + candidates.length - 1) % candidates.length]; - } - this._zone.updatePositionAndHeight(newPos); - renderHunks(); - break; - } - } - }; - - const zoneLineNumber = this._zone.position!.lineNumber; - const myDistance = zoneLineNumber <= hunkRanges[0].startLineNumber - ? hunkRanges[0].startLineNumber - zoneLineNumber - : zoneLineNumber - hunkRanges[0].endLineNumber; - - data = { - hunk: hunkData, - decorationIds, - viewZoneId: '', - viewZone: viewZoneData, - distance: myDistance, - position: hunkRanges[0].getStartPosition().delta(-1), - acceptHunk, - discardHunk, - toggleDiff: !hunkData.isInsertion() ? toggleDiff : undefined, - remove, - move, - }; - - this._hunkDisplayData.set(hunkData, data); - - } else if (hunkData.getState() !== HunkState.Pending) { - data.remove(); - - } else { - // update distance and position based on modifiedRange-decoration - const zoneLineNumber = this._zone.position!.lineNumber; - const modifiedRangeNow = hunkRanges[0]; - data.position = modifiedRangeNow.getStartPosition().delta(-1); - data.distance = zoneLineNumber <= modifiedRangeNow.startLineNumber - ? modifiedRangeNow.startLineNumber - zoneLineNumber - : zoneLineNumber - modifiedRangeNow.endLineNumber; - } - - if (hunkData.getState() === HunkState.Pending && (!widgetData || data.distance < widgetData.distance)) { - widgetData = data; - } - } - - for (const key of keysNow) { - const data = this._hunkDisplayData.get(key); - if (data) { - this._hunkDisplayData.delete(key); - data.remove(); - } - } - }); - - if (widgetData) { - this._zone.updatePositionAndHeight(widgetData.position); - - const remainingHunks = this._session.hunkData.pending; - this._updateSummaryMessage(remainingHunks, this._session.hunkData.size); - - - const mode = this._configService.getValue<'on' | 'off' | 'auto'>(InlineChatConfigKeys.AccessibleDiffView); - if (mode === 'on' || mode === 'auto' && this._accessibilityService.isScreenReaderOptimized()) { - this._zone.widget.showAccessibleHunk(this._session, widgetData.hunk); - } - - this._ctxCurrentChangeHasDiff.set(Boolean(widgetData.toggleDiff)); - this.toggleDiff = widgetData.toggleDiff; - this.acceptHunk = async () => widgetData!.acceptHunk(); - this.discardHunk = async () => widgetData!.discardHunk(); - this.move = next => widgetData!.move(next); - - } else if (this._hunkDisplayData.size > 0) { - // everything accepted or rejected - let oneAccepted = false; - for (const hunkData of this._session.hunkData.getInfo()) { - if (hunkData.getState() === HunkState.Accepted) { - oneAccepted = true; - break; - } - } - if (oneAccepted) { - this._onDidAccept.fire(); - } else { - this._onDidDiscard.fire(); - } - } - - return widgetData; - }; - - return renderHunks()?.position; - } - - private _updateSummaryMessage(remaining: number, total: number) { - - const needsReview = this._configService.getValue(InlineChatConfigKeys.AcceptedOrDiscardBeforeSave); - let message: string; - if (total === 0) { - message = localize('change.0', "Nothing changed."); - } else if (remaining === 1) { - message = needsReview - ? localize('review.1', "$(info) Accept or Discard change") - : localize('change.1', "1 change"); - } else { - message = needsReview - ? localize('review.N', "$(info) Accept or Discard {0} changes", remaining) - : localize('change.N', "{0} changes", total); - } - - let title: string | undefined; - if (needsReview) { - title = localize('review', "Review (accept or discard) all changes before continuing"); - } - - this._zone.widget.updateStatus(message, { title }); - } - - hasFocus(): boolean { - return this._zone.widget.hasFocus(); - } - - override getWholeRangeDecoration(): IModelDeltaDecoration[] { - // don't render the blue in live mode - return []; - } -} - -function changeDecorationsAndViewZones(editor: ICodeEditor, callback: (accessor: IModelDecorationsChangeAccessor, viewZoneAccessor: IViewZoneChangeAccessor) => void): void { - editor.changeDecorations(decorationsAccessor => { - editor.changeViewZones(viewZoneAccessor => { - callback(decorationsAccessor, viewZoneAccessor); - }); - }); -} diff --git a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatWidget.ts b/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatWidget.ts deleted file mode 100644 index 45dd3ef1e2a..00000000000 --- a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatWidget.ts +++ /dev/null @@ -1,712 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { Dimension, getActiveElement, getTotalHeight, h, reset, trackFocus } from '../../../../base/browser/dom.js'; -import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { ProgressBar } from '../../../../base/browser/ui/progressbar/progressbar.js'; -import { Emitter, Event } from '../../../../base/common/event.js'; -import { IMarkdownString, MarkdownString } from '../../../../base/common/htmlContent.js'; -import { DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { ISettableObservable, constObservable, derived, observableValue } from '../../../../base/common/observable.js'; -import './media/inlineChat.css'; -import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { AccessibleDiffViewer, IAccessibleDiffViewerModel } from '../../../../editor/browser/widget/diffEditor/components/accessibleDiffViewer.js'; -import { EditorOption, IComputedEditorOptions } from '../../../../editor/common/config/editorOptions.js'; -import { LineRange } from '../../../../editor/common/core/lineRange.js'; -import { Position } from '../../../../editor/common/core/position.js'; -import { Range } from '../../../../editor/common/core/range.js'; -import { DetailedLineRangeMapping, RangeMapping } from '../../../../editor/common/diff/rangeMapping.js'; -import { ICodeEditorViewState, ScrollType } from '../../../../editor/common/editorCommon.js'; -import { ITextModel } from '../../../../editor/common/model.js'; -import { IResolvedTextEditorModel, ITextModelService } from '../../../../editor/common/services/resolverService.js'; -import { localize } from '../../../../nls.js'; -import { IAccessibilityService } from '../../../../platform/accessibility/common/accessibility.js'; -import { IWorkbenchButtonBarOptions, MenuWorkbenchButtonBar } from '../../../../platform/actions/browser/buttonbar.js'; -import { HiddenItemStrategy, MenuWorkbenchToolBar } from '../../../../platform/actions/browser/toolbar.js'; -import { MenuId, MenuItemAction } from '../../../../platform/actions/common/actions.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js'; -import { asCssVariable, asCssVariableName, editorBackground, inputBackground } from '../../../../platform/theme/common/colorRegistry.js'; -import { AccessibilityVerbositySettingId } from '../../../../workbench/contrib/accessibility/browser/accessibilityConfiguration.js'; -import { IAccessibleViewService } from '../../../../platform/accessibility/browser/accessibleView.js'; -import { AccessibilityCommandId } from '../../../../workbench/contrib/accessibility/common/accessibilityCommands.js'; -import { ChatModel, IChatModel } from '../../../../workbench/contrib/aideChat/common/aideChatModel.js'; -import { isResponseVM, isWelcomeVM } from '../../../../workbench/contrib/aideChat/common/aideChatViewModel.js'; -import { HunkInformation, Session } from '../../../../workbench/contrib/inlineAideChat/browser/inlineChatSession.js'; -import { CTX_INLINE_CHAT_FOCUSED, CTX_INLINE_CHAT_RESPONSE_FOCUSED, inlineChatBackground, InlineChatConfigKeys, inlineChatForeground } from '../../../../workbench/contrib/inlineAideChat/common/inlineChat.js'; -import { ChatWidget } from '../../../../workbench/contrib/aideChat/browser/aideChatWidget.js'; -import { chatRequestBackground } from '../../../../workbench/contrib/aideChat/common/aideChatColors.js'; -import { Selection } from '../../../../editor/common/core/selection.js'; -import { AideChatAgentLocation } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { isNonEmptyArray, tail } from '../../../../base/common/arrays.js'; -import { IAideChatService } from '../../../../workbench/contrib/aideChat/common/aideChatService.js'; -import { getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; -import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js'; -import { IHoverService } from '../../../../platform/hover/browser/hover.js'; -import { IChatWidgetViewOptions } from '../../../../workbench/contrib/aideChat/browser/aideChat.js'; -import { TextOnlyMenuEntryActionViewItem } from '../../../../platform/actions/browser/menuEntryActionViewItem.js'; - - -export interface InlineChatWidgetViewState { - editorViewState: ICodeEditorViewState; - input: string; - placeholder: string; -} - -export interface IInlineChatWidgetConstructionOptions { - - /** - * The menu that rendered as button bar, use for accept, discard etc - */ - statusMenuId: MenuId | { menu: MenuId; options: IWorkbenchButtonBarOptions }; - - /** - * The men that rendered in the lower right corner, use for feedback - */ - feedbackMenuId?: MenuId; - - /** - * The options for the chat widget - */ - chatWidgetViewOptions?: IChatWidgetViewOptions; -} - -export interface IInlineChatMessage { - message: IMarkdownString; - requestId: string; -} - -export interface IInlineChatMessageAppender { - appendContent(fragment: string): void; - cancel(): void; - complete(): void; -} - -export class InlineChatWidget { - - protected readonly _elements = h( - 'div.inline-chat@root', - [ - h('div.chat-widget@chatWidget'), - h('div.progress@progress'), - h('div.previewDiff.hidden@previewDiff'), - h('div.accessibleViewer@accessibleViewer'), - h('div.status@status', [ - h('div.label.info.hidden@infoLabel'), - h('div.actions.hidden@statusToolbar'), - h('div.label.status.hidden@statusLabel'), - h('div.actions.hidden@feedbackToolbar'), - ]), - ] - ); - - protected readonly _store = new DisposableStore(); - - private readonly _defaultChatModel: ChatModel; - private readonly _ctxInputEditorFocused: IContextKey; - private readonly _ctxResponseFocused: IContextKey; - - private readonly _progressBar: ProgressBar; - private readonly _chatWidget: ChatWidget; - - protected readonly _onDidChangeHeight = this._store.add(new Emitter()); - readonly onDidChangeHeight: Event = Event.filter(this._onDidChangeHeight.event, _ => !this._isLayouting); - - private readonly _onDidChangeInput = this._store.add(new Emitter()); - readonly onDidChangeInput: Event = this._onDidChangeInput.event; - - private _isLayouting: boolean = false; - - readonly scopedContextKeyService: IContextKeyService; - - constructor( - location: AideChatAgentLocation, - options: IInlineChatWidgetConstructionOptions, - @IInstantiationService protected readonly _instantiationService: IInstantiationService, - @IContextKeyService private readonly _contextKeyService: IContextKeyService, - @IKeybindingService private readonly _keybindingService: IKeybindingService, - @IAccessibilityService private readonly _accessibilityService: IAccessibilityService, - @IConfigurationService private readonly _configurationService: IConfigurationService, - @IAccessibleViewService private readonly _accessibleViewService: IAccessibleViewService, - @ITextModelService protected readonly _textModelResolverService: ITextModelService, - @IAideChatService private readonly _chatService: IAideChatService, - @IHoverService private readonly _hoverService: IHoverService, - ) { - // toolbars - this._progressBar = new ProgressBar(this._elements.progress); - this._store.add(this._progressBar); - - this.scopedContextKeyService = this._store.add(_contextKeyService.createScoped(this._elements.chatWidget)); - const scopedInstaService = _instantiationService.createChild( - new ServiceCollection([ - IContextKeyService, - this.scopedContextKeyService - ]), - this._store - ); - - this._chatWidget = scopedInstaService.createInstance( - ChatWidget, - location, - { resource: true }, - { - defaultElementHeight: 32, - renderStyle: 'minimal', - renderInputOnTop: false, - renderFollowups: true, - supportsFileReferences: true, - filter: item => !isWelcomeVM(item), - ...options.chatWidgetViewOptions - }, - { - listForeground: inlineChatForeground, - listBackground: inlineChatBackground, - inputEditorBackground: inputBackground, - resultEditorBackground: editorBackground - } - ); - this._chatWidget.render(this._elements.chatWidget); - this._elements.chatWidget.style.setProperty(asCssVariableName(chatRequestBackground), asCssVariable(inlineChatBackground)); - this._chatWidget.setVisible(true); - this._store.add(this._chatWidget); - - const viewModelStore = this._store.add(new DisposableStore()); - this._store.add(this._chatWidget.onDidChangeViewModel(() => { - viewModelStore.clear(); - const viewModel = this._chatWidget.viewModel; - if (viewModel) { - viewModelStore.add(viewModel.onDidChange(() => this._onDidChangeHeight.fire())); - } - this._onDidChangeHeight.fire(); - })); - - this._store.add(this.chatWidget.onDidChangeContentHeight(() => { - this._onDidChangeHeight.fire(); - })); - - // context keys - this._ctxResponseFocused = CTX_INLINE_CHAT_RESPONSE_FOCUSED.bindTo(this._contextKeyService); - const tracker = this._store.add(trackFocus(this.domNode)); - this._store.add(tracker.onDidBlur(() => this._ctxResponseFocused.set(false))); - this._store.add(tracker.onDidFocus(() => this._ctxResponseFocused.set(true))); - - this._ctxInputEditorFocused = CTX_INLINE_CHAT_FOCUSED.bindTo(_contextKeyService); - this._store.add(this._chatWidget.inputEditor.onDidFocusEditorWidget(() => this._ctxInputEditorFocused.set(true))); - this._store.add(this._chatWidget.inputEditor.onDidBlurEditorWidget(() => this._ctxInputEditorFocused.set(false))); - - const statusMenuId = options.statusMenuId instanceof MenuId ? options.statusMenuId : options.statusMenuId.menu; - - if (this._configurationService.getValue(InlineChatConfigKeys.ExpTextButtons)) { - // TEXT-ONLY bar - const statusToolbarMenu = scopedInstaService.createInstance(MenuWorkbenchToolBar, this._elements.statusToolbar, statusMenuId, { - hiddenItemStrategy: HiddenItemStrategy.NoHide, - telemetrySource: options.chatWidgetViewOptions?.menus?.telemetrySource, - actionViewItemProvider: action => action instanceof MenuItemAction ? this._instantiationService.createInstance(TextOnlyMenuEntryActionViewItem, action, { conversational: true }) : undefined, - toolbarOptions: { primaryGroup: '0_main' }, - menuOptions: { renderShortTitle: true }, - label: true, - icon: false - }); - this._store.add(statusToolbarMenu.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); - this._store.add(statusToolbarMenu); - - } else { - // BUTTON bar - const statusMenuOptions = options.statusMenuId instanceof MenuId ? undefined : options.statusMenuId.options; - const statusButtonBar = scopedInstaService.createInstance(MenuWorkbenchButtonBar, this._elements.statusToolbar, statusMenuId, { - toolbarOptions: { primaryGroup: '0_main' }, - telemetrySource: options.chatWidgetViewOptions?.menus?.telemetrySource, - menuOptions: { renderShortTitle: true }, - ...statusMenuOptions, - }); - this._store.add(statusButtonBar.onDidChange(() => this._onDidChangeHeight.fire())); - this._store.add(statusButtonBar); - } - - const workbenchToolbarOptions = { - hiddenItemStrategy: HiddenItemStrategy.NoHide, - toolbarOptions: { - primaryGroup: () => true, - useSeparatorsInPrimaryActions: true - } - }; - - if (options.feedbackMenuId) { - const feedbackToolbar = this._instantiationService.createInstance(MenuWorkbenchToolBar, this._elements.feedbackToolbar, options.feedbackMenuId, { ...workbenchToolbarOptions, hiddenItemStrategy: HiddenItemStrategy.Ignore }); - this._store.add(feedbackToolbar.onDidChangeMenuItems(() => this._onDidChangeHeight.fire())); - this._store.add(feedbackToolbar); - } - - this._store.add(this._configurationService.onDidChangeConfiguration(e => { - if (e.affectsConfiguration(AccessibilityVerbositySettingId.InlineChat)) { - this._updateAriaLabel(); - } - })); - - this._elements.root.tabIndex = 0; - this._elements.statusLabel.tabIndex = 0; - this._updateAriaLabel(); - - // this._elements.status - this._store.add(this._hoverService.setupManagedHover(getDefaultHoverDelegate('element'), this._elements.statusLabel, () => { - return this._elements.statusLabel.dataset['title']; - })); - - this._store.add(this._chatService.onDidPerformUserAction(e => { - if (e.sessionId === this._chatWidget.viewModel?.model.sessionId && e.action.kind === 'vote') { - this.updateStatus('Thank you for your feedback!', { resetAfter: 1250 }); - } - })); - - // LEGACY - default chat model - // this is only here for as long as we offer updateChatMessage - this._defaultChatModel = this._store.add(this._instantiationService.createInstance(ChatModel, undefined, AideChatAgentLocation.Editor)); - this._defaultChatModel.startInitialize(); - this._defaultChatModel.initialize(undefined); - this.setChatModel(this._defaultChatModel); - } - - private _updateAriaLabel(): void { - - this._elements.root.ariaLabel = this._accessibleViewService.getOpenAriaHint(AccessibilityVerbositySettingId.InlineChat); - - if (this._accessibilityService.isScreenReaderOptimized()) { - let label = defaultAriaLabel; - if (this._configurationService.getValue(AccessibilityVerbositySettingId.InlineChat)) { - const kbLabel = this._keybindingService.lookupKeybinding(AccessibilityCommandId.OpenAccessibilityHelp)?.getLabel(); - label = kbLabel - ? localize('inlineChat.accessibilityHelp', "Inline Chat Input, Use {0} for Inline Chat Accessibility Help.", kbLabel) - : localize('inlineChat.accessibilityHelpNoKb', "Inline Chat Input, Run the Inline Chat Accessibility Help command for more information."); - } - this._chatWidget.inputEditor.updateOptions({ ariaLabel: label }); - } - } - - dispose(): void { - this._store.dispose(); - } - - get domNode(): HTMLElement { - return this._elements.root; - } - - get chatWidget(): ChatWidget { - return this._chatWidget; - } - - saveState() { - this._chatWidget.saveState(); - } - - layout(widgetDim: Dimension) { - this._isLayouting = true; - try { - this._doLayout(widgetDim); - } finally { - this._isLayouting = false; - } - } - - protected _doLayout(dimension: Dimension): void { - const extraHeight = this._getExtraHeight(); - const progressHeight = getTotalHeight(this._elements.progress); - const statusHeight = getTotalHeight(this._elements.status); - - // console.log('ZONE#Widget#layout', { height: dimension.height, extraHeight, progressHeight, followUpsHeight, statusHeight, LIST: dimension.height - progressHeight - followUpsHeight - statusHeight - extraHeight }); - - this._elements.root.style.height = `${dimension.height - extraHeight}px`; - this._elements.root.style.width = `${dimension.width}px`; - this._elements.progress.style.width = `${dimension.width}px`; - - this._chatWidget.layout( - dimension.height - progressHeight - statusHeight - extraHeight, - dimension.width - ); - } - - /** - * The content height of this widget is the size that would require no scrolling - */ - get contentHeight(): number { - const data = { - chatWidgetContentHeight: this._chatWidget.contentHeight, - progressHeight: getTotalHeight(this._elements.progress), - statusHeight: getTotalHeight(this._elements.status), - extraHeight: this._getExtraHeight() - }; - const result = data.progressHeight + data.chatWidgetContentHeight + data.statusHeight + data.extraHeight; - return result; - } - - get minHeight(): number { - // The chat widget is variable height and supports scrolling. It should be - // at least "maxWidgetHeight" high and at most the content height. - - let maxWidgetOutputHeight = 100; - for (const item of this._chatWidget.viewModel?.getItems() ?? []) { - if (isResponseVM(item) && item.response.value.some(r => r.kind === 'textEditGroup' && !r.state?.applied)) { - maxWidgetOutputHeight = 270; - break; - } - } - - let value = this.contentHeight; - value -= this._chatWidget.contentHeight; - value += Math.min(this._chatWidget.input.contentHeight + maxWidgetOutputHeight, this._chatWidget.contentHeight); - return value; - } - - protected _getExtraHeight(): number { - return 4 /* padding */ + 2 /*border*/ + 12 /*shadow*/; - } - - updateProgress(show: boolean) { - if (show) { - this._progressBar.show(); - this._progressBar.infinite(); - } else { - this._progressBar.stop(); - this._progressBar.hide(); - } - } - - get value(): string { - return this._chatWidget.getInput(); - } - - set value(value: string) { - this._chatWidget.setInput(value); - } - - - selectAll(includeSlashCommand: boolean = true) { - // DEBT@jrieken - // REMOVE when agents are adopted - let startColumn = 1; - if (!includeSlashCommand) { - const match = /^(\/\w+)\s*/.exec(this._chatWidget.inputEditor.getModel()!.getLineContent(1)); - if (match) { - startColumn = match[1].length + 1; - } - } - this._chatWidget.inputEditor.setSelection(new Selection(1, startColumn, Number.MAX_SAFE_INTEGER, 1)); - } - - set placeholder(value: string) { - this._chatWidget.setInputPlaceholder(value); - } - - updateToolbar(show: boolean) { - this._elements.root.classList.toggle('toolbar', show); - this._elements.statusToolbar.classList.toggle('hidden', !show); - this._elements.feedbackToolbar.classList.toggle('hidden', !show); - this._elements.status.classList.toggle('actions', show); - this._elements.infoLabel.classList.toggle('hidden', show); - this._onDidChangeHeight.fire(); - } - - async getCodeBlockInfo(codeBlockIndex: number): Promise { - const { viewModel } = this._chatWidget; - if (!viewModel) { - return undefined; - } - const items = viewModel.getItems().filter(i => isResponseVM(i)); - if (!items.length) { - return; - } - const item = items[items.length - 1]; - return viewModel.codeBlockModelCollection.get(viewModel.sessionId, item, codeBlockIndex)?.model; - } - - get responseContent(): string | undefined { - const requests = this._chatWidget.viewModel?.model.getRequests(); - if (!isNonEmptyArray(requests)) { - return undefined; - } - return tail(requests)?.response?.response.asString(); - } - - - getChatModel(): IChatModel { - return this._chatWidget.viewModel?.model ?? this._defaultChatModel; - } - - setChatModel(chatModel: IChatModel) { - this._chatWidget.setModel(chatModel, { inputValue: undefined }); - } - - - /** - * @deprecated use `setChatModel` instead - */ - addToHistory(input: string) { - if (this._chatWidget.viewModel?.model === this._defaultChatModel) { - this._chatWidget.input.acceptInput(true); - } - } - - /** - * @deprecated use `setChatModel` instead - */ - updateChatMessage(message: IInlineChatMessage, isIncomplete: true): IInlineChatMessageAppender; - updateChatMessage(message: IInlineChatMessage | undefined): void; - updateChatMessage(message: IInlineChatMessage | undefined, isIncomplete?: boolean, isCodeBlockEditable?: boolean): IInlineChatMessageAppender | undefined; - updateChatMessage(message: IInlineChatMessage | undefined, isIncomplete?: boolean, isCodeBlockEditable?: boolean): IInlineChatMessageAppender | undefined { - - if (!this._chatWidget.viewModel || this._chatWidget.viewModel.model !== this._defaultChatModel) { - // this can only be used with the default chat model - return; - } - - const model = this._defaultChatModel; - if (!message?.message.value) { - for (const request of model.getRequests()) { - model.removeRequest(request.id); - } - return; - } - - const chatRequest = model.addRequest({ parts: [], text: '' }, { variables: [] }, 0); - model.acceptResponseProgress(chatRequest, { - kind: 'markdownContent', - content: message.message - }); - - if (!isIncomplete) { - model.completeResponse(chatRequest); - return; - } - return { - cancel: () => model.cancelRequest(chatRequest), - complete: () => model.completeResponse(chatRequest), - appendContent: (fragment: string) => { - model.acceptResponseProgress(chatRequest, { - kind: 'markdownContent', - content: new MarkdownString(fragment) - }); - } - }; - } - - updateInfo(message: string): void { - this._elements.infoLabel.classList.toggle('hidden', !message); - const renderedMessage = renderLabelWithIcons(message); - reset(this._elements.infoLabel, ...renderedMessage); - this._onDidChangeHeight.fire(); - } - - updateStatus(message: string, ops: { classes?: string[]; resetAfter?: number; keepMessage?: boolean; title?: string } = {}) { - const isTempMessage = typeof ops.resetAfter === 'number'; - if (isTempMessage && !this._elements.statusLabel.dataset['state']) { - const statusLabel = this._elements.statusLabel.innerText; - const title = this._elements.statusLabel.dataset['title']; - const classes = Array.from(this._elements.statusLabel.classList.values()); - setTimeout(() => { - this.updateStatus(statusLabel, { classes, keepMessage: true, title }); - }, ops.resetAfter); - } - const renderedMessage = renderLabelWithIcons(message); - reset(this._elements.statusLabel, ...renderedMessage); - this._elements.statusLabel.className = `label status ${(ops.classes ?? []).join(' ')}`; - this._elements.statusLabel.classList.toggle('hidden', !message); - if (isTempMessage) { - this._elements.statusLabel.dataset['state'] = 'temp'; - } else { - delete this._elements.statusLabel.dataset['state']; - } - - if (ops.title) { - this._elements.statusLabel.dataset['title'] = ops.title; - } else { - delete this._elements.statusLabel.dataset['title']; - } - this._onDidChangeHeight.fire(); - } - - reset() { - this._chatWidget.saveState(); - this.updateChatMessage(undefined); - - reset(this._elements.statusLabel); - this._elements.statusLabel.classList.toggle('hidden', true); - this._elements.statusToolbar.classList.add('hidden'); - this._elements.feedbackToolbar.classList.add('hidden'); - this.updateInfo(''); - - this.chatWidget.setModel(this._defaultChatModel, {}); - - this._elements.accessibleViewer.classList.toggle('hidden', true); - this._onDidChangeHeight.fire(); - } - - focus() { - this._chatWidget.focusInput(); - } - - hasFocus() { - return this.domNode.contains(getActiveElement()); - } - -} - -const defaultAriaLabel = localize('aria-label', "Inline Chat Input"); - -export class EditorBasedInlineChatWidget extends InlineChatWidget { - - private readonly _accessibleViewer = this._store.add(new MutableDisposable()); - - constructor( - location: AideChatAgentLocation, - private readonly _parentEditor: ICodeEditor, - options: IInlineChatWidgetConstructionOptions, - @IContextKeyService contextKeyService: IContextKeyService, - @IKeybindingService keybindingService: IKeybindingService, - @IInstantiationService instantiationService: IInstantiationService, - @IAccessibilityService accessibilityService: IAccessibilityService, - @IConfigurationService configurationService: IConfigurationService, - @IAccessibleViewService accessibleViewService: IAccessibleViewService, - @ITextModelService textModelResolverService: ITextModelService, - @IAideChatService chatService: IAideChatService, - @IHoverService hoverService: IHoverService, - ) { - super(location, { ...options, chatWidgetViewOptions: { ...options.chatWidgetViewOptions, editorOverflowWidgetsDomNode: _parentEditor.getOverflowWidgetsDomNode() } }, instantiationService, contextKeyService, keybindingService, accessibilityService, configurationService, accessibleViewService, textModelResolverService, chatService, hoverService); - } - - // --- layout - - override get contentHeight(): number { - let result = super.contentHeight; - - if (this._accessibleViewer.value) { - result += this._accessibleViewer.value.height; - } - - return result; - } - - protected override _doLayout(dimension: Dimension): void { - - let newHeight = dimension.height; - - if (this._accessibleViewer.value) { - this._accessibleViewer.value.width = dimension.width - 12; - newHeight -= this._accessibleViewer.value.height; - } - - super._doLayout(dimension.with(undefined, newHeight)); - - // update/fix the height of the zone which was set to newHeight in super._doLayout - this._elements.root.style.height = `${dimension.height - this._getExtraHeight()}px`; - } - - override reset() { - this._accessibleViewer.clear(); - super.reset(); - } - - // --- accessible viewer - - showAccessibleHunk(session: Session, hunkData: HunkInformation): void { - - this._elements.accessibleViewer.classList.remove('hidden'); - this._accessibleViewer.clear(); - - this._accessibleViewer.value = this._instantiationService.createInstance(HunkAccessibleDiffViewer, - this._elements.accessibleViewer, - session, - hunkData, - new AccessibleHunk(this._parentEditor, session, hunkData) - ); - - this._onDidChangeHeight.fire(); - } -} - -class HunkAccessibleDiffViewer extends AccessibleDiffViewer { - - readonly height: number; - - set width(value: number) { - this._width2.set(value, undefined); - } - - private readonly _width2: ISettableObservable; - - constructor( - parentNode: HTMLElement, - session: Session, - hunk: HunkInformation, - models: IAccessibleDiffViewerModel, - @IInstantiationService instantiationService: IInstantiationService, - ) { - const width = observableValue('width', 0); - const diff = observableValue('diff', HunkAccessibleDiffViewer._asMapping(hunk)); - const diffs = derived(r => [diff.read(r)]); - const lines = Math.min(10, 8 + diff.get().changedLineCount); - const height = models.getModifiedOptions().get(EditorOption.lineHeight) * lines; - - super(parentNode, constObservable(true), () => { }, constObservable(false), width, constObservable(height), diffs, models, instantiationService); - - this.height = height; - this._width2 = width; - - this._store.add(session.textModelN.onDidChangeContent(() => { - diff.set(HunkAccessibleDiffViewer._asMapping(hunk), undefined); - })); - } - - private static _asMapping(hunk: HunkInformation): DetailedLineRangeMapping { - const ranges0 = hunk.getRanges0(); - const rangesN = hunk.getRangesN(); - const originalLineRange = LineRange.fromRangeInclusive(ranges0[0]); - const modifiedLineRange = LineRange.fromRangeInclusive(rangesN[0]); - const innerChanges: RangeMapping[] = []; - for (let i = 1; i < ranges0.length; i++) { - innerChanges.push(new RangeMapping(ranges0[i], rangesN[i])); - } - return new DetailedLineRangeMapping(originalLineRange, modifiedLineRange, innerChanges); - } - -} - -class AccessibleHunk implements IAccessibleDiffViewerModel { - - constructor( - private readonly _editor: ICodeEditor, - private readonly _session: Session, - private readonly _hunk: HunkInformation - ) { } - - getOriginalModel(): ITextModel { - return this._session.textModel0; - } - getModifiedModel(): ITextModel { - return this._session.textModelN; - } - getOriginalOptions(): IComputedEditorOptions { - return this._editor.getOptions(); - } - getModifiedOptions(): IComputedEditorOptions { - return this._editor.getOptions(); - } - originalReveal(range: Range): void { - // throw new Error('Method not implemented.'); - } - modifiedReveal(range?: Range | undefined): void { - this._editor.revealRangeInCenterIfOutsideViewport(range || this._hunk.getRangesN()[0], ScrollType.Smooth); - } - modifiedSetSelection(range: Range): void { - // this._editor.revealRangeInCenterIfOutsideViewport(range, ScrollType.Smooth); - // this._editor.setSelection(range); - } - modifiedFocus(): void { - this._editor.focus(); - } - getModifiedPosition(): Position | undefined { - return this._hunk.getRangesN()[0].getStartPosition(); - } -} diff --git a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatZoneWidget.ts b/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatZoneWidget.ts deleted file mode 100644 index 831e92e5254..00000000000 --- a/src/vs/workbench/contrib/inlineAideChat/browser/inlineChatZoneWidget.ts +++ /dev/null @@ -1,227 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ -import { addDisposableListener, Dimension } from '../../../../base/browser/dom.js'; -import * as aria from '../../../../base/browser/ui/aria/aria.js'; -import { toDisposable } from '../../../../base/common/lifecycle.js'; -import { assertType } from '../../../../base/common/types.js'; -import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js'; -import { EditorLayoutInfo, EditorOption } from '../../../../editor/common/config/editorOptions.js'; -import { Position } from '../../../../editor/common/core/position.js'; -import { Range } from '../../../../editor/common/core/range.js'; -import { ZoneWidget } from '../../../../editor/contrib/zoneWidget/browser/zoneWidget.js'; -import { localize } from '../../../../nls.js'; -import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'; -import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'; -import { ACTION_ACCEPT_CHANGES, ACTION_REGENERATE_RESPONSE, ACTION_TOGGLE_DIFF, CTX_INLINE_CHAT_OUTER_CURSOR_POSITION, EditMode, InlineChatConfigKeys, MENU_INLINE_CHAT_EXECUTE, MENU_INLINE_CHAT_WIDGET_STATUS } from '../../../../workbench/contrib/inlineAideChat/common/inlineChat.js'; -import { EditorBasedInlineChatWidget } from './inlineChatWidget.js'; -import { isEqual } from '../../../../base/common/resources.js'; -import { StableEditorBottomScrollState } from '../../../../editor/browser/stableEditorScroll.js'; -import { ScrollType } from '../../../../editor/common/editorCommon.js'; -import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js'; -import { AideChatAgentLocation } from '../../../../workbench/contrib/aideChat/common/aideChatAgents.js'; -import { ILogService } from '../../../../platform/log/common/log.js'; - -export class InlineChatZoneWidget extends ZoneWidget { - - readonly widget: EditorBasedInlineChatWidget; - - private readonly _ctxCursorPosition: IContextKey<'above' | 'below' | ''>; - private _dimension?: Dimension; - - constructor( - location: AideChatAgentLocation, - editor: ICodeEditor, - @IInstantiationService private readonly _instaService: IInstantiationService, - @ILogService private _logService: ILogService, - @IContextKeyService contextKeyService: IContextKeyService, - @IConfigurationService configurationService: IConfigurationService, - ) { - super(editor, { showFrame: false, showArrow: false, isAccessible: true, className: 'inline-chat-widget', keepEditorSelection: true, showInHiddenAreas: true, ordinal: 10000 }); - - this._ctxCursorPosition = CTX_INLINE_CHAT_OUTER_CURSOR_POSITION.bindTo(contextKeyService); - - this._disposables.add(toDisposable(() => { - this._ctxCursorPosition.reset(); - })); - - this.widget = this._instaService.createInstance(EditorBasedInlineChatWidget, location, this.editor, { - statusMenuId: { - menu: MENU_INLINE_CHAT_WIDGET_STATUS, - options: { - buttonConfigProvider: action => { - if (new Set([ACTION_REGENERATE_RESPONSE, ACTION_TOGGLE_DIFF]).has(action.id)) { - return { isSecondary: true, showIcon: true, showLabel: false }; - } else if (action.id === ACTION_ACCEPT_CHANGES) { - return { isSecondary: false }; - } else { - return { isSecondary: true }; - } - } - } - }, - chatWidgetViewOptions: { - menus: { - executeToolbar: MENU_INLINE_CHAT_EXECUTE, - telemetrySource: 'interactiveEditorWidget-toolbar', - }, - rendererOptions: { - renderTextEditsAsSummary: (uri) => { - // render edits as summary only when using Live mode and when - // dealing with the current file in the editor - return isEqual(uri, editor.getModel()?.uri) - && configurationService.getValue(InlineChatConfigKeys.Mode) === EditMode.Live; - }, - } - } - }); - this._disposables.add(this.widget); - - let scrollState: StableEditorBottomScrollState | undefined; - this._disposables.add(this.widget.chatWidget.onWillMaybeChangeHeight(() => { - if (this.position) { - scrollState = StableEditorBottomScrollState.capture(this.editor); - } - })); - this._disposables.add(this.widget.onDidChangeHeight(() => { - if (this.position) { - // only relayout when visible - scrollState ??= StableEditorBottomScrollState.capture(this.editor); - const height = this._computeHeight(); - this._relayout(height.linesValue); - scrollState.restore(this.editor); - scrollState = undefined; - this._revealTopOfZoneWidget(this.position, height); - } - })); - - this.create(); - - this._disposables.add(addDisposableListener(this.domNode, 'click', e => { - if (!this.editor.hasWidgetFocus() && !this.widget.hasFocus()) { - this.editor.focus(); - } - }, true)); - - - // todo@jrieken listen ONLY when showing - const updateCursorIsAboveContextKey = () => { - if (!this.position || !this.editor.hasModel()) { - this._ctxCursorPosition.reset(); - } else if (this.position.lineNumber === this.editor.getPosition().lineNumber) { - this._ctxCursorPosition.set('above'); - } else if (this.position.lineNumber + 1 === this.editor.getPosition().lineNumber) { - this._ctxCursorPosition.set('below'); - } else { - this._ctxCursorPosition.reset(); - } - }; - this._disposables.add(this.editor.onDidChangeCursorPosition(e => updateCursorIsAboveContextKey())); - this._disposables.add(this.editor.onDidFocusEditorText(e => updateCursorIsAboveContextKey())); - updateCursorIsAboveContextKey(); - } - - protected override _fillContainer(container: HTMLElement): void { - container.appendChild(this.widget.domNode); - } - - protected override _doLayout(heightInPixel: number): void { - - const info = this.editor.getLayoutInfo(); - let width = info.contentWidth - (info.glyphMarginWidth + info.decorationsWidth); - width = Math.min(640, width); - - this._dimension = new Dimension(width, heightInPixel); - this.widget.layout(this._dimension); - } - - private _computeHeight(): { linesValue: number; pixelsValue: number } { - const chatContentHeight = this.widget.contentHeight; - const editorHeight = this.editor.getLayoutInfo().height; - - const contentHeight = Math.min(chatContentHeight, Math.max(this.widget.minHeight, editorHeight * 0.42)); - const heightInLines = contentHeight / this.editor.getOption(EditorOption.lineHeight); - return { linesValue: heightInLines, pixelsValue: contentHeight }; - } - - protected override _onWidth(_widthInPixel: number): void { - if (this._dimension) { - this._doLayout(this._dimension.height); - } - } - - override show(position: Position): void { - assertType(this.container); - - const scrollState = StableEditorBottomScrollState.capture(this.editor); - const info = this.editor.getLayoutInfo(); - const marginWithoutIndentation = info.glyphMarginWidth + info.decorationsWidth + info.lineNumbersWidth; - this.container.style.marginLeft = `${marginWithoutIndentation}px`; - - const height = this._computeHeight(); - super.show(position, height.linesValue); - this.widget.chatWidget.setVisible(true); - this.widget.focus(); - - scrollState.restore(this.editor); - - this._revealTopOfZoneWidget(position, height); - } - - override updatePositionAndHeight(position: Position): void { - const scrollState = StableEditorBottomScrollState.capture(this.editor); - const height = this._computeHeight(); - super.updatePositionAndHeight(position, height.linesValue); - scrollState.restore(this.editor); - - this._revealTopOfZoneWidget(position, height); - } - - private _revealTopOfZoneWidget(position: Position, height: { linesValue: number; pixelsValue: number }) { - - // reveal top of zone widget - - const lineNumber = position.lineNumber <= 1 ? 1 : 1 + position.lineNumber; - - const scrollTop = this.editor.getScrollTop(); - const lineTop = this.editor.getTopForLineNumber(lineNumber); - const zoneTop = lineTop - height.pixelsValue; - - const editorHeight = this.editor.getLayoutInfo().height; - const lineBottom = this.editor.getBottomForLineNumber(lineNumber); - - let newScrollTop = zoneTop; - let forceScrollTop = false; - - if (lineBottom >= (scrollTop + editorHeight)) { - // revealing the top of the zone would pust out the line we are interested it and - // therefore we keep the line in the view port - newScrollTop = lineBottom - editorHeight; - forceScrollTop = true; - } - - if (newScrollTop < scrollTop || forceScrollTop) { - this._logService.trace('[IE] REVEAL zone', { zoneTop, lineTop, lineBottom, scrollTop, newScrollTop, forceScrollTop }); - this.editor.setScrollTop(newScrollTop, ScrollType.Immediate); - } - } - - protected override revealRange(range: Range, isLastLine: boolean): void { - // noop - } - - protected override _getWidth(info: EditorLayoutInfo): number { - return info.width - info.minimap.minimapWidth; - } - - override hide(): void { - const scrollState = StableEditorBottomScrollState.capture(this.editor); - this._ctxCursorPosition.reset(); - this.widget.reset(); - this.widget.chatWidget.setVisible(false); - super.hide(); - aria.status(localize('inlineChatClosed', 'Closed inline chat widget')); - scrollState.restore(this.editor); - } -} diff --git a/src/vs/workbench/contrib/inlineAideChat/browser/media/inlineChat.css b/src/vs/workbench/contrib/inlineAideChat/browser/media/inlineChat.css deleted file mode 100644 index 1a1e12015f3..00000000000 --- a/src/vs/workbench/contrib/inlineAideChat/browser/media/inlineChat.css +++ /dev/null @@ -1,340 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-workbench .zone-widget.inline-chat-widget { - z-index: 3; -} - -.monaco-workbench .zone-widget.inline-chat-widget .cschat-session { - max-width: unset; -} - -.monaco-workbench .inline-chat { - color: inherit; - padding: 0 8px; - border-radius: 4px; - border: 1px solid var(--vscode-inlineChat-border); - box-shadow: 0 2px 4px 0 var(--vscode-widget-shadow); - margin-top: 8px; - background: var(--vscode-inlineChat-background); -} - -.monaco-workbench .inline-chat .chat-widget .cschat-session .cschat-input-part .interactive-execute-toolbar { - margin-bottom: 1px; -} - -.monaco-workbench .inline-chat .chat-widget .cschat-session .cschat-input-part .interactive-input-and-execute-toolbar { - width: 100%; - border-radius: 2px; -} - -.monaco-workbench .inline-chat .chat-widget .cschat-session .interactive-list { - padding: 2px 0 0 0; -} - -.monaco-workbench .inline-chat .chat-widget .cschat-session .interactive-list .interactive-item-container.interactive-item-compact { - padding: 2px 0; - gap: 6px; -} - -.monaco-workbench .inline-chat .chat-widget .cschat-session .interactive-list .interactive-item-container.interactive-item-compact .header .avatar { - outline-offset: -1px; -} - -.monaco-workbench .inline-chat .chat-widget .cschat-session .interactive-list .interactive-item-container.interactive-item-compact .chat-notification-widget { - margin-bottom: 0; - padding: 0; - border: none; -} - -.monaco-workbench .inline-chat .chat-widget .cschat-session .interactive-list .interactive-request { - border: none; -} - -/* progress bit */ - -.monaco-workbench .inline-chat .progress { - position: relative; -} - -/* UGLY - fighting against workbench styles */ -.monaco-workbench .part.editor > .content .inline-chat .progress .monaco-progress-container { - top: 0; -} - -/* status */ - -.monaco-workbench .inline-chat .status { - display: flex; - justify-content: space-between; - align-items: center; -} - -.monaco-workbench .inline-chat .status .actions.hidden { - display: none; -} - -.monaco-workbench .inline-chat .status .label { - overflow: hidden; - color: var(--vscode-descriptionForeground); - font-size: 11px; - display: inline-flex; -} - -.monaco-workbench .inline-chat .status .label.info { - margin-right: auto; - padding-left: 2px; -} - -.monaco-workbench .inline-chat .status .label.status { - margin-left: auto; -} - -.monaco-workbench .inline-chat .status .label.hidden { - display: none; -} - -.monaco-workbench .inline-chat .status .label.error { - color: var(--vscode-errorForeground); -} - -.monaco-workbench .inline-chat .status .label.warn { - color: var(--vscode-editorWarning-foreground); -} - -.monaco-workbench .inline-chat .status .label > .codicon { - padding: 0 5px; - font-size: 12px; - line-height: 18px; -} - -.monaco-workbench .inline-chat .chatMessage .chatMessageContent .value { - overflow: hidden; - -webkit-user-select: text; - user-select: text; -} - -.monaco-workbench .inline-chat .followUps { - padding: 5px 5px; -} - -.monaco-workbench .inline-chat .followUps .cschat-session-followups .monaco-button { - display: block; - color: var(--vscode-textLink-foreground); - font-size: 12px; -} - -.monaco-workbench .inline-chat .followUps.hidden { - display: none; -} - -.monaco-workbench .inline-chat .chatMessage { - padding: 0 3px; -} - -.monaco-workbench .inline-chat .chatMessage .chatMessageContent { - padding: 2px 2px; -} - -.monaco-workbench .inline-chat .chatMessage.hidden { - display: none; -} - -.monaco-workbench .inline-chat .status .actions, -.monaco-workbench .inline-chat-content-widget .toolbar { - - display: flex; - height: 18px; - padding: 3px 0; /* makes space for action focus borders: https://github.com/microsoft/vscode-copilot/issues/5814 */ - - .actions-container { - gap: 3px - } - - .action-item.text-only .action-label { - font-size: 12px; - line-height: 16px; - padding: 1px 2px; - border-radius: 3px; - } - - .monaco-action-bar .action-item.menu-entry.text-only:first-of-type .action-label{ - color: var(--vscode-button-foreground); - background-color: var(--vscode-button-background); - } - - .monaco-action-bar .action-item.menu-entry.text-only + .action-item:not(.text-only) > .monaco-dropdown .action-label { - font-size: 12px; - line-height: 16px; - width: unset; - height: unset; - } -} - -.monaco-workbench .inline-chat .status .actions > .monaco-button, -.monaco-workbench .inline-chat .status .actions > .monaco-button-dropdown { - margin-right: 4px; -} - -.monaco-workbench .inline-chat .status .actions > .monaco-button-dropdown > .monaco-dropdown-button { - display: flex; - align-items: center; - padding: 0 4px; -} - -.monaco-workbench .inline-chat .status .actions > .monaco-button.codicon { - display: flex; -} - -.monaco-workbench .inline-chat .status .actions > .monaco-button.codicon::before { - align-self: center; -} - -.monaco-workbench .inline-chat .status .actions .monaco-text-button { - padding: 0 2px; - white-space: nowrap; -} - -/* TODO@jrieken not needed? */ -.monaco-workbench .inline-chat .status .monaco-toolbar .action-label.checked { - color: var(--vscode-inputOption-activeForeground); - background-color: var(--vscode-inputOption-activeBackground); - outline: 1px solid var(--vscode-inputOption-activeBorder); -} - - -.monaco-workbench .inline-chat .status .monaco-toolbar .action-item.button-item .action-label:is(:hover, :focus) { - background-color: var(--vscode-button-hoverBackground); -} - -/* preview */ - -.monaco-workbench .inline-chat .preview { - display: none; -} - -.monaco-workbench .inline-chat .previewDiff, -.monaco-workbench .inline-chat .previewCreate { - display: inherit; - border: 1px solid var(--vscode-inlineChat-border); - border-radius: 2px; - margin: 6px 0px; -} - -.monaco-workbench .inline-chat .previewCreateTitle { - padding-top: 6px; -} - -.monaco-workbench .inline-chat .diff-review.hidden, -.monaco-workbench .inline-chat .previewDiff.hidden, -.monaco-workbench .inline-chat .previewCreate.hidden, -.monaco-workbench .inline-chat .previewCreateTitle.hidden { - display: none; -} - -.monaco-workbench .inline-chat-toolbar { - display: flex; -} - -.monaco-workbench .inline-chat-toolbar > .monaco-button { - margin-right: 6px; -} - -.monaco-workbench .inline-chat-toolbar .action-label.checked { - color: var(--vscode-inputOption-activeForeground); - background-color: var(--vscode-inputOption-activeBackground); - outline: 1px solid var(--vscode-inputOption-activeBorder); -} - -/* decoration styles */ - -.monaco-workbench .inline-chat-inserted-range { - background-color: var(--vscode-inlineChatDiff-inserted); -} - -.monaco-workbench .inline-chat-inserted-range-linehighlight { - background-color: var(--vscode-diffEditor-insertedLineBackground); -} - -.monaco-workbench .inline-chat-original-zone2 { - background-color: var(--vscode-diffEditor-removedLineBackground); - opacity: 0.8; -} - -.monaco-workbench .inline-chat-lines-inserted-range { - background-color: var(--vscode-diffEditor-insertedTextBackground); -} - -.monaco-workbench .inline-chat-block-selection { - background-color: var(--vscode-inlineChat-regionHighlight); -} - -.monaco-workbench .cschat-session .interactive-input-and-execute-toolbar .monaco-editor .inline-chat-slash-command { - background-color: var(--vscode-chat-slashCommandBackground); - color: var(--vscode-chat-slashCommandForeground); /* Overrides the foreground color rule in chat.css */ - border-radius: 2px; - padding: 1px; -} - -.monaco-workbench .inline-chat-slash-command-detail { - opacity: 0.5; -} - -/* diff zone */ - -.monaco-workbench .inline-chat-diff-widget .monaco-diff-editor .monaco-editor-background, -.monaco-workbench .inline-chat-diff-widget .monaco-diff-editor .monaco-workbench .margin-view-overlays { - background-color: var(--vscode-inlineChat-regionHighlight); -} - -/* create zone */ - -.monaco-workbench .inline-chat-newfile-widget { - background-color: var(--vscode-inlineChat-regionHighlight); -} - -.monaco-workbench .inline-chat-newfile-widget .title { - display: flex; - align-items: center; - justify-content: space-between; -} - -.monaco-workbench .inline-chat-newfile-widget .title .detail { - margin-left: 4px; -} - -.monaco-workbench .inline-chat-newfile-widget .buttonbar-widget { - display: flex; - margin-left: auto; - margin-right: 8px; -} - -.monaco-workbench .inline-chat-newfile-widget .buttonbar-widget > .monaco-button { - display: inline-flex; - white-space: nowrap; - margin-left: 4px; -} - -/* gutter decoration */ - -.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque, -.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent { - display: block; - cursor: pointer; - transition: opacity .2s ease-in-out; -} - -.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque { - opacity: 0.5; -} - -.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent { - opacity: 0; -} - -.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-opaque:hover, -.monaco-workbench .glyph-margin-widgets .cgmr.codicon-inline-chat-transparent:hover { - opacity: 1; -} diff --git a/src/vs/workbench/contrib/inlineAideChat/browser/media/inlineChatContentWidget.css b/src/vs/workbench/contrib/inlineAideChat/browser/media/inlineChatContentWidget.css deleted file mode 100644 index 1c2afa45fa4..00000000000 --- a/src/vs/workbench/contrib/inlineAideChat/browser/media/inlineChatContentWidget.css +++ /dev/null @@ -1,25 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -.monaco-workbench .inline-chat-content-widget { - z-index: 50; - padding: 6px 6px 0px 6px; - border-radius: 4px; - background-color: var(--vscode-inlineChat-background); - box-shadow: 0 4px 8px var(--vscode-inlineChat-shadow); -} - - -.monaco-workbench .inline-chat-content-widget.cschat-session .cschat-session { - max-width: unset; -} - -.monaco-workbench .inline-chat-content-widget.cschat-session .cschat-input-part .interactive-execute-toolbar { - margin-bottom: 1px; -} - -.monaco-workbench .inline-chat-content-widget.cschat-session .cschat-input-part.compact { - padding: 0; -} diff --git a/src/vs/workbench/contrib/inlineAideChat/browser/utils.ts b/src/vs/workbench/contrib/inlineAideChat/browser/utils.ts deleted file mode 100644 index 4077eae32c0..00000000000 --- a/src/vs/workbench/contrib/inlineAideChat/browser/utils.ts +++ /dev/null @@ -1,92 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { EditOperation } from '../../../../editor/common/core/editOperation.js'; -import { IRange } from '../../../../editor/common/core/range.js'; -import { IIdentifiedSingleEditOperation, ITextModel, IValidEditOperation, TrackedRangeStickiness } from '../../../../editor/common/model.js'; -import { IEditObserver } from './inlineChatStrategies.js'; -import { IProgress } from '../../../../platform/progress/common/progress.js'; -import { IntervalTimer, AsyncIterableSource } from '../../../../base/common/async.js'; -import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { getNWords } from '../../../../workbench/contrib/aideChat/common/aideChatWordCounter.js'; - - - -// --- async edit - -export interface AsyncTextEdit { - readonly range: IRange; - readonly newText: AsyncIterable; -} - -export async function performAsyncTextEdit(model: ITextModel, edit: AsyncTextEdit, progress?: IProgress, obs?: IEditObserver) { - - const [id] = model.deltaDecorations([], [{ - range: edit.range, - options: { - description: 'asyncTextEdit', - stickiness: TrackedRangeStickiness.AlwaysGrowsWhenTypingAtEdges - } - }]); - - let first = true; - for await (const part of edit.newText) { - - if (model.isDisposed()) { - break; - } - - const range = model.getDecorationRange(id); - if (!range) { - throw new Error('FAILED to perform async replace edit because the anchor decoration was removed'); - } - - const edit = first - ? EditOperation.replace(range, part) // first edit needs to override the "anchor" - : EditOperation.insert(range.getEndPosition(), part); - obs?.start(); - model.pushEditOperations(null, [edit], (undoEdits) => { - progress?.report(undoEdits); - return null; - }); - obs?.stop(); - first = false; - } -} - -export function asProgressiveEdit(interval: IntervalTimer, edit: IIdentifiedSingleEditOperation, wordsPerSec: number, token: CancellationToken): AsyncTextEdit { - - wordsPerSec = Math.max(100000, wordsPerSec); - - const stream = new AsyncIterableSource(); - let newText = edit.text ?? ''; - - interval.cancelAndSet(() => { - if (token.isCancellationRequested) { - return; - } - const r = getNWords(newText, 1); - stream.emitOne(r.value); - newText = newText.substring(r.value.length); - if (r.isFullString) { - interval.cancel(); - stream.resolve(); - d.dispose(); - } - - }, 1000 / wordsPerSec); - - // cancel ASAP - const d = token.onCancellationRequested(() => { - interval.cancel(); - stream.resolve(); - d.dispose(); - }); - - return { - range: edit.range, - newText: stream.asyncIterable - }; -} diff --git a/src/vs/workbench/contrib/inlineAideChat/common/inlineChat.ts b/src/vs/workbench/contrib/inlineAideChat/common/inlineChat.ts deleted file mode 100644 index 133536267ab..00000000000 --- a/src/vs/workbench/contrib/inlineAideChat/common/inlineChat.ts +++ /dev/null @@ -1,140 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See License.txt in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import { localize } from '../../../../nls.js'; -import { MenuId } from '../../../../platform/actions/common/actions.js'; -import { Extensions, IConfigurationRegistry } from '../../../../platform/configuration/common/configurationRegistry.js'; -import { ContextKeyExpr, RawContextKey } from '../../../../platform/contextkey/common/contextkey.js'; -import { Registry } from '../../../../platform/registry/common/platform.js'; -import { diffInserted, diffRemoved, editorWidgetBackground, editorWidgetBorder, editorWidgetForeground, focusBorder, inputBackground, inputPlaceholderForeground, registerColor, transparent, widgetShadow } from '../../../../platform/theme/common/colorRegistry.js'; - -// settings - -export const enum InlineChatConfigKeys { - Mode = 'inlineChat.mode', - FinishOnType = 'inlineChat.finishOnType', - AcceptedOrDiscardBeforeSave = 'inlineChat.acceptedOrDiscardBeforeSave', - HoldToSpeech = 'inlineChat.holdToSpeech', - AccessibleDiffView = 'inlineChat.accessibleDiffView', - ExpTextButtons = 'inlineChat.experimental.textButtons' -} - -export const enum EditMode { - Live = 'live', - Preview = 'preview' -} - -Registry.as(Extensions.Configuration).registerConfiguration({ - id: 'editor', - properties: { - [InlineChatConfigKeys.Mode]: { - description: localize('mode', "Configure if changes crafted with inline chat are applied directly to the document or are previewed first."), - default: EditMode.Live, - type: 'string', - enum: [EditMode.Live, EditMode.Preview], - markdownEnumDescriptions: [ - localize('mode.live', "Changes are applied directly to the document, can be highlighted via inline diffs, and accepted/discarded by hunks. Ending a session will keep the changes."), - localize('mode.preview', "Changes are previewed only and need to be accepted via the apply button. Ending a session will discard the changes."), - ], - tags: ['experimental'] - }, - [InlineChatConfigKeys.FinishOnType]: { - description: localize('finishOnType', "Whether to finish an inline chat session when typing outside of changed regions."), - default: false, - type: 'boolean' - }, - [InlineChatConfigKeys.AcceptedOrDiscardBeforeSave]: { - description: localize('acceptedOrDiscardBeforeSave', "Whether pending inline chat sessions prevent saving."), - default: true, - type: 'boolean' - }, - [InlineChatConfigKeys.HoldToSpeech]: { - description: localize('holdToSpeech', "Whether holding the inline chat keybinding will automatically enable speech recognition."), - default: true, - type: 'boolean' - }, - [InlineChatConfigKeys.AccessibleDiffView]: { - description: localize('accessibleDiffView', "Whether the inline chat also renders an accessible diff viewer for its changes."), - default: 'auto', - type: 'string', - enum: ['auto', 'on', 'off'], - markdownEnumDescriptions: [ - localize('accessibleDiffView.auto', "The accessible diff viewer is based screen reader mode being enabled."), - localize('accessibleDiffView.on', "The accessible diff viewer is always enabled."), - localize('accessibleDiffView.off', "The accessible diff viewer is never enabled."), - ], - }, - [InlineChatConfigKeys.ExpTextButtons]: { - description: localize('txtButtons', "Whether to use textual buttons (Requires restart)."), - default: false, - type: 'boolean' - }, - } -}); - - -export const INLINE_CHAT_ID = 'inlineAideChat'; -export const INTERACTIVE_EDITOR_ACCESSIBILITY_HELP_ID = 'interactiveEditorAccessiblityHelp'; - -// --- CONTEXT - -export const enum InlineChatResponseType { - None = 'none', - Messages = 'messages', - MessagesAndEdits = 'messagesAndEdits' -} - -export const CTX_INLINE_CHAT_HAS_AGENT = new RawContextKey('inlineAideChatHasProvider', false, localize('inlineAideChatHasProvider', "Whether a provider for interactive editors exists")); -export const CTX_INLINE_CHAT_VISIBLE = new RawContextKey('inlineAideChatVisible', false, localize('inlineAideChatVisible', "Whether the interactive editor input is visible")); -export const CTX_INLINE_CHAT_FOCUSED = new RawContextKey('inlineAideChatFocused', false, localize('inlineAideChatFocused', "Whether the interactive editor input is focused")); -export const CTX_INLINE_CHAT_RESPONSE_FOCUSED = new RawContextKey('inlineAideChatResponseFocused', false, localize('inlineAideChatResponseFocused', "Whether the interactive widget's response is focused")); -export const CTX_INLINE_CHAT_EMPTY = new RawContextKey('inlineAideChatEmpty', false, localize('inlineAideChatEmpty', "Whether the interactive editor input is empty")); -export const CTX_INLINE_CHAT_INNER_CURSOR_FIRST = new RawContextKey('inlineAideChatInnerCursorFirst', false, localize('inlineAideChatInnerCursorFirst', "Whether the cursor of the iteractive editor input is on the first line")); -export const CTX_INLINE_CHAT_INNER_CURSOR_LAST = new RawContextKey('inlineAideChatInnerCursorLast', false, localize('inlineAideChatInnerCursorLast', "Whether the cursor of the iteractive editor input is on the last line")); -export const CTX_INLINE_CHAT_INNER_CURSOR_START = new RawContextKey('inlineAideChatInnerCursorStart', false, localize('inlineAideChatInnerCursorStart', "Whether the cursor of the iteractive editor input is on the start of the input")); -export const CTX_INLINE_CHAT_INNER_CURSOR_END = new RawContextKey('inlineAideChatInnerCursorEnd', false, localize('inlineAideChatInnerCursorEnd', "Whether the cursor of the iteractive editor input is on the end of the input")); -export const CTX_INLINE_CHAT_OUTER_CURSOR_POSITION = new RawContextKey<'above' | 'below' | ''>('inlineAideChatOuterCursorPosition', '', localize('inlineAideChatOuterCursorPosition', "Whether the cursor of the outer editor is above or below the interactive editor input")); -export const CTX_INLINE_CHAT_HAS_STASHED_SESSION = new RawContextKey('inlineAideChatHasStashedSession', false, localize('inlineAideChatHasStashedSession', "Whether interactive editor has kept a session for quick restore")); -export const CTX_INLINE_CHAT_USER_DID_EDIT = new RawContextKey('inlineAideChatUserDidEdit', undefined, localize('inlineAideChatUserDidEdit', "Whether the user did changes ontop of the inline chat")); -export const CTX_INLINE_CHAT_DOCUMENT_CHANGED = new RawContextKey('inlineAideChatDocumentChanged', false, localize('inlineAideChatDocumentChanged', "Whether the document has changed concurrently")); -export const CTX_INLINE_CHAT_CHANGE_HAS_DIFF = new RawContextKey('inlineAideChatChangeHasDiff', false, localize('inlineAideChatChangeHasDiff', "Whether the current change supports showing a diff")); -export const CTX_INLINE_CHAT_CHANGE_SHOWS_DIFF = new RawContextKey('inlineAideChatChangeShowsDiff', false, localize('inlineAideChatChangeShowsDiff', "Whether the current change showing a diff")); -export const CTX_INLINE_CHAT_EDIT_MODE = new RawContextKey('config.inlineChat.mode', EditMode.Live); -export const CTX_INLINE_CHAT_REQUEST_IN_PROGRESS = new RawContextKey('inlineAideChatRequestInProgress', false, localize('inlineAideChatRequestInProgress', "Whether an inline chat request is currently in progress")); -export const CTX_INLINE_CHAT_RESPONSE_TYPE = new RawContextKey('inlineAideChatResponseType', InlineChatResponseType.None, localize('inlineAideChatResponseTypes', "What type was the responses have been receieved, nothing yet, just messages, or messaged and local edits")); - -export const CTX_INLINE_CHAT_CONFIG_TXT_BTNS = ContextKeyExpr.equals(`config.${[InlineChatConfigKeys.ExpTextButtons]}`, true); - -// --- (selected) action identifier - -export const ACTION_ACCEPT_CHANGES = 'inlineAideChat.acceptChanges'; -export const ACTION_REGENERATE_RESPONSE = 'inlineAideChat.regenerate'; -export const ACTION_VIEW_IN_CHAT = 'inlineAideChat.viewInChat'; -export const ACTION_TOGGLE_DIFF = 'inlineAideChat.toggleDiff'; - -// --- menus - -export const MENU_INLINE_CHAT_EXECUTE = MenuId.for('inlineAideChat.execute'); -export const MENU_INLINE_CHAT_CONTENT_STATUS = MenuId.for('inlineAideChat.content.status'); -export const MENU_INLINE_CHAT_WIDGET_STATUS = MenuId.for('inlineAideChatWidget.status'); - -// --- colors - - -export const inlineChatForeground = registerColor('inlineChat.foreground', { dark: editorWidgetForeground, light: editorWidgetForeground, hcDark: editorWidgetForeground, hcLight: editorWidgetForeground }, localize('inlineChat.foreground', "Foreground color of the interactive editor widget")); -export const inlineChatBackground = registerColor('inlineChat.background', { dark: editorWidgetBackground, light: editorWidgetBackground, hcDark: editorWidgetBackground, hcLight: editorWidgetBackground }, localize('inlineChat.background', "Background color of the interactive editor widget")); -export const inlineChatBorder = registerColor('inlineChat.border', { dark: editorWidgetBorder, light: editorWidgetBorder, hcDark: editorWidgetBorder, hcLight: editorWidgetBorder }, localize('inlineChat.border', "Border color of the interactive editor widget")); -export const inlineChatShadow = registerColor('inlineChat.shadow', { dark: widgetShadow, light: widgetShadow, hcDark: widgetShadow, hcLight: widgetShadow }, localize('inlineChat.shadow', "Shadow color of the interactive editor widget")); -export const inlineChatInputBorder = registerColor('inlineChatInput.border', { dark: editorWidgetBorder, light: editorWidgetBorder, hcDark: editorWidgetBorder, hcLight: editorWidgetBorder }, localize('inlineChatInput.border', "Border color of the interactive editor input")); -export const inlineChatInputFocusBorder = registerColor('inlineChatInput.focusBorder', { dark: focusBorder, light: focusBorder, hcDark: focusBorder, hcLight: focusBorder }, localize('inlineChatInput.focusBorder', "Border color of the interactive editor input when focused")); -export const inlineChatInputPlaceholderForeground = registerColor('inlineChatInput.placeholderForeground', { dark: inputPlaceholderForeground, light: inputPlaceholderForeground, hcDark: inputPlaceholderForeground, hcLight: inputPlaceholderForeground }, localize('inlineChatInput.placeholderForeground', "Foreground color of the interactive editor input placeholder")); -export const inlineChatInputBackground = registerColor('inlineChatInput.background', { dark: inputBackground, light: inputBackground, hcDark: inputBackground, hcLight: inputBackground }, localize('inlineChatInput.background', "Background color of the interactive editor input")); - -export const inlineChatDiffInserted = registerColor('inlineChatDiff.inserted', { dark: transparent(diffInserted, .5), light: transparent(diffInserted, .5), hcDark: transparent(diffInserted, .5), hcLight: transparent(diffInserted, .5) }, localize('inlineChatDiff.inserted', "Background color of inserted text in the interactive editor input")); -export const overviewRulerInlineChatDiffInserted = registerColor('editorOverviewRuler.inlineChatInserted', { dark: transparent(diffInserted, 0.6), light: transparent(diffInserted, 0.8), hcDark: transparent(diffInserted, 0.6), hcLight: transparent(diffInserted, 0.8) }, localize('editorOverviewRuler.inlineChatInserted', 'Overview ruler marker color for inline chat inserted content.')); -export const minimapInlineChatDiffInserted = registerColor('editorOverviewRuler.inlineChatInserted', { dark: transparent(diffInserted, 0.6), light: transparent(diffInserted, 0.8), hcDark: transparent(diffInserted, 0.6), hcLight: transparent(diffInserted, 0.8) }, localize('editorOverviewRuler.inlineChatInserted', 'Overview ruler marker color for inline chat inserted content.')); - -export const inlineChatDiffRemoved = registerColor('inlineChatDiff.removed', { dark: transparent(diffRemoved, .5), light: transparent(diffRemoved, .5), hcDark: transparent(diffRemoved, .5), hcLight: transparent(diffRemoved, .5) }, localize('inlineChatDiff.removed', "Background color of removed text in the interactive editor input")); -export const overviewRulerInlineChatDiffRemoved = registerColor('editorOverviewRuler.inlineChatRemoved', { dark: transparent(diffRemoved, 0.6), light: transparent(diffRemoved, 0.8), hcDark: transparent(diffRemoved, 0.6), hcLight: transparent(diffRemoved, 0.8) }, localize('editorOverviewRuler.inlineChatRemoved', 'Overview ruler marker color for inline chat removed content.')); diff --git a/src/vs/workbench/workbench.common.main.ts b/src/vs/workbench/workbench.common.main.ts index 57376096a45..c2f74e868e7 100644 --- a/src/vs/workbench/workbench.common.main.ts +++ b/src/vs/workbench/workbench.common.main.ts @@ -198,8 +198,6 @@ import './contrib/chat/browser/chat.contribution.js'; import './contrib/inlineChat/browser/inlineChat.contribution.js'; // Aide -import './contrib/aideChat/browser/aideChat.contribution.js'; -import './contrib/inlineAideChat/browser/inlineChat.contribution.js'; import './contrib/aideProbe/browser/aideProbe.contribution.js'; import './contrib/aideAddContext/browser/aideAddContext.contribution.js'; diff --git a/src/vs/workbench/workbench.desktop.main.ts b/src/vs/workbench/workbench.desktop.main.ts index bb522b63c79..aa0338e7fb7 100644 --- a/src/vs/workbench/workbench.desktop.main.ts +++ b/src/vs/workbench/workbench.desktop.main.ts @@ -174,10 +174,6 @@ import './contrib/remoteTunnel/electron-sandbox/remoteTunnel.contribution.js'; import './contrib/chat/electron-sandbox/chat.contribution.js'; import './contrib/inlineChat/electron-sandbox/inlineChat.contribution.js'; -// Aide -// import 'vs/workbench/contrib/aideChat/electron-sandbox/aideChat.contribution'; -// import 'vs/workbench/contrib/inlineAideChat/electron-sandbox/inlineChat.contribution'; - // Encryption import './contrib/encryption/electron-sandbox/encryption.contribution.js'; diff --git a/src/vscode-dts/vscode.proposed.aideChatParticipant.d.ts b/src/vscode-dts/vscode.proposed.aideChatParticipant.d.ts deleted file mode 100644 index b020189c1d3..00000000000 --- a/src/vscode-dts/vscode.proposed.aideChatParticipant.d.ts +++ /dev/null @@ -1,178 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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' { - - /** - * The location at which the chat is happening. - */ - export enum AideChatLocation { - /** - * The chat panel - */ - Panel = 1, - /** - * Terminal inline chat - */ - Terminal = 2, - /** - * Notebook inline chat - */ - Notebook = 3, - /** - * Code editor inline chat - */ - Editor = 4 - } - - export interface AideChatWelcomeMessageProvider { - provideWelcomeMessage(location: AideChatLocation, token: CancellationToken): ProviderResult; - provideSampleQuestions?(location: AideChatLocation, token: CancellationToken): ProviderResult; - } - - export interface AideChatParticipant extends Omit { - /** - * The handler for requests to this participant. - */ - requestHandler: AideChatExtendedRequestHandler; - - /** - * The welcome message provider for this participant. - */ - welcomeMessageProvider?: AideChatWelcomeMessageProvider; - - /** - * An event that fires whenever feedback for a result is received, e.g. when a user up- or down-votes - * a result. - * - * The passed {@link ChatResultFeedback.result result} is guaranteed to be the same instance that was - * previously returned from this chat participant. - */ - onDidReceiveFeedback: Event; - } - - export interface AideChatRequest extends Omit { - readonly threadId: string; - - /** - * The location at which the chat is happening. This will always be one of the supported values - */ - readonly location: AideChatLocation; - } - - interface CodeReferenceByName { - readonly name: string; - readonly uri: Uri; - } - - export interface AideChatResponseBreakdown { - /** - * Code reference relevant to this breakdown. - */ - readonly reference: CodeReferenceByName; - - /** - * The query made to AI for this breakdown. - */ - readonly query?: MarkdownString; - - /** - * The reason this query was made. - */ - readonly reason?: MarkdownString; - - /** - * The response from AI for this breakdown. - */ - readonly response?: MarkdownString; - } - - export interface AideProbeResponseOpenFile { - /** - * The file where the agent went to the definition. - */ - readonly uri: Uri; - } - - export interface AideProbeResponseRepoMapGeneration { - /** - * Whether the repo map generation is finished - */ - readonly finished: boolean; - } - - export interface AideProbeResponseLongContextSearch { - /** - * Whether the repo map generation is finished - */ - readonly finished: boolean; - } - - export interface AideProbeCodeIterationFinishedPart { - readonly finished: boolean; - } - - export interface AideChatResponseTextEdit { - /** - * The file where the agent went to the definition. - */ - readonly uri: Uri; - - /** - * Edits applied to the document - */ - readonly edits: TextEdit[]; - } - - export interface AideChatResponseStream extends ChatResponseStream { - } - - export type AideChatExtendedRequestHandler = (request: AideChatRequest, context: ChatContext, response: AideChatResponseStream, token: CancellationToken) => ProviderResult; - - /** - * Represents the type of user feedback received. - */ - export enum AideChatResultFeedbackKind { - /** - * The user marked the result as unhelpful. - */ - Unhelpful = 0, - - /** - * The user marked the result as helpful. - */ - Helpful = 1, - } - - export interface AideChatResultFeedback { - /** - * The ChatResult for which the user is providing feedback. - * This object has the same properties as the result returned from the participant callback, including `metadata`, but is not the same instance. - */ - readonly result: ChatResult; - - /** - * The kind of feedback that was received. - */ - readonly kind: AideChatResultFeedbackKind; - } - - export namespace aideChat { - /** - * Current version of the proposal. Changes whenever backwards-incompatible changes are made. - * If a new feature is added that doesn't break existing code, the version is not incremented. When the extension uses this new feature, it should set its engines.vscode version appropriately. - * But if a change is made to an existing feature that would break existing code, the version should be incremented. - * The chat extension should not activate if it doesn't support the current version. - */ - export const _version: 1 | number; - - /** - * Create a chat participant with the extended progress type - */ - export function createChatParticipant(id: string, handler: AideChatExtendedRequestHandler): AideChatParticipant; - - export function createDynamicChatParticipant(id: string, dynamicProps: DynamicChatParticipantProps, handler: AideChatExtendedRequestHandler): AideChatParticipant; - } -} diff --git a/src/vscode-dts/vscode.proposed.aideChatVariableResolver.d.ts b/src/vscode-dts/vscode.proposed.aideChatVariableResolver.d.ts deleted file mode 100644 index 30743c41740..00000000000 --- a/src/vscode-dts/vscode.proposed.aideChatVariableResolver.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * 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 namespace aideChat { - - /** - * Register a variable which can be used in a chat request to any participant. - * @param id A unique ID for the variable. - * @param name The name of the variable, to be used in the chat input as `#name`. - * @param userDescription A description of the variable for the chat input suggest widget. - * @param modelDescription A description of the variable for the model. - * @param isSlow Temp, to limit access to '#codebase' which is not a 'reference' and will fit into a tools API later. - * @param resolver Will be called to provide the chat variable's value when it is used. - * @param fullName The full name of the variable when selecting context in the picker UI. - * @param icon An icon to display when selecting context in the picker UI. - */ - export function registerChatVariableResolver(id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: ChatVariableResolver, fullName?: string, icon?: ThemeIcon): Disposable; - } -} diff --git a/src/vscode-dts/vscode.proposed.aideProbe.d.ts b/src/vscode-dts/vscode.proposed.aideProbe.d.ts index 28ec0af8096..b197811b434 100644 --- a/src/vscode-dts/vscode.proposed.aideProbe.d.ts +++ b/src/vscode-dts/vscode.proposed.aideProbe.d.ts @@ -43,6 +43,58 @@ declare module 'vscode' { type: 'followUpRequest'; } + interface CodeReferenceByName { + readonly name: string; + readonly uri: Uri; + } + + export interface AideChatResponseBreakdown { + /** + * Code reference relevant to this breakdown. + */ + readonly reference: CodeReferenceByName; + + /** + * The query made to AI for this breakdown. + */ + readonly query?: MarkdownString; + + /** + * The reason this query was made. + */ + readonly reason?: MarkdownString; + + /** + * The response from AI for this breakdown. + */ + readonly response?: MarkdownString; + } + + export interface AideProbeResponseOpenFile { + /** + * The file where the agent went to the definition. + */ + readonly uri: Uri; + } + + export interface AideProbeResponseRepoMapGeneration { + /** + * Whether the repo map generation is finished + */ + readonly finished: boolean; + } + + export interface AideProbeResponseLongContextSearch { + /** + * Whether the repo map generation is finished + */ + readonly finished: boolean; + } + + export interface AideProbeCodeIterationFinishedPart { + readonly finished: boolean; + } + export interface AideProbeSessionAction { sessionId: string; action: FollowAlongAction | NavigateBreakdownAction | NewIterationAction | ContextChangedAction | FollowUpRequestAction | AnchorSessionStart;