Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatNewActions.ts
5297 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { Codicon } from '../../../../../base/common/codicons.js';6import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';7import { URI } from '../../../../../base/common/uri.js';8import { generateUuid } from '../../../../../base/common/uuid.js';9import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';10import { localize, localize2 } from '../../../../../nls.js';11import { IAccessibilityService } from '../../../../../platform/accessibility/common/accessibility.js';12import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../../platform/actions/common/actions.js';13import { CommandsRegistry } from '../../../../../platform/commands/common/commands.js';14import { ContextKeyExpr } from '../../../../../platform/contextkey/common/contextkey.js';15import { IDialogService } from '../../../../../platform/dialogs/common/dialogs.js';16import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';17import { ActiveEditorContext } from '../../../../common/contextkeys.js';18import { IViewsService } from '../../../../services/views/common/viewsService.js';19import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';20import { IChatEditingSession } from '../../common/editing/chatEditingService.js';21import { IChatService } from '../../common/chatService/chatService.js';22import { localChatSessionType } from '../../common/chatSessionsService.js';23import { ChatAgentLocation, ChatConfiguration, ChatModeKind } from '../../common/constants.js';24import { getChatSessionType, LocalChatSessionUri } from '../../common/model/chatUri.js';25import { ChatViewId, IChatWidgetService, isIChatViewViewContext } from '../chat.js';26import { EditingSessionAction, EditingSessionActionContext, getEditingSessionContext } from '../chatEditing/chatEditingActions.js';27import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js';28import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js';29import { ACTION_ID_NEW_CHAT, ACTION_ID_NEW_EDIT_SESSION, CHAT_CATEGORY, handleCurrentEditingSession } from './chatActions.js';30import { clearChatEditor } from './chatClear.js';31import { AgentSessionProviders, AgentSessionsViewerOrientation } from '../agentSessions/agentSessions.js';32import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';3334export interface INewEditSessionActionContext {3536/**37* An initial prompt to write to the chat.38*/39inputValue?: string;4041/**42* Selects opening in agent mode or not. If not set, the current mode is used.43* This is ignored when coming from a chat view title context.44*/45agentMode?: boolean;4647/**48* Whether the inputValue is partial and should wait for further user input.49* If false or not set, the prompt is sent immediately.50*/51isPartialQuery?: boolean;52}5354function isNewEditSessionActionContext(arg: unknown): arg is INewEditSessionActionContext {55if (arg && typeof arg === 'object') {56const obj = arg as Record<string, unknown>;57if (obj.inputValue !== undefined && typeof obj.inputValue !== 'string') {58return false;59}60if (obj.agentMode !== undefined && typeof obj.agentMode !== 'boolean') {61return false;62}63if (obj.isPartialQuery !== undefined && typeof obj.isPartialQuery !== 'boolean') {64return false;65}66return true;67}68return false;69}7071export function registerNewChatActions() {7273// Add "New Chat" submenu to Chat view menu74MenuRegistry.appendMenuItem(MenuId.ViewTitle, {75submenu: MenuId.ChatNewMenu,76title: localize2('chat.newEdits.label', "New Chat"),77icon: Codicon.plus,78when: ContextKeyExpr.equals('view', ChatViewId),79group: 'navigation',80order: -1,81isSplitButton: true82});8384registerAction2(class NewChatEditorAction extends Action2 {85constructor() {86super({87id: 'workbench.action.chatEditor.newChat',88title: localize2('chat.newChat.label', "New Chat"),89icon: Codicon.plus,90f1: false,91precondition: ChatContextKeys.enabled,92});93}94async run(accessor: ServicesAccessor, ...args: unknown[]) {95await clearChatEditor(accessor);96}97});9899registerAction2(class NewChatAction extends Action2 {100constructor() {101super({102id: ACTION_ID_NEW_CHAT,103title: localize2('chat.newEdits.label', "New Chat"),104category: CHAT_CATEGORY,105icon: Codicon.plus,106precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat)),107f1: true,108menu: [109{110id: MenuId.ChatContext,111group: 'z_clear'112},113{114id: MenuId.ChatNewMenu,115group: '1_open',116order: 1,117},118{119id: MenuId.CompactWindowEditorTitle,120group: 'navigation',121when: ActiveEditorContext.isEqualTo(ChatEditorInput.EditorID),122order: 1123}124],125keybinding: {126weight: KeybindingWeight.WorkbenchContrib + 1,127primary: KeyMod.CtrlCmd | KeyCode.KeyN,128secondary: [KeyMod.CtrlCmd | KeyCode.KeyL],129mac: {130primary: KeyMod.CtrlCmd | KeyCode.KeyN,131secondary: [KeyMod.WinCtrl | KeyCode.KeyL]132},133when: ChatContextKeys.inChatSession134}135});136}137138async run(accessor: ServicesAccessor, ...args: unknown[]) {139const executeCommandContext = isNewEditSessionActionContext(args[0]) ? args[0] : undefined;140141// Context from toolbar or lastFocusedWidget142const context = getEditingSessionContext(accessor, args);143await runNewChatAction(accessor, context, executeCommandContext);144}145}146);147CommandsRegistry.registerCommandAlias(ACTION_ID_NEW_EDIT_SESSION, ACTION_ID_NEW_CHAT);148149registerAction2(class NewLocalChatAction extends Action2 {150constructor() {151super({152id: 'workbench.action.chat.newLocalChat',153title: localize2('chat.newLocalChat.label', "New Local Chat"),154category: CHAT_CATEGORY,155icon: Codicon.plus,156precondition: ContextKeyExpr.and(ChatContextKeys.enabled, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat)),157f1: false,158});159}160161async run(accessor: ServicesAccessor, ...args: unknown[]) {162const executeCommandContext = isNewEditSessionActionContext(args[0]) ? args[0] : undefined;163164// Context from toolbar or lastFocusedWidget165const context = getEditingSessionContext(accessor, args);166await runNewChatAction(accessor, context, executeCommandContext, AgentSessionProviders.Local);167}168});169170MenuRegistry.appendMenuItem(MenuId.ChatViewSessionTitleNavigationToolbar, {171command: {172id: ACTION_ID_NEW_CHAT,173title: localize2('chat.goBack', "Go Back"),174icon: Codicon.arrowLeft,175},176when: ChatContextKeys.agentSessionsViewerOrientation.notEqualsTo(AgentSessionsViewerOrientation.SideBySide), // when sessions show side by side, no need for a back button177group: 'navigation',178order: 1179});180181registerAction2(class UndoChatEditInteractionAction extends EditingSessionAction {182constructor() {183super({184id: 'workbench.action.chat.undoEdit',185title: localize2('chat.undoEdit.label', "Undo Last Edit"),186category: CHAT_CATEGORY,187icon: Codicon.discard,188precondition: ContextKeyExpr.and(ChatContextKeys.chatEditingCanUndo, ChatContextKeys.enabled),189f1: true,190menu: [{191id: MenuId.ViewTitle,192when: ContextKeyExpr.equals('view', ChatViewId),193group: 'navigation',194order: -3,195isHiddenByDefault: true196}]197});198}199200async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession) {201await editingSession.undoInteraction();202}203});204205registerAction2(class RedoChatEditInteractionAction extends EditingSessionAction {206constructor() {207super({208id: 'workbench.action.chat.redoEdit',209title: localize2('chat.redoEdit.label', "Redo Last Edit"),210category: CHAT_CATEGORY,211icon: Codicon.redo,212precondition: ContextKeyExpr.and(ChatContextKeys.chatEditingCanRedo, ChatContextKeys.enabled),213f1: true,214menu: [215{216id: MenuId.ViewTitle,217when: ContextKeyExpr.equals('view', ChatViewId),218group: 'navigation',219order: -2,220isHiddenByDefault: true221}222]223});224}225226async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession) {227const chatService = accessor.get(IChatService);228await editingSession.redoInteraction();229chatService.getSession(editingSession.chatSessionResource)?.setCheckpoint(undefined);230}231});232233registerAction2(class RedoChatCheckpoints extends EditingSessionAction {234constructor() {235super({236id: 'workbench.action.chat.redoEdit2',237title: localize2('chat.redoEdit.label2', "Redo"),238tooltip: localize2('chat.redoEdit.tooltip', "Reapply discarded workspace changes and chat"),239category: CHAT_CATEGORY,240precondition: ContextKeyExpr.and(ChatContextKeys.chatEditingCanRedo, ChatContextKeys.enabled),241f1: true,242menu: [{243id: MenuId.ChatMessageRestoreCheckpoint,244when: ChatContextKeys.lockedToCodingAgent.negate(),245group: 'navigation',246order: -1247}]248});249}250251async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession) {252const widget = accessor.get(IChatWidgetService);253254while (editingSession.canRedo.get()) {255await editingSession.redoInteraction();256}257258const currentWidget = widget.getWidgetBySessionResource(editingSession.chatSessionResource);259const requestText = currentWidget?.viewModel?.model.checkpoint?.message.text;260261// if the input has the same text that we just restored, clear it.262if (currentWidget?.inputEditor.getValue() === requestText) {263currentWidget?.input.setValue('', false);264}265266currentWidget?.viewModel?.model.setCheckpoint(undefined);267currentWidget?.focusInput();268}269});270}271272/**273* Creates a new session resource URI with the specified session type.274* For remote sessions, creates a URI with the session type as the scheme.275* For local sessions, creates a LocalChatSessionUri.276*/277function getResourceForNewChatSession(sessionType: string): URI {278const isRemoteSession = sessionType !== localChatSessionType;279if (isRemoteSession) {280return URI.from({281scheme: sessionType,282path: `/untitled-${generateUuid()}`,283});284}285286return LocalChatSessionUri.forSession(generateUuid());287}288289async function runNewChatAction(290accessor: ServicesAccessor,291context: EditingSessionActionContext | undefined,292executeCommandContext?: INewEditSessionActionContext,293sessionType?: AgentSessionProviders294) {295const accessibilityService = accessor.get(IAccessibilityService);296const viewsService = accessor.get(IViewsService);297const configurationService = accessor.get(IConfigurationService);298299const { editingSession, chatWidget: widget } = context ?? {};300if (!widget) {301return;302}303304const dialogService = accessor.get(IDialogService);305306const model = widget.viewModel?.model;307if (model && !(await handleCurrentEditingSession(model, undefined, dialogService))) {308return;309}310311await editingSession?.stop();312313// Create a new session with the same type as the current session314const currentResource = widget.viewModel?.model.sessionResource;315const newSessionType = sessionType ?? (currentResource ? getChatSessionType(currentResource) : localChatSessionType);316if (isIChatViewViewContext(widget.viewContext) && newSessionType !== localChatSessionType) {317// For the sidebar, we need to explicitly load a session with the same type318const newResource = getResourceForNewChatSession(newSessionType);319const view = await viewsService.openView(ChatViewId) as ChatViewPane;320await view.loadSession(newResource);321} else {322// For the editor, widget.clear() already preserves the session type via clearChatEditor323await widget.clear();324}325326widget.attachmentModel.clear(true);327widget.focusInput();328329accessibilityService.alert(localize('newChat', "New chat"));330331if (!executeCommandContext) {332return;333}334335if (typeof executeCommandContext.agentMode === 'boolean') {336widget.input.setChatMode(executeCommandContext.agentMode ? ChatModeKind.Agent : ChatModeKind.Edit);337} else if (widget.input.currentModeKind === ChatModeKind.Edit && configurationService.getValue<boolean>(ChatConfiguration.EditModeHidden)) {338widget.input.setChatMode(ChatModeKind.Agent);339}340341if (executeCommandContext.inputValue) {342if (executeCommandContext.isPartialQuery) {343widget.setInput(executeCommandContext.inputValue);344} else {345widget.acceptInput(executeCommandContext.inputValue);346}347}348}349350351