Path: blob/main/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts
5257 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 { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';8import { alert } from '../../../../../base/browser/ui/aria/aria.js';9import { basename } from '../../../../../base/common/resources.js';10import { URI, UriComponents } from '../../../../../base/common/uri.js';11import { isCodeEditor } from '../../../../../editor/browser/editorBrowser.js';12import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';13import { Position } from '../../../../../editor/common/core/position.js';14import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js';15import { isLocation, Location } from '../../../../../editor/common/languages.js';16import { ITextModel } from '../../../../../editor/common/model.js';17import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js';18import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';19import { localize, localize2 } from '../../../../../nls.js';20import { Action2, IAction2Options, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';21import { CommandsRegistry, ICommandService } from '../../../../../platform/commands/common/commands.js';22import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';23import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';24import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';25import { EditorActivation } from '../../../../../platform/editor/common/editor.js';26import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';27import { IEditorPane } from '../../../../common/editor.js';28import { IEditorService } from '../../../../services/editor/common/editorService.js';29import { IAgentSessionsService } from '../agentSessions/agentSessionsService.js';30import { isChatViewTitleActionContext } from '../../common/actions/chatActions.js';31import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';32import { applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingResourceContextKey, chatEditingWidgetFileStateContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js';33import { IChatService } from '../../common/chatService/chatService.js';34import { isChatTreeItem, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js';35import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';36import { CHAT_CATEGORY } from '../actions/chatActions.js';37import { ChatTreeItem, IChatWidget, IChatWidgetService } from '../chat.js';38import { IAgentSession, isAgentSession } from '../agentSessions/agentSessionsModel.js';39import { AgentSessionProviders } from '../agentSessions/agentSessions.js';4041export abstract class EditingSessionAction extends Action2 {4243constructor(opts: Readonly<IAction2Options>) {44super({45category: CHAT_CATEGORY,46...opts47});48}4950run(accessor: ServicesAccessor, ...args: unknown[]) {51const context = getEditingSessionContext(accessor, args);52if (!context || !context.editingSession) {53return;54}5556return this.runEditingSessionAction(accessor, context.editingSession, context.chatWidget, ...args);57}5859// eslint-disable-next-line @typescript-eslint/no-explicit-any60abstract runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]): any;61}6263export type EditingSessionActionContext = { editingSession?: IChatEditingSession; chatWidget: IChatWidget };6465/**66* Resolve view title toolbar context. If none, return context from the lastFocusedWidget.67*/68// eslint-disable-next-line @typescript-eslint/no-explicit-any69export function getEditingSessionContext(accessor: ServicesAccessor, args: any[]): EditingSessionActionContext | undefined {70const arg0 = args.at(0);71const context = isChatViewTitleActionContext(arg0) ? arg0 : undefined;7273const chatWidgetService = accessor.get(IChatWidgetService);74const chatEditingService = accessor.get(IChatEditingService);75let chatWidget = context ? chatWidgetService.getWidgetBySessionResource(context.sessionResource) : undefined;76if (!chatWidget) {77chatWidget = chatWidgetService.lastFocusedWidget ?? chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat).find(w => w.supportsChangingModes);78}7980if (!chatWidget?.viewModel) {81return;82}8384const editingSession = chatEditingService.getEditingSession(chatWidget.viewModel.model.sessionResource);85return { editingSession, chatWidget };86}878889abstract class WorkingSetAction extends EditingSessionAction {9091runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]) {9293const uris: URI[] = [];94if (URI.isUri(args[0])) {95uris.push(args[0]);96} else if (chatWidget) {97uris.push(...chatWidget.input.selectedElements);98}99if (!uris.length) {100return;101}102103return this.runWorkingSetAction(accessor, editingSession, chatWidget, ...uris);104}105106// eslint-disable-next-line @typescript-eslint/no-explicit-any107abstract runWorkingSetAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget | undefined, ...uris: URI[]): any;108}109110registerAction2(class OpenFileInDiffAction extends WorkingSetAction {111constructor() {112super({113id: 'chatEditing.openFileInDiff',114title: localize2('open.fileInDiff', 'Open Changes in Diff Editor'),115icon: Codicon.diffSingle,116menu: [{117id: MenuId.ChatEditingWidgetModifiedFilesToolbar,118when: ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, ModifiedFileEntryState.Modified),119order: 2,120group: 'navigation'121}],122});123}124125async runWorkingSetAction(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession, _chatWidget: IChatWidget, ...uris: URI[]): Promise<void> {126const editorService = accessor.get(IEditorService);127128129for (const uri of uris) {130131let pane: IEditorPane | undefined = editorService.activeEditorPane;132if (!pane) {133pane = await editorService.openEditor({ resource: uri });134}135136if (!pane) {137return;138}139140const editedFile = currentEditingSession.getEntry(uri);141editedFile?.getEditorIntegration(pane).toggleDiff(undefined, true);142}143}144});145146registerAction2(class AcceptAction extends WorkingSetAction {147constructor() {148super({149id: 'chatEditing.acceptFile',150title: localize2('accept.file', 'Keep'),151icon: Codicon.check,152menu: [{153when: ContextKeyExpr.and(ContextKeyExpr.equals('resourceScheme', CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME), ContextKeyExpr.notIn(chatEditingResourceContextKey.key, decidedChatEditingResourceContextKey.key)),154id: MenuId.MultiDiffEditorFileToolbar,155order: 0,156group: 'navigation',157}, {158id: MenuId.ChatEditingWidgetModifiedFilesToolbar,159when: ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, ModifiedFileEntryState.Modified),160order: 0,161group: 'navigation'162}],163});164}165166async runWorkingSetAction(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession, chatWidget: IChatWidget, ...uris: URI[]): Promise<void> {167await currentEditingSession.accept(...uris);168}169});170171registerAction2(class DiscardAction extends WorkingSetAction {172constructor() {173super({174id: 'chatEditing.discardFile',175title: localize2('discard.file', 'Undo'),176icon: Codicon.discard,177menu: [{178when: ContextKeyExpr.and(ContextKeyExpr.equals('resourceScheme', CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME), ContextKeyExpr.notIn(chatEditingResourceContextKey.key, decidedChatEditingResourceContextKey.key)),179id: MenuId.MultiDiffEditorFileToolbar,180order: 2,181group: 'navigation',182}, {183id: MenuId.ChatEditingWidgetModifiedFilesToolbar,184when: ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, ModifiedFileEntryState.Modified),185order: 1,186group: 'navigation'187}],188});189}190191async runWorkingSetAction(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession, chatWidget: IChatWidget, ...uris: URI[]): Promise<void> {192await currentEditingSession.reject(...uris);193}194});195196export class ChatEditingAcceptAllAction extends EditingSessionAction {197198constructor() {199super({200id: 'chatEditing.acceptAllFiles',201title: localize('accept', 'Keep'),202icon: Codicon.check,203tooltip: localize('acceptAllEdits', 'Keep All Edits'),204precondition: hasUndecidedChatEditingResourceContextKey,205keybinding: {206primary: KeyMod.CtrlCmd | KeyCode.Enter,207when: ContextKeyExpr.and(hasUndecidedChatEditingResourceContextKey, ChatContextKeys.inChatInput),208weight: KeybindingWeight.WorkbenchContrib,209},210menu: [211212{213id: MenuId.ChatEditingWidgetToolbar,214group: 'navigation',215order: 0,216when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), ContextKeyExpr.and(hasUndecidedChatEditingResourceContextKey))217}218]219});220}221222override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]) {223await editingSession.accept();224}225}226registerAction2(ChatEditingAcceptAllAction);227228export class ChatEditingDiscardAllAction extends EditingSessionAction {229230constructor() {231super({232id: 'chatEditing.discardAllFiles',233title: localize('discard', 'Undo'),234icon: Codicon.discard,235tooltip: localize('discardAllEdits', 'Undo All Edits'),236precondition: hasUndecidedChatEditingResourceContextKey,237menu: [238{239id: MenuId.ChatEditingWidgetToolbar,240group: 'navigation',241order: 1,242when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), hasUndecidedChatEditingResourceContextKey)243}244],245keybinding: {246when: ContextKeyExpr.and(hasUndecidedChatEditingResourceContextKey, ChatContextKeys.inChatInput, ChatContextKeys.inputHasText.negate()),247weight: KeybindingWeight.WorkbenchContrib,248primary: KeyMod.CtrlCmd | KeyCode.Backspace,249},250});251}252253override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]) {254await discardAllEditsWithConfirmation(accessor, editingSession);255}256}257registerAction2(ChatEditingDiscardAllAction);258259export class ToggleExplanationWidgetAction extends EditingSessionAction {260261static readonly ID = 'chatEditing.toggleExplanationWidget';262263constructor() {264super({265id: ToggleExplanationWidgetAction.ID,266title: localize('explainButton', 'Explain'),267tooltip: localize('toggleExplanationTooltip', 'Toggle Change Explanations'),268precondition: hasUndecidedChatEditingResourceContextKey,269menu: [270{271id: MenuId.ChatEditingWidgetToolbar,272group: 'navigation',273order: 2,274when: ContextKeyExpr.and(hasUndecidedChatEditingResourceContextKey, ContextKeyExpr.has(`config.${ChatConfiguration.ExplainChangesEnabled}`))275}276],277});278}279280override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]) {281if (editingSession.hasExplanations()) {282editingSession.clearExplanations();283} else {284await editingSession.triggerExplanationGeneration();285}286}287}288registerAction2(ToggleExplanationWidgetAction);289290export async function discardAllEditsWithConfirmation(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession): Promise<boolean> {291292const dialogService = accessor.get(IDialogService);293294// Ask for confirmation if there are any edits295const entries = currentEditingSession.entries.get().filter(e => e.state.get() === ModifiedFileEntryState.Modified);296if (entries.length > 0) {297const confirmation = await dialogService.confirm({298title: localize('chat.editing.discardAll.confirmation.title', "Undo all edits?"),299message: entries.length === 1300? localize('chat.editing.discardAll.confirmation.oneFile', "This will undo changes made in {0}. Do you want to proceed?", basename(entries[0].modifiedURI))301: localize('chat.editing.discardAll.confirmation.manyFiles', "This will undo changes made in {0} files. Do you want to proceed?", entries.length),302primaryButton: localize('chat.editing.discardAll.confirmation.primaryButton', "Yes"),303type: 'info'304});305if (!confirmation.confirmed) {306return false;307}308}309310await currentEditingSession.reject();311return true;312}313314export class ChatEditingShowChangesAction extends EditingSessionAction {315static readonly ID = 'chatEditing.viewChanges';316static readonly LABEL = localize('chatEditing.viewChanges', 'View All Edits');317318constructor() {319super({320id: ChatEditingShowChangesAction.ID,321title: { value: ChatEditingShowChangesAction.LABEL, original: ChatEditingShowChangesAction.LABEL },322tooltip: ChatEditingShowChangesAction.LABEL,323f1: true,324icon: Codicon.diffMultiple,325precondition: hasUndecidedChatEditingResourceContextKey,326menu: [327{328id: MenuId.ChatEditingWidgetToolbar,329group: 'navigation',330order: 4,331when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), ContextKeyExpr.and(hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey))332}333],334});335}336337override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]): Promise<void> {338await editingSession.show();339}340}341registerAction2(ChatEditingShowChangesAction);342343export class ViewAllSessionChangesAction extends Action2 {344static readonly ID = 'chatEditing.viewAllSessionChanges';345346constructor() {347super({348id: ViewAllSessionChangesAction.ID,349title: localize2('chatEditing.viewAllSessionChanges', 'View All Changes'),350icon: Codicon.diffMultiple,351category: CHAT_CATEGORY,352precondition: ChatContextKeys.hasAgentSessionChanges,353menu: [354{355id: MenuId.ChatEditingSessionChangesToolbar,356group: 'navigation',357order: 10,358when: ChatContextKeys.hasAgentSessionChanges359},360{361id: MenuId.AgentSessionItemToolbar,362group: 'navigation',363order: 0,364when: ChatContextKeys.hasAgentSessionChanges365}366],367});368}369370override async run(accessor: ServicesAccessor, sessionOrSessionResource?: URI | IAgentSession): Promise<void> {371const agentSessionsService = accessor.get(IAgentSessionsService);372const commandService = accessor.get(ICommandService);373const chatEditingService = accessor.get(IChatEditingService);374375if (!URI.isUri(sessionOrSessionResource) && !isAgentSession(sessionOrSessionResource)) {376return;377}378379const sessionResource = URI.isUri(sessionOrSessionResource)380? sessionOrSessionResource381: sessionOrSessionResource.resource;382383const session = agentSessionsService.getSession(sessionResource);384const changes = session?.changes;385386if (!session || !changes) {387return;388}389390if (391session.providerType === AgentSessionProviders.Background ||392session.providerType === AgentSessionProviders.Cloud393) {394if (!Array.isArray(changes) || changes.length === 0) {395return;396}397398// Use agent session changes399const resources = changes.map(d => ({400originalUri: d.originalUri,401modifiedUri: d.modifiedUri402}));403404await commandService.executeCommand('_workbench.openMultiDiffEditor', {405multiDiffSourceUri: sessionResource.with({ scheme: sessionResource.scheme + '-worktree-changes' }),406title: localize('chatEditing.allChanges.title', 'All Session Changes'),407resources,408});409410session?.setRead(true);411return;412}413414// Use edit session changes415const editingSession = chatEditingService.getEditingSession(sessionResource);416await editingSession?.show();417session?.setRead(true);418}419}420registerAction2(ViewAllSessionChangesAction);421422async function restoreSnapshotWithConfirmationByRequestId(accessor: ServicesAccessor, sessionResource: URI, requestId: string): Promise<void> {423const configurationService = accessor.get(IConfigurationService);424const dialogService = accessor.get(IDialogService);425const chatWidgetService = accessor.get(IChatWidgetService);426const widget = chatWidgetService.getWidgetBySessionResource(sessionResource);427const chatService = accessor.get(IChatService);428const chatModel = chatService.getSession(sessionResource);429if (!chatModel) {430return;431}432433const session = chatModel.editingSession;434if (!session) {435return;436}437438const chatRequests = chatModel.getRequests();439const itemIndex = chatRequests.findIndex(request => request.id === requestId);440if (itemIndex === -1) {441return;442}443444const editsToUndo = chatRequests.length - itemIndex;445446const requestsToRemove = chatRequests.slice(itemIndex);447const requestIdsToRemove = new Set(requestsToRemove.map(request => request.id));448const entriesModifiedInRequestsToRemove = session.entries.get().filter((entry) => requestIdsToRemove.has(entry.lastModifyingRequestId)) ?? [];449const shouldPrompt = entriesModifiedInRequestsToRemove.length > 0 && configurationService.getValue('chat.editing.confirmEditRequestRemoval') === true;450451let message: string;452if (editsToUndo === 1) {453if (entriesModifiedInRequestsToRemove.length === 1) {454message = 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));455} else {456message = 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);457}458} else {459if (entriesModifiedInRequestsToRemove.length === 1) {460message = 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));461} else {462message = 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);463}464}465466const confirmation = shouldPrompt467? await dialogService.confirm({468title: editsToUndo === 1469? localize('chat.removeLast.confirmation.title', "Do you want to undo your last edit?")470: localize('chat.remove.confirmation.title', "Do you want to undo {0} edits?", editsToUndo),471message: message,472primaryButton: localize('chat.remove.confirmation.primaryButton', "Yes"),473checkbox: { label: localize('chat.remove.confirmation.checkbox', "Don't ask again"), checked: false },474type: 'info'475})476: { confirmed: true };477478if (!confirmation.confirmed) {479widget?.viewModel?.model.setCheckpoint(undefined);480return;481}482483if (confirmation.checkboxChecked) {484await configurationService.updateValue('chat.editing.confirmEditRequestRemoval', false);485}486487// Restore the snapshot to what it was before the request(s) that we deleted488const snapshotRequestId = chatRequests[itemIndex].id;489await session.restoreSnapshot(snapshotRequestId, undefined);490}491492async function restoreSnapshotWithConfirmation(accessor: ServicesAccessor, item: ChatTreeItem): Promise<void> {493const requestId = isRequestVM(item) ? item.id :494isResponseVM(item) ? item.requestId : undefined;495496if (!requestId) {497return;498}499500await restoreSnapshotWithConfirmationByRequestId(accessor, item.sessionResource, requestId);501}502503registerAction2(class RemoveAction extends Action2 {504constructor() {505super({506id: 'workbench.action.chat.undoEdits',507title: localize2('chat.undoEdits.label', "Undo Requests"),508f1: false,509category: CHAT_CATEGORY,510icon: Codicon.discard,511keybinding: {512primary: KeyCode.Delete,513mac: {514primary: KeyMod.CtrlCmd | KeyCode.Backspace,515},516when: ContextKeyExpr.and(ChatContextKeys.inChatSession, EditorContextKeys.textInputFocus.negate()),517weight: KeybindingWeight.WorkbenchContrib,518},519menu: [520{521id: MenuId.ChatMessageTitle,522group: 'navigation',523order: 2,524when: ContextKeyExpr.and(ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'input').negate(), ContextKeyExpr.equals(`config.${ChatConfiguration.CheckpointsEnabled}`, false), ChatContextKeys.lockedToCodingAgent.negate()),525}526]527});528}529530async run(accessor: ServicesAccessor, ...args: unknown[]) {531let item = args[0] as ChatTreeItem | undefined;532const chatWidgetService = accessor.get(IChatWidgetService);533const configurationService = accessor.get(IConfigurationService);534const widget = (isChatTreeItem(item) && chatWidgetService.getWidgetBySessionResource(item.sessionResource)) || chatWidgetService.lastFocusedWidget;535if (!isResponseVM(item) && !isRequestVM(item)) {536item = widget?.getFocus();537}538539if (!item) {540return;541}542543await restoreSnapshotWithConfirmation(accessor, item);544545if (isRequestVM(item) && configurationService.getValue('chat.undoRequests.restoreInput')) {546widget?.focusInput();547widget?.input.setValue(item.messageText, false);548}549}550});551552registerAction2(class RestoreCheckpointAction extends Action2 {553constructor() {554super({555id: 'workbench.action.chat.restoreCheckpoint',556title: localize2('chat.restoreCheckpoint.label', "Restore Checkpoint"),557tooltip: localize2('chat.restoreCheckpoint.tooltip', "Restores workspace and chat to this point"),558f1: false,559category: CHAT_CATEGORY,560keybinding: {561primary: KeyCode.Delete,562mac: {563primary: KeyMod.CtrlCmd | KeyCode.Backspace,564},565when: ContextKeyExpr.and(ChatContextKeys.inChatSession, EditorContextKeys.textInputFocus.negate()),566weight: KeybindingWeight.WorkbenchContrib,567},568menu: [569{570id: MenuId.ChatMessageCheckpoint,571group: 'navigation',572order: 2,573when: ContextKeyExpr.and(ChatContextKeys.isRequest, ChatContextKeys.lockedToCodingAgent.negate())574}575]576});577}578579async run(accessor: ServicesAccessor, ...args: unknown[]) {580let item = args[0] as ChatTreeItem | undefined;581const chatWidgetService = accessor.get(IChatWidgetService);582const widget = (isChatTreeItem(item) && chatWidgetService.getWidgetBySessionResource(item.sessionResource)) || chatWidgetService.lastFocusedWidget;583if (!isResponseVM(item) && !isRequestVM(item)) {584item = widget?.getFocus();585}586587if (!item) {588return;589}590591if (isRequestVM(item)) {592widget?.focusInput();593widget?.input.setValue(item.messageText, false);594}595596widget?.viewModel?.model.setCheckpoint(item.id);597await restoreSnapshotWithConfirmation(accessor, item);598}599});600601registerAction2(class RestoreLastCheckpoint extends Action2 {602constructor() {603super({604id: 'workbench.action.chat.restoreLastCheckpoint',605title: localize2('chat.restoreLastCheckpoint.label', "Restore to Last Checkpoint"),606f1: true,607category: CHAT_CATEGORY,608icon: Codicon.discard,609precondition: ContextKeyExpr.and(610ChatContextKeys.inChatSession,611ContextKeyExpr.equals(`config.${ChatConfiguration.CheckpointsEnabled}`, true),612ChatContextKeys.lockedToCodingAgent.negate()613),614menu: [615{616id: MenuId.ChatMessageFooter,617group: 'navigation',618order: 1,619when: ContextKeyExpr.and(ContextKeyExpr.in(ChatContextKeys.itemId.key, ChatContextKeys.lastItemId.key), ContextKeyExpr.equals(`config.${ChatConfiguration.CheckpointsEnabled}`, true), ChatContextKeys.lockedToCodingAgent.negate()),620}621]622});623}624625async run(accessor: ServicesAccessor, ...args: unknown[]) {626let item = args[0] as ChatTreeItem | undefined;627const chatWidgetService = accessor.get(IChatWidgetService);628const chatService = accessor.get(IChatService);629const widget = (isChatTreeItem(item) && chatWidgetService.getWidgetBySessionResource(item.sessionResource)) || chatWidgetService.lastFocusedWidget;630if (!isResponseVM(item) && !isRequestVM(item)) {631item = widget?.getFocus();632}633634const sessionResource = widget?.viewModel?.sessionResource ?? (isChatTreeItem(item) ? item.sessionResource : undefined);635if (!sessionResource) {636return;637}638639const chatModel = chatService.getSession(sessionResource);640if (!chatModel?.editingSession) {641return;642}643644const checkpointRequest = chatModel.checkpoint;645if (!checkpointRequest) {646alert(localize('chat.restoreCheckpoint.none', 'There is no checkpoint to restore.'));647return;648}649650widget?.viewModel?.model.setCheckpoint(checkpointRequest.id);651widget?.focusInput();652widget?.input.setValue(checkpointRequest.message.text, false);653654await restoreSnapshotWithConfirmationByRequestId(accessor, sessionResource, checkpointRequest.id);655}656});657658registerAction2(class EditAction extends Action2 {659constructor() {660super({661id: 'workbench.action.chat.editRequests',662title: localize2('chat.editRequests.label', "Edit Request"),663f1: false,664category: CHAT_CATEGORY,665icon: Codicon.edit,666keybinding: {667primary: KeyCode.Enter,668when: ContextKeyExpr.and(ChatContextKeys.inChatSession, EditorContextKeys.textInputFocus.negate()),669weight: KeybindingWeight.WorkbenchContrib,670},671menu: [672{673id: MenuId.ChatMessageTitle,674group: 'navigation',675order: 2,676when: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'hover'), ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'input')))677}678]679});680}681682async run(accessor: ServicesAccessor, ...args: unknown[]) {683let item = args[0] as ChatTreeItem | undefined;684const chatWidgetService = accessor.get(IChatWidgetService);685const widget = (isChatTreeItem(item) && chatWidgetService.getWidgetBySessionResource(item.sessionResource)) || chatWidgetService.lastFocusedWidget;686if (!isResponseVM(item) && !isRequestVM(item)) {687item = widget?.getFocus();688}689690if (!item) {691return;692}693694if (isRequestVM(item)) {695widget?.startEditing(item.id);696}697}698});699700export interface ChatEditingActionContext {701readonly sessionResource: URI;702readonly requestId: string;703readonly uri: URI;704readonly stopId: string | undefined;705}706707registerAction2(class OpenWorkingSetHistoryAction extends Action2 {708709static readonly id = 'chat.openFileUpdatedBySnapshot';710constructor() {711super({712id: OpenWorkingSetHistoryAction.id,713title: localize('chat.openFileUpdatedBySnapshot.label', "Open File"),714menu: [{715id: MenuId.ChatEditingCodeBlockContext,716group: 'navigation',717order: 0,718},]719});720}721722override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise<void> {723const context = args[0] as ChatEditingActionContext | undefined;724if (!context?.sessionResource) {725return;726}727728const editorService = accessor.get(IEditorService);729await editorService.openEditor({ resource: context.uri });730}731});732733registerAction2(class OpenWorkingSetHistoryAction extends Action2 {734735static readonly id = 'chat.openFileSnapshot';736constructor() {737super({738id: OpenWorkingSetHistoryAction.id,739title: localize('chat.openSnapshot.label', "Open File Snapshot"),740menu: [{741id: MenuId.ChatEditingCodeBlockContext,742group: 'navigation',743order: 1,744},]745});746}747748override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise<void> {749const context = args[0] as ChatEditingActionContext | undefined;750if (!context?.sessionResource) {751return;752}753754const chatService = accessor.get(IChatService);755const chatEditingService = accessor.get(IChatEditingService);756const editorService = accessor.get(IEditorService);757758const chatModel = chatService.getSession(context.sessionResource);759if (!chatModel) {760return;761}762763const snapshot = chatEditingService.getEditingSession(chatModel.sessionResource)?.getSnapshotUri(context.requestId, context.uri, context.stopId);764if (snapshot) {765const editor = await editorService.openEditor({ resource: snapshot, label: localize('chatEditing.snapshot', '{0} (Snapshot)', basename(context.uri)), options: { activation: EditorActivation.ACTIVATE } });766if (isCodeEditor(editor)) {767editor.updateOptions({ readOnly: true });768}769}770}771});772773registerAction2(class ResolveSymbolsContextAction extends EditingSessionAction {774constructor() {775super({776id: 'workbench.action.edits.addFilesFromReferences',777title: localize2('addFilesFromReferences', "Add Files From References"),778f1: false,779category: CHAT_CATEGORY,780menu: {781id: MenuId.ChatInputSymbolAttachmentContext,782group: 'navigation',783order: 1,784when: ContextKeyExpr.and(ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask), EditorContextKeys.hasReferenceProvider)785}786});787}788789override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]): Promise<void> {790if (args.length === 0 || !isLocation(args[0])) {791return;792}793794const textModelService = accessor.get(ITextModelService);795const languageFeaturesService = accessor.get(ILanguageFeaturesService);796const symbol = args[0] as Location;797798const modelReference = await textModelService.createModelReference(symbol.uri);799const textModel = modelReference.object.textEditorModel;800if (!textModel) {801return;802}803804const position = new Position(symbol.range.startLineNumber, symbol.range.startColumn);805806const [references, definitions, implementations] = await Promise.all([807this.getReferences(position, textModel, languageFeaturesService),808this.getDefinitions(position, textModel, languageFeaturesService),809this.getImplementations(position, textModel, languageFeaturesService)810]);811812// Sort the references, definitions and implementations by813// how important it is that they make it into the working set as it has limited size814const attachments = [];815for (const reference of [...definitions, ...implementations, ...references]) {816attachments.push(chatWidget.attachmentModel.asFileVariableEntry(reference.uri));817}818819chatWidget.attachmentModel.addContext(...attachments);820}821822private async getReferences(position: Position, textModel: ITextModel, languageFeaturesService: ILanguageFeaturesService): Promise<Location[]> {823const referenceProviders = languageFeaturesService.referenceProvider.all(textModel);824825const references = await Promise.all(referenceProviders.map(async (referenceProvider) => {826return await referenceProvider.provideReferences(textModel, position, { includeDeclaration: true }, CancellationToken.None) ?? [];827}));828829return references.flat();830}831832private async getDefinitions(position: Position, textModel: ITextModel, languageFeaturesService: ILanguageFeaturesService): Promise<Location[]> {833const definitionProviders = languageFeaturesService.definitionProvider.all(textModel);834835const definitions = await Promise.all(definitionProviders.map(async (definitionProvider) => {836return await definitionProvider.provideDefinition(textModel, position, CancellationToken.None) ?? [];837}));838839return definitions.flat();840}841842private async getImplementations(position: Position, textModel: ITextModel, languageFeaturesService: ILanguageFeaturesService): Promise<Location[]> {843const implementationProviders = languageFeaturesService.implementationProvider.all(textModel);844845const implementations = await Promise.all(implementationProviders.map(async (implementationProvider) => {846return await implementationProvider.provideImplementation(textModel, position, CancellationToken.None) ?? [];847}));848849return implementations.flat();850}851});852853export class ViewPreviousEditsAction extends EditingSessionAction {854static readonly Id = 'chatEditing.viewPreviousEdits';855static readonly Label = localize('chatEditing.viewPreviousEdits', 'View Previous Edits');856857constructor() {858super({859id: ViewPreviousEditsAction.Id,860title: { value: ViewPreviousEditsAction.Label, original: ViewPreviousEditsAction.Label },861tooltip: ViewPreviousEditsAction.Label,862f1: true,863icon: Codicon.diffMultiple,864precondition: ContextKeyExpr.and(ChatContextKeys.enabled, hasUndecidedChatEditingResourceContextKey.negate()),865menu: [866{867id: MenuId.ChatEditingWidgetToolbar,868group: 'navigation',869order: 4,870when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), ContextKeyExpr.and(hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey.negate()))871}872],873});874}875876override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]): Promise<void> {877await editingSession.show(true);878}879}880registerAction2(ViewPreviousEditsAction);881882/**883* Workbench command to explore accepting working set changes from an extension. Executing884* the command will accept the changes for the provided resources across all edit sessions.885*/886CommandsRegistry.registerCommand('_chat.editSessions.accept', async (accessor: ServicesAccessor, resources: UriComponents[]) => {887if (resources.length === 0) {888return;889}890891const uris = resources.map(resource => URI.revive(resource));892const chatEditingService = accessor.get(IChatEditingService);893for (const editingSession of chatEditingService.editingSessionsObs.get()) {894await editingSession.accept(...uris);895}896});897898899