Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts
3296 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 { CancellationToken } from '../../../../../base/common/cancellation.js';6import { Codicon } from '../../../../../base/common/codicons.js';7import { MarkdownString } from '../../../../../base/common/htmlContent.js';8import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';9import { basename, relativePath } from '../../../../../base/common/resources.js';10import { ThemeIcon } from '../../../../../base/common/themables.js';11import { assertType } from '../../../../../base/common/types.js';12import { URI } from '../../../../../base/common/uri.js';13import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';14import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js';15import { localize, localize2 } from '../../../../../nls.js';16import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';17import { ICommandService } from '../../../../../platform/commands/common/commands.js';18import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';19import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';20import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';21import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';22import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';23import { IQuickInputService } from '../../../../../platform/quickinput/common/quickInput.js';24import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';25import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';26import { IEditorService } from '../../../../services/editor/common/editorService.js';27import { IRemoteCodingAgent, IRemoteCodingAgentsService } from '../../../remoteCodingAgents/common/remoteCodingAgentsService.js';28import { IChatAgentHistoryEntry, IChatAgentService } from '../../common/chatAgents.js';29import { ChatContextKeys } from '../../common/chatContextKeys.js';30import { IChatModel, IChatRequestModel, toChatHistoryContent } from '../../common/chatModel.js';31import { IChatMode, IChatModeService } from '../../common/chatModes.js';32import { chatVariableLeader } from '../../common/chatParserTypes.js';33import { ChatRequestParser } from '../../common/chatRequestParser.js';34import { IChatPullRequestContent, IChatService } from '../../common/chatService.js';35import { IChatSessionsExtensionPoint, IChatSessionsService } from '../../common/chatSessionsService.js';36import { ChatSessionUri } from '../../common/chatUri.js';37import { ChatRequestVariableSet, isChatRequestFileEntry } from '../../common/chatVariableEntries.js';38import { ChatAgentLocation, ChatConfiguration, ChatModeKind, } from '../../common/constants.js';39import { ILanguageModelChatMetadata } from '../../common/languageModels.js';40import { ILanguageModelToolsService } from '../../common/languageModelToolsService.js';41import { IChatWidget, IChatWidgetService } from '../chat.js';42import { getEditingSessionContext } from '../chatEditing/chatEditingActions.js';43import { IChatEditorOptions } from '../chatEditor.js';44import { ACTION_ID_NEW_CHAT, CHAT_CATEGORY, handleCurrentEditingSession, handleModeSwitch } from './chatActions.js';4546export interface IVoiceChatExecuteActionContext {47readonly disableTimeout?: boolean;48}4950export interface IChatExecuteActionContext {51widget?: IChatWidget;52inputValue?: string;53voice?: IVoiceChatExecuteActionContext;54}5556abstract class SubmitAction extends Action2 {57async run(accessor: ServicesAccessor, ...args: any[]) {58const context: IChatExecuteActionContext | undefined = args[0];59const telemetryService = accessor.get(ITelemetryService);60const widgetService = accessor.get(IChatWidgetService);61const widget = context?.widget ?? widgetService.lastFocusedWidget;62if (widget?.viewModel?.editing) {63const configurationService = accessor.get(IConfigurationService);64const dialogService = accessor.get(IDialogService);65const chatService = accessor.get(IChatService);66const chatModel = chatService.getSession(widget.viewModel.sessionId);67if (!chatModel) {68return;69}7071const session = chatModel.editingSession;72if (!session) {73return;74}7576const requestId = widget.viewModel?.editing.id;7778if (requestId) {79const chatRequests = chatModel.getRequests();80const itemIndex = chatRequests.findIndex(request => request.id === requestId);81const editsToUndo = chatRequests.length - itemIndex;8283const requestsToRemove = chatRequests.slice(itemIndex);84const requestIdsToRemove = new Set(requestsToRemove.map(request => request.id));85const entriesModifiedInRequestsToRemove = session.entries.get().filter((entry) => requestIdsToRemove.has(entry.lastModifyingRequestId)) ?? [];86const shouldPrompt = entriesModifiedInRequestsToRemove.length > 0 && configurationService.getValue('chat.editing.confirmEditRequestRemoval') === true;8788let message: string;89if (editsToUndo === 1) {90if (entriesModifiedInRequestsToRemove.length === 1) {91message = localize('chat.removeLast.confirmation.message2', "This will remove your last request and undo the edits made to {0}. Do you want to proceed?", basename(entriesModifiedInRequestsToRemove[0].modifiedURI));92} else {93message = localize('chat.removeLast.confirmation.multipleEdits.message', "This will remove your last request and undo edits made to {0} files in your working set. Do you want to proceed?", entriesModifiedInRequestsToRemove.length);94}95} else {96if (entriesModifiedInRequestsToRemove.length === 1) {97message = localize('chat.remove.confirmation.message2', "This will remove all subsequent requests and undo edits made to {0}. Do you want to proceed?", basename(entriesModifiedInRequestsToRemove[0].modifiedURI));98} else {99message = localize('chat.remove.confirmation.multipleEdits.message', "This will remove all subsequent requests and undo edits made to {0} files in your working set. Do you want to proceed?", entriesModifiedInRequestsToRemove.length);100}101}102103const confirmation = shouldPrompt104? await dialogService.confirm({105title: editsToUndo === 1106? localize('chat.removeLast.confirmation.title', "Do you want to undo your last edit?")107: localize('chat.remove.confirmation.title', "Do you want to undo {0} edits?", editsToUndo),108message: message,109primaryButton: localize('chat.remove.confirmation.primaryButton', "Yes"),110checkbox: { label: localize('chat.remove.confirmation.checkbox', "Don't ask again"), checked: false },111type: 'info'112})113: { confirmed: true };114115type EditUndoEvent = {116editRequestType: string;117outcome: 'cancelled' | 'applied';118editsUndoCount: number;119};120121type EditUndoEventClassification = {122owner: 'justschen';123comment: 'Event used to gain insights into when there are pending changes to undo, and whether edited requests are applied or cancelled.';124editRequestType: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Current entry point for editing a request.' };125outcome: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the edit was cancelled or applied.' };126editsUndoCount: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of edits that would be undone.'; 'isMeasurement': true };127};128129if (!confirmation.confirmed) {130telemetryService.publicLog2<EditUndoEvent, EditUndoEventClassification>('chat.undoEditsConfirmation', {131editRequestType: configurationService.getValue<string>('chat.editRequests'),132outcome: 'cancelled',133editsUndoCount: editsToUndo134});135return;136} else if (editsToUndo > 0) {137telemetryService.publicLog2<EditUndoEvent, EditUndoEventClassification>('chat.undoEditsConfirmation', {138editRequestType: configurationService.getValue<string>('chat.editRequests'),139outcome: 'applied',140editsUndoCount: editsToUndo141});142}143144if (confirmation.checkboxChecked) {145await configurationService.updateValue('chat.editing.confirmEditRequestRemoval', false);146}147148// Restore the snapshot to what it was before the request(s) that we deleted149const snapshotRequestId = chatRequests[itemIndex].id;150await session.restoreSnapshot(snapshotRequestId, undefined);151}152} else if (widget?.viewModel?.model.checkpoint) {153widget.viewModel.model.setCheckpoint(undefined);154}155widget?.acceptInput(context?.inputValue);156}157}158159const whenNotInProgress = ChatContextKeys.requestInProgress.negate();160161export class ChatSubmitAction extends SubmitAction {162static readonly ID = 'workbench.action.chat.submit';163164constructor() {165const menuCondition = ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask);166167super({168id: ChatSubmitAction.ID,169title: localize2('interactive.submit.label', "Send and Dispatch"),170f1: false,171category: CHAT_CATEGORY,172icon: Codicon.send,173toggled: {174condition: ChatContextKeys.lockedToCodingAgent,175icon: Codicon.sendToRemoteAgent,176tooltip: localize('sendToRemoteAgent', "Send to coding agent"),177},178keybinding: {179when: ChatContextKeys.inChatInput,180primary: KeyCode.Enter,181weight: KeybindingWeight.EditorContrib182},183menu: [184{185id: MenuId.ChatExecuteSecondary,186group: 'group_1',187order: 1,188when: ContextKeyExpr.and(menuCondition, ChatContextKeys.lockedToCodingAgent.negate()),189},190{191id: MenuId.ChatExecute,192order: 4,193when: ContextKeyExpr.and(194whenNotInProgress,195menuCondition,196),197group: 'navigation',198}]199});200}201}202203export const ToggleAgentModeActionId = 'workbench.action.chat.toggleAgentMode';204205export interface IToggleChatModeArgs {206modeId: ChatModeKind | string;207}208209type ChatModeChangeClassification = {210owner: 'digitarald';211comment: 'Reporting when Chat mode is switched between different modes';212fromMode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The previous chat mode' };213toMode?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'The new chat mode' };214requestCount?: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Number of requests in the current chat session'; 'isMeasurement': true };215};216217type ChatModeChangeEvent = {218fromMode: string;219toMode: string;220requestCount: number;221};222223class ToggleChatModeAction extends Action2 {224225static readonly ID = ToggleAgentModeActionId;226227constructor() {228super({229id: ToggleChatModeAction.ID,230title: localize2('interactive.toggleAgent.label', "Switch to Next Chat Mode"),231f1: true,232category: CHAT_CATEGORY,233precondition: ContextKeyExpr.and(234ChatContextKeys.enabled,235ChatContextKeys.requestInProgress.negate())236});237}238239async run(accessor: ServicesAccessor, ...args: any[]) {240const commandService = accessor.get(ICommandService);241const configurationService = accessor.get(IConfigurationService);242const instaService = accessor.get(IInstantiationService);243const modeService = accessor.get(IChatModeService);244const telemetryService = accessor.get(ITelemetryService);245246const context = getEditingSessionContext(accessor, args);247if (!context?.chatWidget) {248return;249}250251const arg = args.at(0) as IToggleChatModeArgs | undefined;252const chatSession = context.chatWidget.viewModel?.model;253const requestCount = chatSession?.getRequests().length ?? 0;254const switchToMode = (arg && modeService.findModeById(arg.modeId)) ?? this.getNextMode(context.chatWidget, requestCount, configurationService, modeService);255256const currentMode = context.chatWidget.input.currentModeObs.get();257if (switchToMode.id === currentMode.id) {258return;259}260261const chatModeCheck = await instaService.invokeFunction(handleModeSwitch, context.chatWidget.input.currentModeKind, switchToMode.kind, requestCount, context.editingSession);262if (!chatModeCheck) {263return;264}265266// Send telemetry for mode change267telemetryService.publicLog2<ChatModeChangeEvent, ChatModeChangeClassification>('chat.modeChange', {268fromMode: currentMode.id,269toMode: switchToMode.id,270requestCount: requestCount271});272273context.chatWidget.input.setChatMode(switchToMode.id);274275if (chatModeCheck.needToClearSession) {276await commandService.executeCommand(ACTION_ID_NEW_CHAT);277}278}279280private getNextMode(chatWidget: IChatWidget, requestCount: number, configurationService: IConfigurationService, modeService: IChatModeService): IChatMode {281const modes = modeService.getModes();282const flat = [283...modes.builtin.filter(mode => {284return mode.kind !== ChatModeKind.Edit || configurationService.getValue(ChatConfiguration.Edits2Enabled) || requestCount === 0;285}),286...(modes.custom ?? []),287];288289const curModeIndex = flat.findIndex(mode => mode.id === chatWidget.input.currentModeObs.get().id);290const newMode = flat[(curModeIndex + 1) % flat.length];291return newMode;292}293}294295class SwitchToNextModelAction extends Action2 {296static readonly ID = 'workbench.action.chat.switchToNextModel';297298constructor() {299super({300id: SwitchToNextModelAction.ID,301title: localize2('interactive.switchToNextModel.label', "Switch to Next Model"),302category: CHAT_CATEGORY,303f1: true,304precondition: ChatContextKeys.enabled,305});306}307308override run(accessor: ServicesAccessor, ...args: any[]): void {309const widgetService = accessor.get(IChatWidgetService);310const widget = widgetService.lastFocusedWidget;311widget?.input.switchToNextModel();312}313}314315export const ChatOpenModelPickerActionId = 'workbench.action.chat.openModelPicker';316class OpenModelPickerAction extends Action2 {317static readonly ID = ChatOpenModelPickerActionId;318319constructor() {320super({321id: OpenModelPickerAction.ID,322title: localize2('interactive.openModelPicker.label', "Open Model Picker"),323category: CHAT_CATEGORY,324f1: false,325keybinding: {326primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.Period,327weight: KeybindingWeight.WorkbenchContrib,328when: ChatContextKeys.inChatInput329},330precondition: ChatContextKeys.enabled,331menu: {332id: MenuId.ChatInput,333order: 3,334group: 'navigation',335when:336ContextKeyExpr.and(337ChatContextKeys.lockedToCodingAgent.negate(),338ChatContextKeys.languageModelsAreUserSelectable,339ContextKeyExpr.or(340ContextKeyExpr.equals(ChatContextKeys.location.key, ChatAgentLocation.Panel),341ContextKeyExpr.equals(ChatContextKeys.location.key, ChatAgentLocation.Editor),342ContextKeyExpr.equals(ChatContextKeys.location.key, ChatAgentLocation.Notebook),343ContextKeyExpr.equals(ChatContextKeys.location.key, ChatAgentLocation.Terminal))344)345}346});347}348349override async run(accessor: ServicesAccessor, ...args: any[]): Promise<void> {350const widgetService = accessor.get(IChatWidgetService);351const widget = widgetService.lastFocusedWidget;352if (widget) {353widget.input.openModelPicker();354}355}356}357358export class OpenModePickerAction extends Action2 {359static readonly ID = 'workbench.action.chat.openModePicker';360361constructor() {362super({363id: OpenModePickerAction.ID,364title: localize2('interactive.openModePicker.label', "Open Mode Picker"),365tooltip: localize('setChatMode', "Set Mode"),366category: CHAT_CATEGORY,367f1: false,368precondition: ChatContextKeys.enabled,369keybinding: {370when: ContextKeyExpr.and(371ChatContextKeys.inChatInput,372ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel)),373primary: KeyMod.CtrlCmd | KeyCode.Period,374weight: KeybindingWeight.EditorContrib375},376menu: [377{378id: MenuId.ChatInput,379order: 1,380when: ContextKeyExpr.and(381ChatContextKeys.enabled,382ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel),383ChatContextKeys.inQuickChat.negate(),384ChatContextKeys.lockedToCodingAgent.negate()),385group: 'navigation',386},387]388});389}390391override async run(accessor: ServicesAccessor, ...args: any[]): Promise<void> {392const widgetService = accessor.get(IChatWidgetService);393const widget = widgetService.lastFocusedWidget;394if (widget) {395widget.input.openModePicker();396}397}398}399400export const ChangeChatModelActionId = 'workbench.action.chat.changeModel';401class ChangeChatModelAction extends Action2 {402static readonly ID = ChangeChatModelActionId;403404constructor() {405super({406id: ChangeChatModelAction.ID,407title: localize2('interactive.changeModel.label', "Change Model"),408category: CHAT_CATEGORY,409f1: false,410precondition: ChatContextKeys.enabled,411});412}413414override run(accessor: ServicesAccessor, ...args: any[]): void {415const modelInfo: Pick<ILanguageModelChatMetadata, 'vendor' | 'id' | 'family'> = args[0];416// Type check the arg417assertType(typeof modelInfo.vendor === 'string' && typeof modelInfo.id === 'string' && typeof modelInfo.family === 'string');418const widgetService = accessor.get(IChatWidgetService);419const widgets = widgetService.getAllWidgets();420for (const widget of widgets) {421widget.input.switchModel(modelInfo);422}423}424}425426export class ChatEditingSessionSubmitAction extends SubmitAction {427static readonly ID = 'workbench.action.edits.submit';428429constructor() {430const menuCondition = ChatContextKeys.chatModeKind.notEqualsTo(ChatModeKind.Ask);431432super({433id: ChatEditingSessionSubmitAction.ID,434title: localize2('edits.submit.label', "Send"),435f1: false,436category: CHAT_CATEGORY,437icon: Codicon.send,438menu: [439{440id: MenuId.ChatExecuteSecondary,441group: 'group_1',442when: ContextKeyExpr.and(whenNotInProgress, menuCondition),443order: 1444},445{446id: MenuId.ChatExecute,447order: 4,448when: ContextKeyExpr.and(449ChatContextKeys.requestInProgress.negate(),450menuCondition),451group: 'navigation',452}]453});454}455}456457class SubmitWithoutDispatchingAction extends Action2 {458static readonly ID = 'workbench.action.chat.submitWithoutDispatching';459460constructor() {461const precondition = ContextKeyExpr.and(462// if the input has prompt instructions attached, allow submitting requests even463// without text present - having instructions is enough context for a request464ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.hasPromptFile),465whenNotInProgress,466ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask),467);468469super({470id: SubmitWithoutDispatchingAction.ID,471title: localize2('interactive.submitWithoutDispatch.label', "Send"),472f1: false,473category: CHAT_CATEGORY,474precondition,475keybinding: {476when: ChatContextKeys.inChatInput,477primary: KeyMod.Alt | KeyMod.Shift | KeyCode.Enter,478weight: KeybindingWeight.EditorContrib479},480menu: [481{482id: MenuId.ChatExecuteSecondary,483group: 'group_1',484order: 2,485when: ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask),486}487]488});489}490491run(accessor: ServicesAccessor, ...args: any[]) {492const context: IChatExecuteActionContext | undefined = args[0];493494const widgetService = accessor.get(IChatWidgetService);495const widget = context?.widget ?? widgetService.lastFocusedWidget;496widget?.acceptInput(context?.inputValue, { noCommandDetection: true });497}498}499export class CreateRemoteAgentJobAction extends Action2 {500static readonly ID = 'workbench.action.chat.createRemoteAgentJob';501502static readonly markdownStringTrustedOptions = {503isTrusted: {504enabledCommands: [] as string[],505},506};507508constructor() {509const precondition = ContextKeyExpr.and(510whenNotInProgress,511ChatContextKeys.remoteJobCreating.negate(),512);513514super({515id: CreateRemoteAgentJobAction.ID,516// TODO(joshspicer): Generalize title, pull from contribution517title: localize2('actions.chat.createRemoteJob', "Delegate to Coding Agent"),518icon: Codicon.sendToRemoteAgent,519precondition,520toggled: {521condition: ChatContextKeys.remoteJobCreating,522icon: Codicon.sync,523tooltip: localize('remoteJobCreating', "Delegating to Coding Agent"),524},525menu: [526{527id: MenuId.ChatExecute,528group: 'navigation',529order: 3.4,530when: ContextKeyExpr.and(ChatContextKeys.hasRemoteCodingAgent, ChatContextKeys.lockedToCodingAgent.negate()),531},532{533id: MenuId.ChatExecuteSecondary,534group: 'group_3',535order: 1,536when: ContextKeyExpr.and(ChatContextKeys.hasRemoteCodingAgent, ChatContextKeys.lockedToCodingAgent.negate()),537}538]539});540}541542private async pickCodingAgent<T extends IChatSessionsExtensionPoint | IRemoteCodingAgent>(543quickPickService: IQuickInputService,544options: T[]545): Promise<T | undefined> {546if (options.length === 0) {547return undefined;548}549if (options.length === 1) {550return options[0];551}552const pick = await quickPickService.pick(553options.map(a => ({554label: a.displayName,555description: a.description,556agent: a,557})),558{559title: localize('selectCodingAgent', "Select Coding Agent"),560}561);562if (!pick) {563return undefined;564}565return pick.agent;566}567568private async createWithChatSessions(569chatSessionsService: IChatSessionsService,570quickPickService: IQuickInputService,571editorService: IEditorService,572chatModel: IChatModel,573addedRequest: IChatRequestModel,574userPrompt: string,575summary?: string576) {577const contributions = chatSessionsService.getAllChatSessionContributions();578const agent = await this.pickCodingAgent(quickPickService, contributions);579if (!agent) {580chatModel.completeResponse(addedRequest);581return;582}583const { type } = agent;584const newChatSession = await chatSessionsService.provideNewChatSessionItem(585type,586{587prompt: userPrompt,588request: {589agentId: '',590location: ChatAgentLocation.Panel,591message: userPrompt,592requestId: '',593sessionId: '',594variables: { variables: [] },595},596metadata: {597summary,598source: 'chatExecuteActions',599}600},601CancellationToken.None,602);603const options: IChatEditorOptions = {604pinned: true,605preferredTitle: newChatSession.label,606};607await editorService.openEditor({608resource: ChatSessionUri.forSession(type, newChatSession.id),609options,610});611612}613614private async createWithLegacy(615remoteCodingAgentService: IRemoteCodingAgentsService,616commandService: ICommandService,617quickPickService: IQuickInputService,618chatModel: IChatModel,619addedRequest: IChatRequestModel,620widget: IChatWidget,621userPrompt: string,622summary?: string,623) {624const agents = remoteCodingAgentService.getAvailableAgents();625const agent = await this.pickCodingAgent(quickPickService, agents);626if (!agent) {627chatModel.completeResponse(addedRequest);628return;629}630631// Execute the remote command632const result: Omit<IChatPullRequestContent, 'kind'> | string | undefined = await commandService.executeCommand(agent.command, {633userPrompt,634summary: summary || userPrompt,635_version: 2, // Signal that we support the new response format636});637638if (result && typeof result === 'object') { /* _version === 2 */639chatModel.acceptResponseProgress(addedRequest, { kind: 'pullRequest', ...result });640chatModel.acceptResponseProgress(addedRequest, {641kind: 'markdownContent', content: new MarkdownString(642localize('remoteAgentResponse2', "Your work will be continued in this pull request."),643CreateRemoteAgentJobAction.markdownStringTrustedOptions644)645});646} else if (typeof result === 'string') {647chatModel.acceptResponseProgress(addedRequest, {648kind: 'markdownContent',649content: new MarkdownString(650localize('remoteAgentResponse', "Coding agent response: {0}", result),651CreateRemoteAgentJobAction.markdownStringTrustedOptions652)653});654// Extension will open up the pull request in another view655widget.clear();656} else {657chatModel.acceptResponseProgress(addedRequest, {658kind: 'markdownContent',659content: new MarkdownString(660localize('remoteAgentError', "Coding agent session cancelled."),661CreateRemoteAgentJobAction.markdownStringTrustedOptions662)663});664}665}666667/**668* Converts full URIs from the user's systems into workspace-relative paths for coding agent.669*/670private extractRelativeFromAttachedContext(attachedContext: ChatRequestVariableSet, workspaceContextService: IWorkspaceContextService): string[] {671const workspaceFolder = workspaceContextService.getWorkspace().folders[0];672if (!workspaceFolder) {673return [];674}675const relativePaths: string[] = [];676for (const contextEntry of attachedContext.asArray()) {677if (isChatRequestFileEntry(contextEntry)) { // TODO: Extend for more variable types as needed678if (!(contextEntry.value instanceof URI)) {679continue;680}681const fileUri = contextEntry.value;682const relativePathResult = relativePath(workspaceFolder.uri, fileUri);683if (relativePathResult) {684relativePaths.push(relativePathResult);685}686}687}688return relativePaths;689}690691private extractChatTurns(historyEntries: IChatAgentHistoryEntry[]): string {692let result = '\n';693for (const entry of historyEntries) {694if (entry.request.message) {695result += `User: ${entry.request.message}\n`;696}697if (entry.response) {698for (const content of entry.response) {699if (content.kind === 'markdownContent') {700result += `AI: ${content.content.value}\n`;701}702}703}704}705return `${result}\n`;706}707708async run(accessor: ServicesAccessor, ...args: any[]) {709const contextKeyService = accessor.get(IContextKeyService);710const remoteJobCreatingKey = ChatContextKeys.remoteJobCreating.bindTo(contextKeyService);711712try {713remoteJobCreatingKey.set(true);714715const configurationService = accessor.get(IConfigurationService);716const widgetService = accessor.get(IChatWidgetService);717const chatAgentService = accessor.get(IChatAgentService);718const commandService = accessor.get(ICommandService);719const quickPickService = accessor.get(IQuickInputService);720const remoteCodingAgentService = accessor.get(IRemoteCodingAgentsService);721const chatSessionsService = accessor.get(IChatSessionsService);722const editorService = accessor.get(IEditorService);723724725const widget = widgetService.lastFocusedWidget;726if (!widget) {727return;728}729const session = widget.viewModel?.sessionId;730if (!session) {731return;732}733const chatModel = widget.viewModel?.model;734if (!chatModel) {735return;736}737738const chatRequests = chatModel.getRequests();739let userPrompt = widget.getInput();740if (!userPrompt) {741if (!chatRequests.length) {742// Nothing to do743return;744}745userPrompt = 'implement this.';746}747748const attachedContext = widget.input.getAttachedAndImplicitContext(session);749widget.input.acceptInput(true);750751const defaultAgent = chatAgentService.getDefaultAgent(ChatAgentLocation.Panel);752const instantiationService = accessor.get(IInstantiationService);753const requestParser = instantiationService.createInstance(ChatRequestParser);754const parsedRequest = requestParser.parseChatRequest(session, userPrompt, ChatAgentLocation.Panel);755756757// Add the request to the model first758const addedRequest = chatModel.addRequest(759parsedRequest,760{ variables: attachedContext.asArray() },7610,762undefined,763defaultAgent,764);765766let summary: string = '';767const relativeAttachedContext = this.extractRelativeFromAttachedContext(attachedContext, accessor.get(IWorkspaceContextService));768if (relativeAttachedContext.length) {769summary += `\n\n${localize('attachedFiles', "The user has attached the following files from their workspace:")}\n${relativeAttachedContext.map(file => `- ${file}`).join('\n')}\n\n`;770}771772if (defaultAgent && chatRequests.length > 1) {773chatModel.acceptResponseProgress(addedRequest, {774kind: 'progressMessage',775content: new MarkdownString(776localize('analyzingChatHistory', "Analyzing chat history"),777CreateRemoteAgentJobAction.markdownStringTrustedOptions778)779});780const historyEntries: IChatAgentHistoryEntry[] = chatRequests781.map(req => ({782request: {783sessionId: session,784requestId: req.id,785agentId: req.response?.agent?.id ?? '',786message: req.message.text,787command: req.response?.slashCommand?.name,788variables: req.variableData,789location: ChatAgentLocation.Panel,790editedFileEvents: req.editedFileEvents,791},792response: toChatHistoryContent(req.response!.response.value),793result: req.response?.result ?? {}794}));795796// TODO: Determine a cutoff point where we stop including earlier history797// For example, if the user has already delegated to a coding agent once,798// prefer the conversation afterwards.799800summary += 'The following is a snapshot of a chat conversation between a user and an AI coding assistant. Prioritize later messages in the conversation.';801summary += this.extractChatTurns(historyEntries);802summary += await chatAgentService.getChatSummary(defaultAgent.id, historyEntries, CancellationToken.None);803}804805chatModel.acceptResponseProgress(addedRequest, {806kind: 'progressMessage',807content: new MarkdownString(808localize('creatingRemoteJob', "Delegating to coding agent"),809CreateRemoteAgentJobAction.markdownStringTrustedOptions810)811});812813const isChatSessionsEnabled = configurationService.getValue<boolean>(ChatConfiguration.UseChatSessionsForCloudButton);814if (isChatSessionsEnabled) {815await this.createWithChatSessions(chatSessionsService, quickPickService, editorService, chatModel, addedRequest, userPrompt, summary);816} else {817await this.createWithLegacy(remoteCodingAgentService, commandService, quickPickService, chatModel, addedRequest, widget, userPrompt, summary);818}819820chatModel.setResponse(addedRequest, {});821chatModel.completeResponse(addedRequest);822} finally {823remoteJobCreatingKey.set(false);824}825}826}827828export class ChatSubmitWithCodebaseAction extends Action2 {829static readonly ID = 'workbench.action.chat.submitWithCodebase';830831constructor() {832const precondition = ContextKeyExpr.and(833// if the input has prompt instructions attached, allow submitting requests even834// without text present - having instructions is enough context for a request835ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.hasPromptFile),836whenNotInProgress,837);838839super({840id: ChatSubmitWithCodebaseAction.ID,841title: localize2('actions.chat.submitWithCodebase', "Send with {0}", `${chatVariableLeader}codebase`),842precondition,843menu: {844id: MenuId.ChatExecuteSecondary,845group: 'group_1',846order: 3,847when: ContextKeyExpr.and(848ContextKeyExpr.equals(ChatContextKeys.location.key, ChatAgentLocation.Panel),849ChatContextKeys.lockedToCodingAgent.negate()850),851},852keybinding: {853when: ChatContextKeys.inChatInput,854primary: KeyMod.CtrlCmd | KeyCode.Enter,855weight: KeybindingWeight.EditorContrib856},857});858}859860run(accessor: ServicesAccessor, ...args: any[]) {861const context: IChatExecuteActionContext | undefined = args[0];862863const widgetService = accessor.get(IChatWidgetService);864const widget = context?.widget ?? widgetService.lastFocusedWidget;865if (!widget) {866return;867}868869const languageModelToolsService = accessor.get(ILanguageModelToolsService);870const codebaseTool = languageModelToolsService.getToolByName('codebase');871if (!codebaseTool) {872return;873}874875widget.input.attachmentModel.addContext({876id: codebaseTool.id,877name: codebaseTool.displayName ?? '',878fullName: codebaseTool.displayName ?? '',879value: undefined,880icon: ThemeIcon.isThemeIcon(codebaseTool.icon) ? codebaseTool.icon : undefined,881kind: 'tool'882});883widget.acceptInput();884}885}886887class SendToNewChatAction extends Action2 {888constructor() {889const precondition = ContextKeyExpr.and(890// if the input has prompt instructions attached, allow submitting requests even891// without text present - having instructions is enough context for a request892ContextKeyExpr.or(ChatContextKeys.inputHasText, ChatContextKeys.hasPromptFile),893whenNotInProgress,894);895896super({897id: 'workbench.action.chat.sendToNewChat',898title: localize2('chat.newChat.label', "Send to New Chat"),899precondition,900category: CHAT_CATEGORY,901f1: false,902menu: {903id: MenuId.ChatExecuteSecondary,904group: 'group_2',905when: ContextKeyExpr.and(906ContextKeyExpr.equals(ChatContextKeys.location.key, ChatAgentLocation.Panel),907ChatContextKeys.lockedToCodingAgent.negate()908)909},910keybinding: {911weight: KeybindingWeight.WorkbenchContrib,912primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter,913when: ChatContextKeys.inChatInput,914}915});916}917918async run(accessor: ServicesAccessor, ...args: any[]) {919const context: IChatExecuteActionContext | undefined = args[0];920921const widgetService = accessor.get(IChatWidgetService);922const dialogService = accessor.get(IDialogService);923const widget = context?.widget ?? widgetService.lastFocusedWidget;924if (!widget) {925return;926}927928const editingSession = widget.viewModel?.model.editingSession;929if (editingSession) {930if (!(await handleCurrentEditingSession(editingSession, undefined, dialogService))) {931return;932}933}934935widget.clear();936await widget.waitForReady();937widget.acceptInput(context?.inputValue);938}939}940941export const CancelChatActionId = 'workbench.action.chat.cancel';942export class CancelAction extends Action2 {943static readonly ID = CancelChatActionId;944constructor() {945super({946id: CancelAction.ID,947title: localize2('interactive.cancel.label', "Cancel"),948f1: false,949category: CHAT_CATEGORY,950icon: Codicon.stopCircle,951menu: [{952id: MenuId.ChatExecute,953when: ContextKeyExpr.and(954ChatContextKeys.requestInProgress,955ChatContextKeys.remoteJobCreating.negate()956),957order: 4,958group: 'navigation',959},960],961keybinding: {962weight: KeybindingWeight.WorkbenchContrib,963primary: KeyMod.CtrlCmd | KeyCode.Escape,964win: { primary: KeyMod.Alt | KeyCode.Backspace },965}966});967}968969run(accessor: ServicesAccessor, ...args: any[]) {970const context: IChatExecuteActionContext | undefined = args[0];971const widgetService = accessor.get(IChatWidgetService);972const widget = context?.widget ?? widgetService.lastFocusedWidget;973if (!widget) {974return;975}976977const chatService = accessor.get(IChatService);978if (widget.viewModel) {979chatService.cancelCurrentRequestForSession(widget.viewModel.sessionId);980}981}982}983984export const CancelChatEditId = 'workbench.edit.chat.cancel';985export class CancelEdit extends Action2 {986static readonly ID = CancelChatEditId;987constructor() {988super({989id: CancelEdit.ID,990title: localize2('interactive.cancelEdit.label', "Cancel Edit"),991f1: false,992category: CHAT_CATEGORY,993icon: Codicon.x,994menu: [995{996id: MenuId.ChatMessageTitle,997group: 'navigation',998order: 1,999when: ContextKeyExpr.and(ChatContextKeys.isRequest, ChatContextKeys.currentlyEditing, ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'input'))1000}1001],1002keybinding: {1003primary: KeyCode.Escape,1004when: ContextKeyExpr.and(ChatContextKeys.inChatInput,1005EditorContextKeys.hoverVisible.toNegated(),1006EditorContextKeys.hasNonEmptySelection.toNegated(),1007EditorContextKeys.hasMultipleSelections.toNegated(),1008ContextKeyExpr.or(ChatContextKeys.currentlyEditing, ChatContextKeys.currentlyEditingInput)),1009weight: KeybindingWeight.EditorContrib - 51010}1011});1012}10131014run(accessor: ServicesAccessor, ...args: any[]) {1015const context: IChatExecuteActionContext | undefined = args[0];10161017const widgetService = accessor.get(IChatWidgetService);1018const widget = context?.widget ?? widgetService.lastFocusedWidget;1019if (!widget) {1020return;1021}1022widget.finishedEditing();1023}1024}102510261027export function registerChatExecuteActions() {1028registerAction2(ChatSubmitAction);1029registerAction2(ChatEditingSessionSubmitAction);1030registerAction2(SubmitWithoutDispatchingAction);1031registerAction2(CancelAction);1032registerAction2(SendToNewChatAction);1033registerAction2(ChatSubmitWithCodebaseAction);1034registerAction2(CreateRemoteAgentJobAction);1035registerAction2(ToggleChatModeAction);1036registerAction2(SwitchToNextModelAction);1037registerAction2(OpenModelPickerAction);1038registerAction2(OpenModePickerAction);1039registerAction2(ChangeChatModelAction);1040registerAction2(CancelEdit);1041}104210431044