mirror of
https://github.com/microsoft/vscode.git
synced 2026-02-15 07:28:05 +00:00
898 lines
33 KiB
TypeScript
898 lines
33 KiB
TypeScript
/*---------------------------------------------------------------------------------------------
|
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
* Licensed under the MIT License. See License.txt in the project root for license information.
|
|
*--------------------------------------------------------------------------------------------*/
|
|
|
|
import { CancellationToken } from '../../../../../base/common/cancellation.js';
|
|
import { Codicon } from '../../../../../base/common/codicons.js';
|
|
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
|
|
import { alert } from '../../../../../base/browser/ui/aria/aria.js';
|
|
import { basename } from '../../../../../base/common/resources.js';
|
|
import { URI, UriComponents } from '../../../../../base/common/uri.js';
|
|
import { isCodeEditor } from '../../../../../editor/browser/editorBrowser.js';
|
|
import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';
|
|
import { Position } from '../../../../../editor/common/core/position.js';
|
|
import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js';
|
|
import { isLocation, Location } from '../../../../../editor/common/languages.js';
|
|
import { ITextModel } from '../../../../../editor/common/model.js';
|
|
import { ILanguageFeaturesService } from '../../../../../editor/common/services/languageFeatures.js';
|
|
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
|
|
import { localize, localize2 } from '../../../../../nls.js';
|
|
import { Action2, IAction2Options, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';
|
|
import { CommandsRegistry, ICommandService } from '../../../../../platform/commands/common/commands.js';
|
|
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
|
|
import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';
|
|
import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';
|
|
import { EditorActivation } from '../../../../../platform/editor/common/editor.js';
|
|
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
|
|
import { IEditorPane } from '../../../../common/editor.js';
|
|
import { IEditorService } from '../../../../services/editor/common/editorService.js';
|
|
import { IAgentSessionsService } from '../agentSessions/agentSessionsService.js';
|
|
import { isChatViewTitleActionContext } from '../../common/actions/chatActions.js';
|
|
import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';
|
|
import { applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingResourceContextKey, chatEditingWidgetFileStateContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js';
|
|
import { IChatService } from '../../common/chatService/chatService.js';
|
|
import { isChatTreeItem, isRequestVM, isResponseVM } from '../../common/model/chatViewModel.js';
|
|
import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';
|
|
import { CHAT_CATEGORY } from '../actions/chatActions.js';
|
|
import { ChatTreeItem, IChatWidget, IChatWidgetService } from '../chat.js';
|
|
import { IAgentSession, isAgentSession } from '../agentSessions/agentSessionsModel.js';
|
|
import { AgentSessionProviders } from '../agentSessions/agentSessions.js';
|
|
|
|
export abstract class EditingSessionAction extends Action2 {
|
|
|
|
constructor(opts: Readonly<IAction2Options>) {
|
|
super({
|
|
category: CHAT_CATEGORY,
|
|
...opts
|
|
});
|
|
}
|
|
|
|
run(accessor: ServicesAccessor, ...args: unknown[]) {
|
|
const context = getEditingSessionContext(accessor, args);
|
|
if (!context || !context.editingSession) {
|
|
return;
|
|
}
|
|
|
|
return this.runEditingSessionAction(accessor, context.editingSession, context.chatWidget, ...args);
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
abstract runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]): any;
|
|
}
|
|
|
|
export type EditingSessionActionContext = { editingSession?: IChatEditingSession; chatWidget: IChatWidget };
|
|
|
|
/**
|
|
* Resolve view title toolbar context. If none, return context from the lastFocusedWidget.
|
|
*/
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
export function getEditingSessionContext(accessor: ServicesAccessor, args: any[]): EditingSessionActionContext | undefined {
|
|
const arg0 = args.at(0);
|
|
const context = isChatViewTitleActionContext(arg0) ? arg0 : undefined;
|
|
|
|
const chatWidgetService = accessor.get(IChatWidgetService);
|
|
const chatEditingService = accessor.get(IChatEditingService);
|
|
let chatWidget = context ? chatWidgetService.getWidgetBySessionResource(context.sessionResource) : undefined;
|
|
if (!chatWidget) {
|
|
chatWidget = chatWidgetService.lastFocusedWidget ?? chatWidgetService.getWidgetsByLocations(ChatAgentLocation.Chat).find(w => w.supportsChangingModes);
|
|
}
|
|
|
|
if (!chatWidget?.viewModel) {
|
|
return;
|
|
}
|
|
|
|
const editingSession = chatEditingService.getEditingSession(chatWidget.viewModel.model.sessionResource);
|
|
return { editingSession, chatWidget };
|
|
}
|
|
|
|
|
|
abstract class WorkingSetAction extends EditingSessionAction {
|
|
|
|
runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]) {
|
|
|
|
const uris: URI[] = [];
|
|
if (URI.isUri(args[0])) {
|
|
uris.push(args[0]);
|
|
} else if (chatWidget) {
|
|
uris.push(...chatWidget.input.selectedElements);
|
|
}
|
|
if (!uris.length) {
|
|
return;
|
|
}
|
|
|
|
return this.runWorkingSetAction(accessor, editingSession, chatWidget, ...uris);
|
|
}
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
abstract runWorkingSetAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget | undefined, ...uris: URI[]): any;
|
|
}
|
|
|
|
registerAction2(class OpenFileInDiffAction extends WorkingSetAction {
|
|
constructor() {
|
|
super({
|
|
id: 'chatEditing.openFileInDiff',
|
|
title: localize2('open.fileInDiff', 'Open Changes in Diff Editor'),
|
|
icon: Codicon.diffSingle,
|
|
menu: [{
|
|
id: MenuId.ChatEditingWidgetModifiedFilesToolbar,
|
|
when: ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, ModifiedFileEntryState.Modified),
|
|
order: 2,
|
|
group: 'navigation'
|
|
}],
|
|
});
|
|
}
|
|
|
|
async runWorkingSetAction(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession, _chatWidget: IChatWidget, ...uris: URI[]): Promise<void> {
|
|
const editorService = accessor.get(IEditorService);
|
|
|
|
|
|
for (const uri of uris) {
|
|
|
|
let pane: IEditorPane | undefined = editorService.activeEditorPane;
|
|
if (!pane) {
|
|
pane = await editorService.openEditor({ resource: uri });
|
|
}
|
|
|
|
if (!pane) {
|
|
return;
|
|
}
|
|
|
|
const editedFile = currentEditingSession.getEntry(uri);
|
|
editedFile?.getEditorIntegration(pane).toggleDiff(undefined, true);
|
|
}
|
|
}
|
|
});
|
|
|
|
registerAction2(class AcceptAction extends WorkingSetAction {
|
|
constructor() {
|
|
super({
|
|
id: 'chatEditing.acceptFile',
|
|
title: localize2('accept.file', 'Keep'),
|
|
icon: Codicon.check,
|
|
menu: [{
|
|
when: ContextKeyExpr.and(ContextKeyExpr.equals('resourceScheme', CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME), ContextKeyExpr.notIn(chatEditingResourceContextKey.key, decidedChatEditingResourceContextKey.key)),
|
|
id: MenuId.MultiDiffEditorFileToolbar,
|
|
order: 0,
|
|
group: 'navigation',
|
|
}, {
|
|
id: MenuId.ChatEditingWidgetModifiedFilesToolbar,
|
|
when: ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, ModifiedFileEntryState.Modified),
|
|
order: 0,
|
|
group: 'navigation'
|
|
}],
|
|
});
|
|
}
|
|
|
|
async runWorkingSetAction(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession, chatWidget: IChatWidget, ...uris: URI[]): Promise<void> {
|
|
await currentEditingSession.accept(...uris);
|
|
}
|
|
});
|
|
|
|
registerAction2(class DiscardAction extends WorkingSetAction {
|
|
constructor() {
|
|
super({
|
|
id: 'chatEditing.discardFile',
|
|
title: localize2('discard.file', 'Undo'),
|
|
icon: Codicon.discard,
|
|
menu: [{
|
|
when: ContextKeyExpr.and(ContextKeyExpr.equals('resourceScheme', CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME), ContextKeyExpr.notIn(chatEditingResourceContextKey.key, decidedChatEditingResourceContextKey.key)),
|
|
id: MenuId.MultiDiffEditorFileToolbar,
|
|
order: 2,
|
|
group: 'navigation',
|
|
}, {
|
|
id: MenuId.ChatEditingWidgetModifiedFilesToolbar,
|
|
when: ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, ModifiedFileEntryState.Modified),
|
|
order: 1,
|
|
group: 'navigation'
|
|
}],
|
|
});
|
|
}
|
|
|
|
async runWorkingSetAction(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession, chatWidget: IChatWidget, ...uris: URI[]): Promise<void> {
|
|
await currentEditingSession.reject(...uris);
|
|
}
|
|
});
|
|
|
|
export class ChatEditingAcceptAllAction extends EditingSessionAction {
|
|
|
|
constructor() {
|
|
super({
|
|
id: 'chatEditing.acceptAllFiles',
|
|
title: localize('accept', 'Keep'),
|
|
icon: Codicon.check,
|
|
tooltip: localize('acceptAllEdits', 'Keep All Edits'),
|
|
precondition: hasUndecidedChatEditingResourceContextKey,
|
|
keybinding: {
|
|
primary: KeyMod.CtrlCmd | KeyCode.Enter,
|
|
when: ContextKeyExpr.and(hasUndecidedChatEditingResourceContextKey, ChatContextKeys.inChatInput),
|
|
weight: KeybindingWeight.WorkbenchContrib,
|
|
},
|
|
menu: [
|
|
|
|
{
|
|
id: MenuId.ChatEditingWidgetToolbar,
|
|
group: 'navigation',
|
|
order: 0,
|
|
when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), ContextKeyExpr.and(hasUndecidedChatEditingResourceContextKey))
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]) {
|
|
await editingSession.accept();
|
|
}
|
|
}
|
|
registerAction2(ChatEditingAcceptAllAction);
|
|
|
|
export class ChatEditingDiscardAllAction extends EditingSessionAction {
|
|
|
|
constructor() {
|
|
super({
|
|
id: 'chatEditing.discardAllFiles',
|
|
title: localize('discard', 'Undo'),
|
|
icon: Codicon.discard,
|
|
tooltip: localize('discardAllEdits', 'Undo All Edits'),
|
|
precondition: hasUndecidedChatEditingResourceContextKey,
|
|
menu: [
|
|
{
|
|
id: MenuId.ChatEditingWidgetToolbar,
|
|
group: 'navigation',
|
|
order: 1,
|
|
when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), hasUndecidedChatEditingResourceContextKey)
|
|
}
|
|
],
|
|
keybinding: {
|
|
when: ContextKeyExpr.and(hasUndecidedChatEditingResourceContextKey, ChatContextKeys.inChatInput, ChatContextKeys.inputHasText.negate()),
|
|
weight: KeybindingWeight.WorkbenchContrib,
|
|
primary: KeyMod.CtrlCmd | KeyCode.Backspace,
|
|
},
|
|
});
|
|
}
|
|
|
|
override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]) {
|
|
await discardAllEditsWithConfirmation(accessor, editingSession);
|
|
}
|
|
}
|
|
registerAction2(ChatEditingDiscardAllAction);
|
|
|
|
export class ToggleExplanationWidgetAction extends EditingSessionAction {
|
|
|
|
static readonly ID = 'chatEditing.toggleExplanationWidget';
|
|
|
|
constructor() {
|
|
super({
|
|
id: ToggleExplanationWidgetAction.ID,
|
|
title: localize('explainButton', 'Explain'),
|
|
tooltip: localize('toggleExplanationTooltip', 'Toggle Change Explanations'),
|
|
precondition: hasUndecidedChatEditingResourceContextKey,
|
|
menu: [
|
|
{
|
|
id: MenuId.ChatEditingWidgetToolbar,
|
|
group: 'navigation',
|
|
order: 2,
|
|
when: ContextKeyExpr.and(hasUndecidedChatEditingResourceContextKey, ContextKeyExpr.has(`config.${ChatConfiguration.ExplainChangesEnabled}`))
|
|
}
|
|
],
|
|
});
|
|
}
|
|
|
|
override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]) {
|
|
if (editingSession.hasExplanations()) {
|
|
editingSession.clearExplanations();
|
|
} else {
|
|
await editingSession.triggerExplanationGeneration();
|
|
}
|
|
}
|
|
}
|
|
registerAction2(ToggleExplanationWidgetAction);
|
|
|
|
export async function discardAllEditsWithConfirmation(accessor: ServicesAccessor, currentEditingSession: IChatEditingSession): Promise<boolean> {
|
|
|
|
const dialogService = accessor.get(IDialogService);
|
|
|
|
// Ask for confirmation if there are any edits
|
|
const entries = currentEditingSession.entries.get().filter(e => e.state.get() === ModifiedFileEntryState.Modified);
|
|
if (entries.length > 0) {
|
|
const confirmation = await dialogService.confirm({
|
|
title: localize('chat.editing.discardAll.confirmation.title', "Undo all edits?"),
|
|
message: entries.length === 1
|
|
? localize('chat.editing.discardAll.confirmation.oneFile', "This will undo changes made in {0}. Do you want to proceed?", basename(entries[0].modifiedURI))
|
|
: localize('chat.editing.discardAll.confirmation.manyFiles', "This will undo changes made in {0} files. Do you want to proceed?", entries.length),
|
|
primaryButton: localize('chat.editing.discardAll.confirmation.primaryButton', "Yes"),
|
|
type: 'info'
|
|
});
|
|
if (!confirmation.confirmed) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
await currentEditingSession.reject();
|
|
return true;
|
|
}
|
|
|
|
export class ChatEditingShowChangesAction extends EditingSessionAction {
|
|
static readonly ID = 'chatEditing.viewChanges';
|
|
static readonly LABEL = localize('chatEditing.viewChanges', 'View All Edits');
|
|
|
|
constructor() {
|
|
super({
|
|
id: ChatEditingShowChangesAction.ID,
|
|
title: { value: ChatEditingShowChangesAction.LABEL, original: ChatEditingShowChangesAction.LABEL },
|
|
tooltip: ChatEditingShowChangesAction.LABEL,
|
|
f1: true,
|
|
icon: Codicon.diffMultiple,
|
|
precondition: hasUndecidedChatEditingResourceContextKey,
|
|
menu: [
|
|
{
|
|
id: MenuId.ChatEditingWidgetToolbar,
|
|
group: 'navigation',
|
|
order: 4,
|
|
when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), ContextKeyExpr.and(hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey))
|
|
}
|
|
],
|
|
});
|
|
}
|
|
|
|
override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]): Promise<void> {
|
|
await editingSession.show();
|
|
}
|
|
}
|
|
registerAction2(ChatEditingShowChangesAction);
|
|
|
|
export class ViewAllSessionChangesAction extends Action2 {
|
|
static readonly ID = 'chatEditing.viewAllSessionChanges';
|
|
|
|
constructor() {
|
|
super({
|
|
id: ViewAllSessionChangesAction.ID,
|
|
title: localize2('chatEditing.viewAllSessionChanges', 'View All Changes'),
|
|
icon: Codicon.diffMultiple,
|
|
category: CHAT_CATEGORY,
|
|
precondition: ChatContextKeys.hasAgentSessionChanges,
|
|
menu: [
|
|
{
|
|
id: MenuId.ChatEditingSessionChangesToolbar,
|
|
group: 'navigation',
|
|
order: 10,
|
|
when: ChatContextKeys.hasAgentSessionChanges
|
|
},
|
|
{
|
|
id: MenuId.AgentSessionItemToolbar,
|
|
group: 'navigation',
|
|
order: 0,
|
|
when: ChatContextKeys.hasAgentSessionChanges
|
|
}
|
|
],
|
|
});
|
|
}
|
|
|
|
override async run(accessor: ServicesAccessor, sessionOrSessionResource?: URI | IAgentSession): Promise<void> {
|
|
const agentSessionsService = accessor.get(IAgentSessionsService);
|
|
const commandService = accessor.get(ICommandService);
|
|
const chatEditingService = accessor.get(IChatEditingService);
|
|
|
|
if (!URI.isUri(sessionOrSessionResource) && !isAgentSession(sessionOrSessionResource)) {
|
|
return;
|
|
}
|
|
|
|
const sessionResource = URI.isUri(sessionOrSessionResource)
|
|
? sessionOrSessionResource
|
|
: sessionOrSessionResource.resource;
|
|
|
|
const session = agentSessionsService.getSession(sessionResource);
|
|
const changes = session?.changes;
|
|
|
|
if (!session || !changes) {
|
|
return;
|
|
}
|
|
|
|
if (
|
|
session.providerType === AgentSessionProviders.Background ||
|
|
session.providerType === AgentSessionProviders.Cloud
|
|
) {
|
|
if (!Array.isArray(changes) || changes.length === 0) {
|
|
return;
|
|
}
|
|
|
|
// Use agent session changes
|
|
const resources = changes.map(d => ({
|
|
originalUri: d.originalUri,
|
|
modifiedUri: d.modifiedUri
|
|
}));
|
|
|
|
await commandService.executeCommand('_workbench.openMultiDiffEditor', {
|
|
multiDiffSourceUri: sessionResource.with({ scheme: sessionResource.scheme + '-worktree-changes' }),
|
|
title: localize('chatEditing.allChanges.title', 'All Session Changes'),
|
|
resources,
|
|
});
|
|
|
|
session?.setRead(true);
|
|
return;
|
|
}
|
|
|
|
// Use edit session changes
|
|
const editingSession = chatEditingService.getEditingSession(sessionResource);
|
|
await editingSession?.show();
|
|
session?.setRead(true);
|
|
}
|
|
}
|
|
registerAction2(ViewAllSessionChangesAction);
|
|
|
|
async function restoreSnapshotWithConfirmationByRequestId(accessor: ServicesAccessor, sessionResource: URI, requestId: string): Promise<void> {
|
|
const configurationService = accessor.get(IConfigurationService);
|
|
const dialogService = accessor.get(IDialogService);
|
|
const chatWidgetService = accessor.get(IChatWidgetService);
|
|
const widget = chatWidgetService.getWidgetBySessionResource(sessionResource);
|
|
const chatService = accessor.get(IChatService);
|
|
const chatModel = chatService.getSession(sessionResource);
|
|
if (!chatModel) {
|
|
return;
|
|
}
|
|
|
|
const session = chatModel.editingSession;
|
|
if (!session) {
|
|
return;
|
|
}
|
|
|
|
const chatRequests = chatModel.getRequests();
|
|
const itemIndex = chatRequests.findIndex(request => request.id === requestId);
|
|
if (itemIndex === -1) {
|
|
return;
|
|
}
|
|
|
|
const editsToUndo = chatRequests.length - itemIndex;
|
|
|
|
const requestsToRemove = chatRequests.slice(itemIndex);
|
|
const requestIdsToRemove = new Set(requestsToRemove.map(request => request.id));
|
|
const entriesModifiedInRequestsToRemove = session.entries.get().filter((entry) => requestIdsToRemove.has(entry.lastModifyingRequestId)) ?? [];
|
|
const shouldPrompt = entriesModifiedInRequestsToRemove.length > 0 && configurationService.getValue('chat.editing.confirmEditRequestRemoval') === true;
|
|
|
|
let message: string;
|
|
if (editsToUndo === 1) {
|
|
if (entriesModifiedInRequestsToRemove.length === 1) {
|
|
message = 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));
|
|
} else {
|
|
message = 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);
|
|
}
|
|
} else {
|
|
if (entriesModifiedInRequestsToRemove.length === 1) {
|
|
message = 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));
|
|
} else {
|
|
message = 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);
|
|
}
|
|
}
|
|
|
|
const confirmation = shouldPrompt
|
|
? await dialogService.confirm({
|
|
title: editsToUndo === 1
|
|
? localize('chat.removeLast.confirmation.title', "Do you want to undo your last edit?")
|
|
: localize('chat.remove.confirmation.title', "Do you want to undo {0} edits?", editsToUndo),
|
|
message: message,
|
|
primaryButton: localize('chat.remove.confirmation.primaryButton', "Yes"),
|
|
checkbox: { label: localize('chat.remove.confirmation.checkbox', "Don't ask again"), checked: false },
|
|
type: 'info'
|
|
})
|
|
: { confirmed: true };
|
|
|
|
if (!confirmation.confirmed) {
|
|
widget?.viewModel?.model.setCheckpoint(undefined);
|
|
return;
|
|
}
|
|
|
|
if (confirmation.checkboxChecked) {
|
|
await configurationService.updateValue('chat.editing.confirmEditRequestRemoval', false);
|
|
}
|
|
|
|
// Restore the snapshot to what it was before the request(s) that we deleted
|
|
const snapshotRequestId = chatRequests[itemIndex].id;
|
|
await session.restoreSnapshot(snapshotRequestId, undefined);
|
|
}
|
|
|
|
async function restoreSnapshotWithConfirmation(accessor: ServicesAccessor, item: ChatTreeItem): Promise<void> {
|
|
const requestId = isRequestVM(item) ? item.id :
|
|
isResponseVM(item) ? item.requestId : undefined;
|
|
|
|
if (!requestId) {
|
|
return;
|
|
}
|
|
|
|
await restoreSnapshotWithConfirmationByRequestId(accessor, item.sessionResource, requestId);
|
|
}
|
|
|
|
registerAction2(class RemoveAction extends Action2 {
|
|
constructor() {
|
|
super({
|
|
id: 'workbench.action.chat.undoEdits',
|
|
title: localize2('chat.undoEdits.label', "Undo Requests"),
|
|
f1: false,
|
|
category: CHAT_CATEGORY,
|
|
icon: Codicon.discard,
|
|
keybinding: {
|
|
primary: KeyCode.Delete,
|
|
mac: {
|
|
primary: KeyMod.CtrlCmd | KeyCode.Backspace,
|
|
},
|
|
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, EditorContextKeys.textInputFocus.negate()),
|
|
weight: KeybindingWeight.WorkbenchContrib,
|
|
},
|
|
menu: [
|
|
{
|
|
id: MenuId.ChatMessageTitle,
|
|
group: 'navigation',
|
|
order: 2,
|
|
when: ContextKeyExpr.and(ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'input').negate(), ContextKeyExpr.equals(`config.${ChatConfiguration.CheckpointsEnabled}`, false), ChatContextKeys.lockedToCodingAgent.negate()),
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
async run(accessor: ServicesAccessor, ...args: unknown[]) {
|
|
let item = args[0] as ChatTreeItem | undefined;
|
|
const chatWidgetService = accessor.get(IChatWidgetService);
|
|
const configurationService = accessor.get(IConfigurationService);
|
|
const widget = (isChatTreeItem(item) && chatWidgetService.getWidgetBySessionResource(item.sessionResource)) || chatWidgetService.lastFocusedWidget;
|
|
if (!isResponseVM(item) && !isRequestVM(item)) {
|
|
item = widget?.getFocus();
|
|
}
|
|
|
|
if (!item) {
|
|
return;
|
|
}
|
|
|
|
await restoreSnapshotWithConfirmation(accessor, item);
|
|
|
|
if (isRequestVM(item) && configurationService.getValue('chat.undoRequests.restoreInput')) {
|
|
widget?.focusInput();
|
|
widget?.input.setValue(item.messageText, false);
|
|
}
|
|
}
|
|
});
|
|
|
|
registerAction2(class RestoreCheckpointAction extends Action2 {
|
|
constructor() {
|
|
super({
|
|
id: 'workbench.action.chat.restoreCheckpoint',
|
|
title: localize2('chat.restoreCheckpoint.label', "Restore Checkpoint"),
|
|
tooltip: localize2('chat.restoreCheckpoint.tooltip', "Restores workspace and chat to this point"),
|
|
f1: false,
|
|
category: CHAT_CATEGORY,
|
|
keybinding: {
|
|
primary: KeyCode.Delete,
|
|
mac: {
|
|
primary: KeyMod.CtrlCmd | KeyCode.Backspace,
|
|
},
|
|
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, EditorContextKeys.textInputFocus.negate()),
|
|
weight: KeybindingWeight.WorkbenchContrib,
|
|
},
|
|
menu: [
|
|
{
|
|
id: MenuId.ChatMessageCheckpoint,
|
|
group: 'navigation',
|
|
order: 2,
|
|
when: ContextKeyExpr.and(ChatContextKeys.isRequest, ChatContextKeys.lockedToCodingAgent.negate())
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
async run(accessor: ServicesAccessor, ...args: unknown[]) {
|
|
let item = args[0] as ChatTreeItem | undefined;
|
|
const chatWidgetService = accessor.get(IChatWidgetService);
|
|
const widget = (isChatTreeItem(item) && chatWidgetService.getWidgetBySessionResource(item.sessionResource)) || chatWidgetService.lastFocusedWidget;
|
|
if (!isResponseVM(item) && !isRequestVM(item)) {
|
|
item = widget?.getFocus();
|
|
}
|
|
|
|
if (!item) {
|
|
return;
|
|
}
|
|
|
|
if (isRequestVM(item)) {
|
|
widget?.focusInput();
|
|
widget?.input.setValue(item.messageText, false);
|
|
}
|
|
|
|
widget?.viewModel?.model.setCheckpoint(item.id);
|
|
await restoreSnapshotWithConfirmation(accessor, item);
|
|
}
|
|
});
|
|
|
|
registerAction2(class RestoreLastCheckpoint extends Action2 {
|
|
constructor() {
|
|
super({
|
|
id: 'workbench.action.chat.restoreLastCheckpoint',
|
|
title: localize2('chat.restoreLastCheckpoint.label', "Restore to Last Checkpoint"),
|
|
f1: true,
|
|
category: CHAT_CATEGORY,
|
|
icon: Codicon.discard,
|
|
precondition: ContextKeyExpr.and(
|
|
ChatContextKeys.inChatSession,
|
|
ContextKeyExpr.equals(`config.${ChatConfiguration.CheckpointsEnabled}`, true),
|
|
ChatContextKeys.lockedToCodingAgent.negate()
|
|
),
|
|
menu: [
|
|
{
|
|
id: MenuId.ChatMessageFooter,
|
|
group: 'navigation',
|
|
order: 1,
|
|
when: ContextKeyExpr.and(ContextKeyExpr.in(ChatContextKeys.itemId.key, ChatContextKeys.lastItemId.key), ContextKeyExpr.equals(`config.${ChatConfiguration.CheckpointsEnabled}`, true), ChatContextKeys.lockedToCodingAgent.negate()),
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
async run(accessor: ServicesAccessor, ...args: unknown[]) {
|
|
let item = args[0] as ChatTreeItem | undefined;
|
|
const chatWidgetService = accessor.get(IChatWidgetService);
|
|
const chatService = accessor.get(IChatService);
|
|
const widget = (isChatTreeItem(item) && chatWidgetService.getWidgetBySessionResource(item.sessionResource)) || chatWidgetService.lastFocusedWidget;
|
|
if (!isResponseVM(item) && !isRequestVM(item)) {
|
|
item = widget?.getFocus();
|
|
}
|
|
|
|
const sessionResource = widget?.viewModel?.sessionResource ?? (isChatTreeItem(item) ? item.sessionResource : undefined);
|
|
if (!sessionResource) {
|
|
return;
|
|
}
|
|
|
|
const chatModel = chatService.getSession(sessionResource);
|
|
if (!chatModel?.editingSession) {
|
|
return;
|
|
}
|
|
|
|
const checkpointRequest = chatModel.checkpoint;
|
|
if (!checkpointRequest) {
|
|
alert(localize('chat.restoreCheckpoint.none', 'There is no checkpoint to restore.'));
|
|
return;
|
|
}
|
|
|
|
widget?.viewModel?.model.setCheckpoint(checkpointRequest.id);
|
|
widget?.focusInput();
|
|
widget?.input.setValue(checkpointRequest.message.text, false);
|
|
|
|
await restoreSnapshotWithConfirmationByRequestId(accessor, sessionResource, checkpointRequest.id);
|
|
}
|
|
});
|
|
|
|
registerAction2(class EditAction extends Action2 {
|
|
constructor() {
|
|
super({
|
|
id: 'workbench.action.chat.editRequests',
|
|
title: localize2('chat.editRequests.label', "Edit Request"),
|
|
f1: false,
|
|
category: CHAT_CATEGORY,
|
|
icon: Codicon.edit,
|
|
keybinding: {
|
|
primary: KeyCode.Enter,
|
|
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, EditorContextKeys.textInputFocus.negate()),
|
|
weight: KeybindingWeight.WorkbenchContrib,
|
|
},
|
|
menu: [
|
|
{
|
|
id: MenuId.ChatMessageTitle,
|
|
group: 'navigation',
|
|
order: 2,
|
|
when: ContextKeyExpr.and(ContextKeyExpr.or(ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'hover'), ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'input')))
|
|
}
|
|
]
|
|
});
|
|
}
|
|
|
|
async run(accessor: ServicesAccessor, ...args: unknown[]) {
|
|
let item = args[0] as ChatTreeItem | undefined;
|
|
const chatWidgetService = accessor.get(IChatWidgetService);
|
|
const widget = (isChatTreeItem(item) && chatWidgetService.getWidgetBySessionResource(item.sessionResource)) || chatWidgetService.lastFocusedWidget;
|
|
if (!isResponseVM(item) && !isRequestVM(item)) {
|
|
item = widget?.getFocus();
|
|
}
|
|
|
|
if (!item) {
|
|
return;
|
|
}
|
|
|
|
if (isRequestVM(item)) {
|
|
widget?.startEditing(item.id);
|
|
}
|
|
}
|
|
});
|
|
|
|
export interface ChatEditingActionContext {
|
|
readonly sessionResource: URI;
|
|
readonly requestId: string;
|
|
readonly uri: URI;
|
|
readonly stopId: string | undefined;
|
|
}
|
|
|
|
registerAction2(class OpenWorkingSetHistoryAction extends Action2 {
|
|
|
|
static readonly id = 'chat.openFileUpdatedBySnapshot';
|
|
constructor() {
|
|
super({
|
|
id: OpenWorkingSetHistoryAction.id,
|
|
title: localize('chat.openFileUpdatedBySnapshot.label', "Open File"),
|
|
menu: [{
|
|
id: MenuId.ChatEditingCodeBlockContext,
|
|
group: 'navigation',
|
|
order: 0,
|
|
},]
|
|
});
|
|
}
|
|
|
|
override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise<void> {
|
|
const context = args[0] as ChatEditingActionContext | undefined;
|
|
if (!context?.sessionResource) {
|
|
return;
|
|
}
|
|
|
|
const editorService = accessor.get(IEditorService);
|
|
await editorService.openEditor({ resource: context.uri });
|
|
}
|
|
});
|
|
|
|
registerAction2(class OpenWorkingSetHistoryAction extends Action2 {
|
|
|
|
static readonly id = 'chat.openFileSnapshot';
|
|
constructor() {
|
|
super({
|
|
id: OpenWorkingSetHistoryAction.id,
|
|
title: localize('chat.openSnapshot.label', "Open File Snapshot"),
|
|
menu: [{
|
|
id: MenuId.ChatEditingCodeBlockContext,
|
|
group: 'navigation',
|
|
order: 1,
|
|
},]
|
|
});
|
|
}
|
|
|
|
override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise<void> {
|
|
const context = args[0] as ChatEditingActionContext | undefined;
|
|
if (!context?.sessionResource) {
|
|
return;
|
|
}
|
|
|
|
const chatService = accessor.get(IChatService);
|
|
const chatEditingService = accessor.get(IChatEditingService);
|
|
const editorService = accessor.get(IEditorService);
|
|
|
|
const chatModel = chatService.getSession(context.sessionResource);
|
|
if (!chatModel) {
|
|
return;
|
|
}
|
|
|
|
const snapshot = chatEditingService.getEditingSession(chatModel.sessionResource)?.getSnapshotUri(context.requestId, context.uri, context.stopId);
|
|
if (snapshot) {
|
|
const editor = await editorService.openEditor({ resource: snapshot, label: localize('chatEditing.snapshot', '{0} (Snapshot)', basename(context.uri)), options: { activation: EditorActivation.ACTIVATE } });
|
|
if (isCodeEditor(editor)) {
|
|
editor.updateOptions({ readOnly: true });
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
registerAction2(class ResolveSymbolsContextAction extends EditingSessionAction {
|
|
constructor() {
|
|
super({
|
|
id: 'workbench.action.edits.addFilesFromReferences',
|
|
title: localize2('addFilesFromReferences', "Add Files From References"),
|
|
f1: false,
|
|
category: CHAT_CATEGORY,
|
|
menu: {
|
|
id: MenuId.ChatInputSymbolAttachmentContext,
|
|
group: 'navigation',
|
|
order: 1,
|
|
when: ContextKeyExpr.and(ChatContextKeys.chatModeKind.isEqualTo(ChatModeKind.Ask), EditorContextKeys.hasReferenceProvider)
|
|
}
|
|
});
|
|
}
|
|
|
|
override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]): Promise<void> {
|
|
if (args.length === 0 || !isLocation(args[0])) {
|
|
return;
|
|
}
|
|
|
|
const textModelService = accessor.get(ITextModelService);
|
|
const languageFeaturesService = accessor.get(ILanguageFeaturesService);
|
|
const symbol = args[0] as Location;
|
|
|
|
const modelReference = await textModelService.createModelReference(symbol.uri);
|
|
const textModel = modelReference.object.textEditorModel;
|
|
if (!textModel) {
|
|
return;
|
|
}
|
|
|
|
const position = new Position(symbol.range.startLineNumber, symbol.range.startColumn);
|
|
|
|
const [references, definitions, implementations] = await Promise.all([
|
|
this.getReferences(position, textModel, languageFeaturesService),
|
|
this.getDefinitions(position, textModel, languageFeaturesService),
|
|
this.getImplementations(position, textModel, languageFeaturesService)
|
|
]);
|
|
|
|
// Sort the references, definitions and implementations by
|
|
// how important it is that they make it into the working set as it has limited size
|
|
const attachments = [];
|
|
for (const reference of [...definitions, ...implementations, ...references]) {
|
|
attachments.push(chatWidget.attachmentModel.asFileVariableEntry(reference.uri));
|
|
}
|
|
|
|
chatWidget.attachmentModel.addContext(...attachments);
|
|
}
|
|
|
|
private async getReferences(position: Position, textModel: ITextModel, languageFeaturesService: ILanguageFeaturesService): Promise<Location[]> {
|
|
const referenceProviders = languageFeaturesService.referenceProvider.all(textModel);
|
|
|
|
const references = await Promise.all(referenceProviders.map(async (referenceProvider) => {
|
|
return await referenceProvider.provideReferences(textModel, position, { includeDeclaration: true }, CancellationToken.None) ?? [];
|
|
}));
|
|
|
|
return references.flat();
|
|
}
|
|
|
|
private async getDefinitions(position: Position, textModel: ITextModel, languageFeaturesService: ILanguageFeaturesService): Promise<Location[]> {
|
|
const definitionProviders = languageFeaturesService.definitionProvider.all(textModel);
|
|
|
|
const definitions = await Promise.all(definitionProviders.map(async (definitionProvider) => {
|
|
return await definitionProvider.provideDefinition(textModel, position, CancellationToken.None) ?? [];
|
|
}));
|
|
|
|
return definitions.flat();
|
|
}
|
|
|
|
private async getImplementations(position: Position, textModel: ITextModel, languageFeaturesService: ILanguageFeaturesService): Promise<Location[]> {
|
|
const implementationProviders = languageFeaturesService.implementationProvider.all(textModel);
|
|
|
|
const implementations = await Promise.all(implementationProviders.map(async (implementationProvider) => {
|
|
return await implementationProvider.provideImplementation(textModel, position, CancellationToken.None) ?? [];
|
|
}));
|
|
|
|
return implementations.flat();
|
|
}
|
|
});
|
|
|
|
export class ViewPreviousEditsAction extends EditingSessionAction {
|
|
static readonly Id = 'chatEditing.viewPreviousEdits';
|
|
static readonly Label = localize('chatEditing.viewPreviousEdits', 'View Previous Edits');
|
|
|
|
constructor() {
|
|
super({
|
|
id: ViewPreviousEditsAction.Id,
|
|
title: { value: ViewPreviousEditsAction.Label, original: ViewPreviousEditsAction.Label },
|
|
tooltip: ViewPreviousEditsAction.Label,
|
|
f1: true,
|
|
icon: Codicon.diffMultiple,
|
|
precondition: ContextKeyExpr.and(ChatContextKeys.enabled, hasUndecidedChatEditingResourceContextKey.negate()),
|
|
menu: [
|
|
{
|
|
id: MenuId.ChatEditingWidgetToolbar,
|
|
group: 'navigation',
|
|
order: 4,
|
|
when: ContextKeyExpr.and(applyingChatEditsFailedContextKey.negate(), ContextKeyExpr.and(hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey.negate()))
|
|
}
|
|
],
|
|
});
|
|
}
|
|
|
|
override async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession, chatWidget: IChatWidget, ...args: unknown[]): Promise<void> {
|
|
await editingSession.show(true);
|
|
}
|
|
}
|
|
registerAction2(ViewPreviousEditsAction);
|
|
|
|
/**
|
|
* Workbench command to explore accepting working set changes from an extension. Executing
|
|
* the command will accept the changes for the provided resources across all edit sessions.
|
|
*/
|
|
CommandsRegistry.registerCommand('_chat.editSessions.accept', async (accessor: ServicesAccessor, resources: UriComponents[]) => {
|
|
if (resources.length === 0) {
|
|
return;
|
|
}
|
|
|
|
const uris = resources.map(resource => URI.revive(resource));
|
|
const chatEditingService = accessor.get(IChatEditingService);
|
|
for (const editingSession of chatEditingService.editingSessionsObs.get()) {
|
|
await editingSession.accept(...uris);
|
|
}
|
|
});
|