Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatContinueInAction.ts
4780 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 { IOpenerService } from '../../../../../platform/opener/common/opener.js';25import { IWorkbenchContribution } from '../../../../common/contributions.js';26import { ResourceContextKey } from '../../../../common/contextkeys.js';27import { IEditorService } from '../../../../services/editor/common/editorService.js';28import { IChatAgentService } from '../../common/participants/chatAgents.js';29import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';30import { chatEditingWidgetFileStateContextKey, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js';31import { ChatModel } from '../../common/model/chatModel.js';32import { ChatRequestParser } from '../../common/requestParser/chatRequestParser.js';33import { IChatService } from '../../common/chatService/chatService.js';34import { IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js';35import { ChatAgentLocation } from '../../common/constants.js';36import { PROMPT_LANGUAGE_ID } from '../../common/promptSyntax/promptTypes.js';37import { AgentSessionProviders, getAgentSessionProviderIcon, getAgentSessionProviderName } from '../agentSessions/agentSessions.js';38import { IChatWidgetService } from '../chat.js';39import { ctxHasEditorModification } from '../chatEditing/chatEditingEditorContextKeys.js';40import { CHAT_SETUP_ACTION_ID } from './chatActions.js';41import { PromptFileVariableKind, toPromptFileVariableEntry } from '../../common/attachments/chatVariableEntries.js';4243export const enum ActionLocation {44ChatWidget = 'chatWidget',45Editor = 'editor'46}4748export class ContinueChatInSessionAction extends Action2 {4950static readonly ID = 'workbench.action.chat.continueChatInSession';5152constructor() {53super({54id: ContinueChatInSessionAction.ID,55title: localize2('continueChatInSession', "Continue Chat in..."),56tooltip: localize('continueChatInSession', "Continue Chat in..."),57precondition: ContextKeyExpr.and(58ChatContextKeys.enabled,59ChatContextKeys.requestInProgress.negate(),60ChatContextKeys.remoteJobCreating.negate(),61ChatContextKeys.hasCanDelegateProviders,62),63menu: [{64id: MenuId.ChatExecute,65group: 'navigation',66order: 3.4,67when: ContextKeyExpr.and(68ChatContextKeys.lockedToCodingAgent.negate(),69ChatContextKeys.hasCanDelegateProviders,70),71},72{73id: MenuId.EditorContent,74group: 'continueIn',75when: ContextKeyExpr.and(76ContextKeyExpr.equals(ResourceContextKey.Scheme.key, Schemas.untitled),77ContextKeyExpr.equals(ResourceContextKey.LangId.key, PROMPT_LANGUAGE_ID),78ContextKeyExpr.notEquals(chatEditingWidgetFileStateContextKey.key, ModifiedFileEntryState.Modified),79ctxHasEditorModification.negate(),80ChatContextKeys.hasCanDelegateProviders,81),82}83]84});85}8687override async run(): Promise<void> {88// Handled by a custom action item89}90}91export class ChatContinueInSessionActionItem extends ActionWidgetDropdownActionViewItem {92constructor(93action: MenuItemAction,94private readonly location: ActionLocation,95@IActionWidgetService actionWidgetService: IActionWidgetService,96@IContextKeyService private readonly contextKeyService: IContextKeyService,97@IKeybindingService keybindingService: IKeybindingService,98@IChatSessionsService chatSessionsService: IChatSessionsService,99@IInstantiationService instantiationService: IInstantiationService,100@IOpenerService openerService: IOpenerService101) {102super(action, {103actionProvider: ChatContinueInSessionActionItem.actionProvider(chatSessionsService, instantiationService, location),104actionBarActions: ChatContinueInSessionActionItem.getActionBarActions(openerService)105}, actionWidgetService, keybindingService, contextKeyService);106}107108protected static getActionBarActions(openerService: IOpenerService) {109const learnMoreUrl = 'https://aka.ms/vscode-continue-chat-in';110return [{111id: 'workbench.action.chat.continueChatInSession.learnMore',112label: localize('chat.learnMore', "Learn More"),113tooltip: localize('chat.learnMore', "Learn More"),114class: undefined,115enabled: true,116run: async () => {117await openerService.open(URI.parse(learnMoreUrl));118}119}];120}121122private static actionProvider(chatSessionsService: IChatSessionsService, instantiationService: IInstantiationService, location: ActionLocation): IActionWidgetDropdownActionProvider {123return {124getActions: () => {125const actions: IActionWidgetDropdownAction[] = [];126const contributions = chatSessionsService.getAllChatSessionContributions();127128// Continue in Background129const backgroundContrib = contributions.find(contrib => contrib.type === AgentSessionProviders.Background);130if (backgroundContrib && backgroundContrib.canDelegate !== false) {131actions.push(this.toAction(AgentSessionProviders.Background, backgroundContrib, instantiationService, location));132}133134// Continue in Cloud135const cloudContrib = contributions.find(contrib => contrib.type === AgentSessionProviders.Cloud);136if (cloudContrib && cloudContrib.canDelegate !== false) {137actions.push(this.toAction(AgentSessionProviders.Cloud, cloudContrib, instantiationService, location));138}139140// Offer actions to enter setup if we have no contributions141if (actions.length === 0) {142actions.push(this.toSetupAction(AgentSessionProviders.Background, instantiationService));143actions.push(this.toSetupAction(AgentSessionProviders.Cloud, instantiationService));144}145146return actions;147}148};149}150151private static toAction(provider: AgentSessionProviders, contrib: IChatSessionsExtensionPoint, instantiationService: IInstantiationService, location: ActionLocation): IActionWidgetDropdownAction {152return {153id: contrib.type,154enabled: true,155icon: getAgentSessionProviderIcon(provider),156class: undefined,157description: `@${contrib.name}`,158label: getAgentSessionProviderName(provider),159tooltip: localize('continueSessionIn', "Continue in {0}", getAgentSessionProviderName(provider)),160category: { label: localize('continueIn', "Continue In"), order: 0, showHeader: true },161run: () => instantiationService.invokeFunction(accessor => {162if (location === ActionLocation.Editor) {163return new CreateRemoteAgentJobFromEditorAction().run(accessor, contrib);164}165return new CreateRemoteAgentJobAction().run(accessor, contrib);166})167};168}169170private static toSetupAction(provider: AgentSessionProviders, instantiationService: IInstantiationService): IActionWidgetDropdownAction {171return {172id: provider,173enabled: true,174icon: getAgentSessionProviderIcon(provider),175class: undefined,176label: getAgentSessionProviderName(provider),177tooltip: localize('continueSessionIn', "Continue in {0}", getAgentSessionProviderName(provider)),178category: { label: localize('continueIn', "Continue In"), order: 0, showHeader: true },179run: () => instantiationService.invokeFunction(accessor => {180const commandService = accessor.get(ICommandService);181return commandService.executeCommand(CHAT_SETUP_ACTION_ID);182})183};184}185186protected override renderLabel(element: HTMLElement): IDisposable | null {187if (this.location === ActionLocation.Editor) {188const view = h('span.action-widget-delegate-label', [189h('span', { className: ThemeIcon.asClassName(Codicon.forward) }),190h('span', [localize('continueInEllipsis', "Continue in...")])191]);192element.appendChild(view.root);193return null;194} else {195const icon = this.contextKeyService.contextMatchesRules(ChatContextKeys.remoteJobCreating) ? Codicon.sync : Codicon.forward;196element.classList.add(...ThemeIcon.asClassNameArray(icon));197return super.renderLabel(element);198}199}200}201202const NEW_CHAT_SESSION_ACTION_ID = 'workbench.action.chat.openNewSessionEditor';203204class CreateRemoteAgentJobAction {205constructor() { }206207private openUntitledEditor(commandService: ICommandService, continuationTarget: IChatSessionsExtensionPoint) {208commandService.executeCommand(`${NEW_CHAT_SESSION_ACTION_ID}.${continuationTarget.type}`);209}210211async run(accessor: ServicesAccessor, continuationTarget: IChatSessionsExtensionPoint) {212const contextKeyService = accessor.get(IContextKeyService);213const commandService = accessor.get(ICommandService);214const widgetService = accessor.get(IChatWidgetService);215const chatAgentService = accessor.get(IChatAgentService);216const chatService = accessor.get(IChatService);217const editorService = accessor.get(IEditorService);218219const remoteJobCreatingKey = ChatContextKeys.remoteJobCreating.bindTo(contextKeyService);220221try {222remoteJobCreatingKey.set(true);223224const widget = widgetService.lastFocusedWidget;225if (!widget || !widget.viewModel) {226return this.openUntitledEditor(commandService, continuationTarget);227}228229// todo@connor4312: remove 'as' cast230const chatModel = widget.viewModel.model as ChatModel;231if (!chatModel) {232return;233}234235const sessionResource = widget.viewModel.sessionResource;236const chatRequests = chatModel.getRequests();237let userPrompt = widget.getInput();238if (!userPrompt) {239if (!chatRequests.length) {240return this.openUntitledEditor(commandService, continuationTarget);241}242userPrompt = 'implement this.';243}244245const attachedContext = widget.input.getAttachedAndImplicitContext(sessionResource);246widget.input.acceptInput(true);247248// For inline editor mode, add selection or cursor information249if (widget.location === ChatAgentLocation.EditorInline) {250const activeEditor = editorService.activeTextEditorControl;251if (activeEditor) {252const model = activeEditor.getModel();253let activeEditorUri: URI | undefined = undefined;254if (model && isITextModel(model)) {255activeEditorUri = model.uri as URI;256}257const selection = activeEditor.getSelection();258if (activeEditorUri && selection) {259attachedContext.add({260kind: 'file',261id: 'vscode.implicit.selection',262name: basename(activeEditorUri),263value: {264uri: activeEditorUri,265range: selection266},267});268}269}270}271272const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Chat);273const instantiationService = accessor.get(IInstantiationService);274const requestParser = instantiationService.createInstance(ChatRequestParser);275const continuationTargetType = continuationTarget.type;276277// Add the request to the model first278const parsedRequest = requestParser.parseChatRequest(sessionResource, userPrompt, ChatAgentLocation.Chat);279const addedRequest = chatModel.addRequest(280parsedRequest,281{ variables: attachedContext.asArray() },2820,283undefined,284defaultAgent285);286287await chatService.removeRequest(sessionResource, addedRequest.id);288const requestData = await chatService.sendRequest(sessionResource, userPrompt, {289agentIdSilent: continuationTargetType,290attachedContext: attachedContext.asArray(),291userSelectedModelId: widget.input.currentLanguageModel,292...widget.getModeRequestOptions()293});294295if (requestData) {296await widget.handleDelegationExitIfNeeded(defaultAgent, requestData.agent);297}298} catch (e) {299console.error('Error creating remote coding agent job', e);300throw e;301} finally {302remoteJobCreatingKey.set(false);303}304}305}306307class CreateRemoteAgentJobFromEditorAction {308constructor() { }309310async run(accessor: ServicesAccessor, continuationTarget: IChatSessionsExtensionPoint) {311312try {313const editorService = accessor.get(IEditorService);314const activeEditor = editorService.activeTextEditorControl;315const commandService = accessor.get(ICommandService);316317if (!activeEditor) {318return;319}320const model = activeEditor.getModel();321if (!model || !isITextModel(model)) {322return;323}324const uri = model.uri;325const attachedContext = [toPromptFileVariableEntry(uri, PromptFileVariableKind.PromptFile, undefined, false, [])];326const prompt = `Follow instructions in [${basename(uri)}](${uri.toString()}).`;327await commandService.executeCommand(`${NEW_CHAT_SESSION_ACTION_ID}.${continuationTarget.type}`, { prompt, attachedContext });328} catch (e) {329console.error('Error creating remote agent job from editor', e);330throw e;331}332}333}334335export class ContinueChatInSessionActionRendering extends Disposable implements IWorkbenchContribution {336337static readonly ID = 'chat.continueChatInSessionActionRendering';338339constructor(340@IActionViewItemService actionViewItemService: IActionViewItemService,341@IInstantiationService instantiationService: IInstantiationService,342) {343super();344const disposable = actionViewItemService.register(MenuId.EditorContent, ContinueChatInSessionAction.ID, (action, options, instantiationService2) => {345if (!(action instanceof MenuItemAction)) {346return undefined;347}348return instantiationService.createInstance(ChatContinueInSessionActionItem, action, ActionLocation.Editor);349});350markAsSingleton(disposable);351}352}353354355