Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts
5285 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Codicon } from '../../../../../base/common/codicons.js';6import { h } from '../../../../../base/browser/dom.js';7import { Disposable, IDisposable, markAsSingleton } from '../../../../../base/common/lifecycle.js';8import { Schemas } from '../../../../../base/common/network.js';9import { basename } from '../../../../../base/common/resources.js';10import { ThemeIcon } from '../../../../../base/common/themables.js';11import { URI } from '../../../../../base/common/uri.js';12import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';13import { isITextModel } from '../../../../../editor/common/model.js';14import { localize, localize2 } from '../../../../../nls.js';15import { ActionWidgetDropdownActionViewItem } from '../../../../../platform/actions/browser/actionWidgetDropdownActionViewItem.js';16import { IActionViewItemService } from '../../../../../platform/actions/browser/actionViewItemService.js';17import { Action2, MenuId, MenuItemAction } from '../../../../../platform/actions/common/actions.js';18import { IActionWidgetService } from '../../../../../platform/actionWidget/browser/actionWidget.js';19import { IActionWidgetDropdownAction, IActionWidgetDropdownActionProvider } from '../../../../../platform/actionWidget/browser/actionWidgetDropdown.js';20import { ICommandService } from '../../../../../platform/commands/common/commands.js';21import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';22import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';23import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';24import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';25import { IOpenerService } from '../../../../../platform/opener/common/opener.js';26import { IWorkbenchContribution } from '../../../../common/contributions.js';27import { ResourceContextKey } from '../../../../common/contextkeys.js';28import { IEditorService } from '../../../../services/editor/common/editorService.js';29import { IChatAgentService } from '../../common/participants/chatAgents.js';30import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';31import { chatEditingWidgetFileStateContextKey, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js';32import { ChatModel } from '../../common/model/chatModel.js';33import { ChatRequestParser } from '../../common/requestParser/chatRequestParser.js';34import { ChatSendResult, IChatService } from '../../common/chatService/chatService.js';35import { IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js';36import { ChatAgentLocation } from '../../common/constants.js';37import { PROMPT_LANGUAGE_ID } from '../../common/promptSyntax/promptTypes.js';38import { AgentSessionProviders, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../agentSessions/agentSessions.js';39import { IChatWidget, IChatWidgetService } from '../chat.js';40import { ctxHasEditorModification } from '../chatEditing/chatEditingEditorContextKeys.js';41import { CHAT_SETUP_ACTION_ID } from './chatActions.js';42import { PromptFileVariableKind, toPromptFileVariableEntry } from '../../common/attachments/chatVariableEntries.js';4344export const enum ActionLocation {45ChatWidget = 'chatWidget',46Editor = 'editor'47}4849export class ContinueChatInSessionAction extends Action2 {5051static readonly ID = 'workbench.action.chat.continueChatInSession';5253constructor() {54super({55id: ContinueChatInSessionAction.ID,56title: localize2('continueChatInSession', "Continue Chat in..."),57tooltip: localize('continueChatInSession', "Continue Chat in..."),58precondition: ContextKeyExpr.and(59ChatContextKeys.enabled,60ChatContextKeys.requestInProgress.negate(),61ChatContextKeys.remoteJobCreating.negate(),62ChatContextKeys.hasCanDelegateProviders,63),64menu: [{65id: MenuId.ChatExecute,66group: 'navigation',67order: 3.4,68when: ContextKeyExpr.and(69ChatContextKeys.lockedToCodingAgent.negate(),70ChatContextKeys.hasCanDelegateProviders,71),72},73{74id: MenuId.EditorContent,75group: 'continueIn',76when: ContextKeyExpr.and(77ContextKeyExpr.equals(ResourceContextKey.Scheme.key, Schemas.untitled),78ContextKeyExpr.equals(ResourceContextKey.LangId.key, PROMPT_LANGUAGE_ID),79ContextKeyExpr.notEquals(chatEditingWidgetFileStateContextKey.key, ModifiedFileEntryState.Modified),80ctxHasEditorModification.negate(),81ChatContextKeys.hasCanDelegateProviders,82),83}84]85});86}8788override async run(): Promise<void> {89// Handled by a custom action item90}91}92export class ChatContinueInSessionActionItem extends ActionWidgetDropdownActionViewItem {93constructor(94action: MenuItemAction,95private readonly location: ActionLocation,96@IActionWidgetService actionWidgetService: IActionWidgetService,97@IContextKeyService private readonly contextKeyService: IContextKeyService,98@IKeybindingService keybindingService: IKeybindingService,99@IChatSessionsService chatSessionsService: IChatSessionsService,100@IInstantiationService instantiationService: IInstantiationService,101@IOpenerService openerService: IOpenerService,102@ITelemetryService telemetryService: ITelemetryService103) {104super(action, {105actionProvider: ChatContinueInSessionActionItem.actionProvider(chatSessionsService, instantiationService, location),106actionBarActions: ChatContinueInSessionActionItem.getActionBarActions(openerService),107reporter: { id: 'ChatContinueInSession', name: 'ChatContinueInSession', includeOptions: true },108}, actionWidgetService, keybindingService, contextKeyService, telemetryService);109}110111protected static getActionBarActions(openerService: IOpenerService) {112const learnMoreUrl = 'https://aka.ms/vscode-continue-chat-in';113return [{114id: 'workbench.action.chat.continueChatInSession.learnMore',115label: localize('chat.learnMore', "Learn More"),116tooltip: localize('chat.learnMore', "Learn More"),117class: undefined,118enabled: true,119run: async () => {120await openerService.open(URI.parse(learnMoreUrl));121}122}];123}124125private static actionProvider(chatSessionsService: IChatSessionsService, instantiationService: IInstantiationService, location: ActionLocation): IActionWidgetDropdownActionProvider {126return {127getActions: () => {128const actions: IActionWidgetDropdownAction[] = [];129const contributions = chatSessionsService.getAllChatSessionContributions();130131// Continue in Background132const backgroundContrib = contributions.find(contrib => contrib.type === AgentSessionProviders.Background);133if (backgroundContrib && backgroundContrib.canDelegate) {134actions.push(this.toAction(AgentSessionProviders.Background, backgroundContrib, instantiationService, location));135}136137// Continue in Cloud138const cloudContrib = contributions.find(contrib => contrib.type === AgentSessionProviders.Cloud);139if (cloudContrib && cloudContrib.canDelegate) {140actions.push(this.toAction(AgentSessionProviders.Cloud, cloudContrib, instantiationService, location));141}142143// Offer actions to enter setup if we have no contributions144if (actions.length === 0) {145actions.push(this.toSetupAction(AgentSessionProviders.Background, instantiationService));146actions.push(this.toSetupAction(AgentSessionProviders.Cloud, instantiationService));147}148149return actions;150}151};152}153154private static toAction(provider: AgentSessionProviders, contrib: IChatSessionsExtensionPoint, instantiationService: IInstantiationService, location: ActionLocation): IActionWidgetDropdownAction {155return {156id: contrib.type,157enabled: true,158icon: getAgentSessionProviderIcon(provider),159class: undefined,160description: `@${contrib.name}`,161label: getAgentSessionProviderName(provider),162tooltip: localize('continueSessionIn', "Continue in {0}", getAgentSessionProviderName(provider)),163category: { label: localize('continueIn', "Continue In"), order: 0, showHeader: true },164run: () => instantiationService.invokeFunction(accessor => {165if (location === ActionLocation.Editor) {166return new CreateRemoteAgentJobFromEditorAction().run(accessor, contrib);167}168return new CreateRemoteAgentJobAction().run(accessor, contrib);169})170};171}172173private static toSetupAction(provider: AgentSessionProviders, instantiationService: IInstantiationService): IActionWidgetDropdownAction {174return {175id: provider,176enabled: true,177icon: getAgentSessionProviderIcon(provider),178class: undefined,179label: getAgentSessionProviderName(provider),180tooltip: localize('continueSessionIn', "Continue in {0}", getAgentSessionProviderName(provider)),181category: { label: localize('continueIn', "Continue In"), order: 0, showHeader: true },182run: () => instantiationService.invokeFunction(accessor => {183const commandService = accessor.get(ICommandService);184return commandService.executeCommand(CHAT_SETUP_ACTION_ID);185})186};187}188189protected override renderLabel(element: HTMLElement): IDisposable | null {190if (this.location === ActionLocation.Editor) {191const view = h('span.action-widget-delegate-label', [192h('span', { className: ThemeIcon.asClassName(Codicon.forward) }),193h('span', [localize('continueInEllipsis', "Continue in...")])194]);195element.appendChild(view.root);196return null;197} else {198const icon = this.contextKeyService.contextMatchesRules(ChatContextKeys.remoteJobCreating) ? Codicon.sync : Codicon.forward;199element.classList.add(...ThemeIcon.asClassNameArray(icon));200return super.renderLabel(element);201}202}203}204205const NEW_CHAT_SESSION_ACTION_ID = 'workbench.action.chat.openNewSessionEditor';206207export class CreateRemoteAgentJobAction {208constructor() { }209210private openUntitledEditor(commandService: ICommandService, continuationTarget: IChatSessionsExtensionPoint) {211commandService.executeCommand(`${NEW_CHAT_SESSION_ACTION_ID}.${continuationTarget.type}`);212}213214async run(accessor: ServicesAccessor, continuationTarget: IChatSessionsExtensionPoint, _widget?: IChatWidget) {215const contextKeyService = accessor.get(IContextKeyService);216const commandService = accessor.get(ICommandService);217const widgetService = accessor.get(IChatWidgetService);218const chatAgentService = accessor.get(IChatAgentService);219const chatService = accessor.get(IChatService);220const editorService = accessor.get(IEditorService);221222const remoteJobCreatingKey = ChatContextKeys.remoteJobCreating.bindTo(contextKeyService);223224try {225remoteJobCreatingKey.set(true);226227const widget = _widget ?? widgetService.lastFocusedWidget;228if (!widget || !widget.viewModel) {229return this.openUntitledEditor(commandService, continuationTarget);230}231232// todo@connor4312: remove 'as' cast233const chatModel = widget.viewModel.model as ChatModel;234if (!chatModel) {235return;236}237238const sessionResource = widget.viewModel.sessionResource;239const chatRequests = chatModel.getRequests();240let userPrompt = widget.getInput();241if (!userPrompt) {242if (!chatRequests.length) {243return this.openUntitledEditor(commandService, continuationTarget);244}245userPrompt = 'implement this.';246}247248const attachedContext = widget.input.getAttachedAndImplicitContext(sessionResource);249widget.input.acceptInput(true);250251// For inline editor mode, add selection or cursor information252if (widget.location === ChatAgentLocation.EditorInline) {253const activeEditor = editorService.activeTextEditorControl;254if (activeEditor) {255const model = activeEditor.getModel();256let activeEditorUri: URI | undefined = undefined;257if (model && isITextModel(model)) {258activeEditorUri = model.uri as URI;259}260const selection = activeEditor.getSelection();261if (activeEditorUri && selection) {262attachedContext.add({263kind: 'file',264id: 'vscode.implicit.selection',265name: basename(activeEditorUri),266value: {267uri: activeEditorUri,268range: selection269},270});271}272}273}274275const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Chat);276const instantiationService = accessor.get(IInstantiationService);277const requestParser = instantiationService.createInstance(ChatRequestParser);278const continuationTargetType = continuationTarget.type;279280// Add the request to the model first281const parsedRequest = requestParser.parseChatRequest(sessionResource, userPrompt, ChatAgentLocation.Chat);282const addedRequest = chatModel.addRequest(283parsedRequest,284{ variables: attachedContext.asArray() },2850,286undefined,287defaultAgent288);289290await chatService.removeRequest(sessionResource, addedRequest.id);291const sendResult = await chatService.sendRequest(sessionResource, userPrompt, {292agentIdSilent: continuationTargetType,293attachedContext: attachedContext.asArray(),294userSelectedModelId: widget.input.currentLanguageModel,295...widget.getModeRequestOptions()296});297298if (ChatSendResult.isSent(sendResult)) {299await widget.handleDelegationExitIfNeeded(defaultAgent, sendResult.data.agent);300}301} catch (e) {302console.error('Error creating remote coding agent job', e);303throw e;304} finally {305remoteJobCreatingKey.set(false);306}307}308}309310class CreateRemoteAgentJobFromEditorAction {311constructor() { }312313async run(accessor: ServicesAccessor, continuationTarget: IChatSessionsExtensionPoint) {314315try {316const editorService = accessor.get(IEditorService);317const activeEditor = editorService.activeTextEditorControl;318const commandService = accessor.get(ICommandService);319320if (!activeEditor) {321return;322}323const model = activeEditor.getModel();324if (!model || !isITextModel(model)) {325return;326}327const uri = model.uri;328const attachedContext = [toPromptFileVariableEntry(uri, PromptFileVariableKind.PromptFile, undefined, false, [])];329const prompt = `Follow instructions in [${basename(uri)}](${uri.toString()}).`;330await commandService.executeCommand(`${NEW_CHAT_SESSION_ACTION_ID}.${continuationTarget.type}`, { prompt, attachedContext });331} catch (e) {332console.error('Error creating remote agent job from editor', e);333throw e;334}335}336}337338export class ContinueChatInSessionActionRendering extends Disposable implements IWorkbenchContribution {339340static readonly ID = 'chat.continueChatInSessionActionRendering';341342constructor(343@IActionViewItemService actionViewItemService: IActionViewItemService,344@IInstantiationService instantiationService: IInstantiationService,345) {346super();347const disposable = actionViewItemService.register(MenuId.EditorContent, ContinueChatInSessionAction.ID, (action, options, instantiationService2) => {348if (!(action instanceof MenuItemAction)) {349return undefined;350}351return instantiationService.createInstance(ChatContinueInSessionActionItem, action, ActionLocation.Editor);352});353markAsSingleton(disposable);354}355}356357358