From da96c730fff3b710b2883f3c11e22b715e84504e Mon Sep 17 00:00:00 2001 From: Martin Aeschlimann Date: Fri, 27 Sep 2024 19:42:26 +0200 Subject: [PATCH] chat editing: new button (#229988) chat editing: add new button --- .../chat/browser/actions/chatClearActions.ts | 54 +++++++++- src/vs/workbench/contrib/chat/browser/chat.ts | 6 ++ .../chat/browser/chatEditingService.ts | 98 +++---------------- .../contrib/chat/browser/chatWidget.ts | 29 ++++-- .../contrib/chat/common/chatEditingService.ts | 2 +- 5 files changed, 91 insertions(+), 98 deletions(-) diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts index 618d2b5a97912..cb5c64f760b2e 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts @@ -14,14 +14,16 @@ import { KeybindingWeight } from '../../../../../platform/keybinding/common/keyb import { ActiveEditorContext } from '../../../../common/contextkeys.js'; import { CHAT_CATEGORY, isChatViewTitleActionContext } from './chatActions.js'; import { clearChatEditor } from './chatClear.js'; -import { CHAT_VIEW_ID, IChatWidgetService } from '../chat.js'; +import { CHAT_VIEW_ID, EDITS_VIEW_ID, IChatWidgetService } from '../chat.js'; import { ChatEditorInput } from '../chatEditorInput.js'; import { ChatViewPane } from '../chatViewPane.js'; -import { CONTEXT_IN_CHAT_SESSION, CONTEXT_CHAT_ENABLED } from '../../common/chatContextKeys.js'; +import { CONTEXT_IN_CHAT_SESSION, CONTEXT_CHAT_ENABLED, CONTEXT_CHAT_EDITING_PARTICIPANT_REGISTERED } from '../../common/chatContextKeys.js'; import { IViewsService } from '../../../../services/views/common/viewsService.js'; export const ACTION_ID_NEW_CHAT = `workbench.action.chat.newChat`; +export const ACTION_ID_NEW_EDIT_SESSION = `workbench.action.chat.newEditSession`; + export function registerNewChatActions() { registerAction2(class NewChatEditorAction extends Action2 { constructor() { @@ -100,6 +102,54 @@ export function registerNewChatActions() { } } }); + + registerAction2(class GlobalClearEditsAction extends Action2 { + constructor() { + super({ + id: ACTION_ID_NEW_EDIT_SESSION, + title: localize2('chat.newEdits.label', "New Edit Session"), + category: CHAT_CATEGORY, + icon: Codicon.plus, + precondition: ContextKeyExpr.and(CONTEXT_CHAT_ENABLED, CONTEXT_CHAT_EDITING_PARTICIPANT_REGISTERED), + f1: true, + menu: [{ + id: MenuId.ChatContext, + group: 'z_clear' + }, + { + id: MenuId.ViewTitle, + when: ContextKeyExpr.equals('view', EDITS_VIEW_ID), + group: 'navigation', + order: -1 + }] + }); + } + + async run(accessor: ServicesAccessor, ...args: any[]) { + const context = args[0]; + const accessibilitySignalService = accessor.get(IAccessibilitySignalService); + if (isChatViewTitleActionContext(context)) { + // Is running in the Chat view title + announceChatCleared(accessibilitySignalService); + context.chatView.widget.clear(); + context.chatView.widget.focusInput(); + } else { + // Is running from f1 or keybinding + const widgetService = accessor.get(IChatWidgetService); + const viewsService = accessor.get(IViewsService); + + let widget = widgetService.lastFocusedWidget; + if (!widget) { + const chatView = await viewsService.openView(EDITS_VIEW_ID) as ChatViewPane; + widget = chatView.widget; + } + + announceChatCleared(accessibilitySignalService); + widget.clear(); + widget.focusInput(); + } + } + }); } function announceChatCleared(accessibilitySignalService: IAccessibilitySignalService): void { diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index 62943602abcf7..cab43391c45ef 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -43,6 +43,10 @@ export async function showChatView(viewsService: IViewsService): Promise(CHAT_VIEW_ID))?.widget; } +export async function showEditsView(viewsService: IViewsService): Promise { + return (await viewsService.openView(EDITS_VIEW_ID))?.widget; +} + export const IQuickChatService = createDecorator('quickChatService'); export interface IQuickChatService { readonly _serviceBrand: undefined; @@ -197,3 +201,5 @@ export interface IChatCodeBlockContextProviderService { export const GeneratingPhrase = localize('generating', "Generating"); export const CHAT_VIEW_ID = `workbench.panel.chat.view.${CHAT_PROVIDER_ID}`; + +export const EDITS_VIEW_ID = 'workbench.panel.chat.view.edits'; diff --git a/src/vs/workbench/contrib/chat/browser/chatEditingService.ts b/src/vs/workbench/contrib/chat/browser/chatEditingService.ts index 2df4a1d6fdee2..3321419dba3a0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditingService.ts @@ -2,15 +2,15 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. See License.txt in the project root for license information. *--------------------------------------------------------------------------------------------*/ + import { Sequencer } from '../../../../base/common/async.js'; +import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { BugIndicatingError } from '../../../../base/common/errors.js'; import { Emitter } from '../../../../base/common/event.js'; import { Disposable, DisposableStore, IDisposable, IReference, MutableDisposable } from '../../../../base/common/lifecycle.js'; -import { Schemas } from '../../../../base/common/network.js'; import { derived, IObservable, ITransaction, observableValue, ValueWithChangeEventFromObservable } from '../../../../base/common/observable.js'; import { URI } from '../../../../base/common/uri.js'; -import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js'; import { IBulkEditService } from '../../../../editor/browser/services/bulkEditService.js'; import { TextEdit } from '../../../../editor/common/languages.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; @@ -29,20 +29,14 @@ import { DiffEditorInput } from '../../../common/editor/diffEditorInput.js'; import { EditorInput } from '../../../common/editor/editorInput.js'; import { IEditorGroup, IEditorGroupsService } from '../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; -import { IViewsService } from '../../../services/views/common/viewsService.js'; import { MultiDiffEditor } from '../../multiDiffEditor/browser/multiDiffEditor.js'; import { MultiDiffEditorInput } from '../../multiDiffEditor/browser/multiDiffEditorInput.js'; import { IMultiDiffSourceResolver, IMultiDiffSourceResolverService, IResolvedMultiDiffSource, MultiDiffEditorItem } from '../../multiDiffEditor/browser/multiDiffSourceResolverService.js'; -import { ChatAgentLocation } from '../common/chatAgents.js'; -import { CONTEXT_CHAT_EDITING_ENABLED, CONTEXT_CHAT_ENABLED } from '../common/chatContextKeys.js'; +import { ICodeMapperResponse, ICodeMapperService } from '../common/chatCodeMapperService.js'; import { ChatEditingSessionState, IChatEditingService, IChatEditingSession, IChatEditingSessionStream, IModifiedFileEntry, ModifiedFileEntryState } from '../common/chatEditingService.js'; import { IChatResponseModel } from '../common/chatModel.js'; import { IChatService } from '../common/chatService.js'; -import { IChatVariablesService } from '../common/chatVariables.js'; -import { CHAT_CATEGORY } from './actions/chatActions.js'; -import { CHAT_VIEW_ID, IChatWidgetService, showChatView } from './chat.js'; -import { ICodeMapperResponse, ICodeMapperService } from '../common/chatCodeMapperService.js'; -import { CancellationToken, CancellationTokenSource } from '../../../../base/common/cancellation.js'; +import { IChatWidgetService } from './chat.js'; const decidedChatEditingResourceContextKey = new RawContextKey('decidedChatEditingResource', []); const chatEditingResourceContextKey = new RawContextKey('chatEditingResource', undefined); @@ -98,7 +92,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic super.dispose(); } - async startOrContinueEditingSession(chatSessionId: string, options?: { silent: boolean }): Promise { + async startOrContinueEditingSession(chatSessionId: string, options?: { silent: boolean }): Promise { const session = this._currentSessionObs.get(); if (session) { if (session.chatSessionId !== chatSessionId) { @@ -108,7 +102,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic return this._createEditingSession(chatSessionId, options); } - private async _createEditingSession(chatSessionId: string, options?: { silent: boolean }): Promise { + private async _createEditingSession(chatSessionId: string, options?: { silent: boolean }): Promise { if (this._currentSessionObs.get()) { throw new BugIndicatingError('Cannot have more than one active editing session'); } @@ -131,6 +125,7 @@ export class ChatEditingService extends Disposable implements IChatEditingServic this._currentSessionObs.set(session, undefined); this._onDidCreateEditingSession.fire(session); + return session; } public triggerEditComputation(responseModel: IChatResponseModel): Promise { @@ -471,79 +466,6 @@ export class ChatEditingDiscardAllAction extends Action2 { } registerAction2(ChatEditingDiscardAllAction); -export class ChatEditingStartSessionAction extends Action2 { - static readonly ID = 'chatEditing.startSession'; - static readonly LABEL = localize2('chatEditing.startSession', 'Start Editing Session'); - - constructor() { - super({ - id: ChatEditingStartSessionAction.ID, - title: ChatEditingStartSessionAction.LABEL, - category: CHAT_CATEGORY, - icon: Codicon.edit, - precondition: CONTEXT_CHAT_ENABLED, - f1: true, - menu: [{ - id: MenuId.ViewTitle, - when: ContextKeyExpr.and(ContextKeyExpr.equals('view', CHAT_VIEW_ID), CONTEXT_CHAT_EDITING_ENABLED), - group: 'navigation', - order: 1 - }] - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const textEditorService = accessor.get(IEditorService); - const variablesService = accessor.get(IChatVariablesService); - const chatEditingService = accessor.get(IChatEditingService); - const chatWidgetService = accessor.get(IChatWidgetService); - const viewsService = accessor.get(IViewsService); - const currentEditingSession = chatEditingService.currentEditingSession; - if (currentEditingSession) { - return; - } - - const panelWidgets = chatWidgetService.getWidgetByLocation(ChatAgentLocation.Panel); - - const widget = panelWidgets[0] ?? await showChatView(viewsService); - if (!widget?.viewModel) { - return; - } - - const visibleTextEditorControls = textEditorService.visibleTextEditorControls.filter((e) => isCodeEditor(e)); - visibleTextEditorControls.forEach((e) => { - const activeUri = e.getModel()?.uri; - if (activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) { - variablesService.attachContext('file', { uri: activeUri, }, ChatAgentLocation.Panel); - } - }); - await chatEditingService.startOrContinueEditingSession(widget.viewModel.sessionId, { silent: true }); - } -} - - - -registerAction2(ChatEditingStartSessionAction); -export class ChatEditingStopSessionAction extends Action2 { - static readonly ID = 'chatEditing.stopSession'; - static readonly LABEL = localize2('chatEditing.stopSession', 'Stop Editing Session'); - - constructor() { - super({ - id: ChatEditingStopSessionAction.ID, - title: ChatEditingStopSessionAction.LABEL, - category: CHAT_CATEGORY, - f1: true - }); - } - - async run(accessor: ServicesAccessor, ...args: any[]): Promise { - const chatEditingService = accessor.get(IChatEditingService); - await chatEditingService.currentEditingSession?.stop(); - } -} -registerAction2(ChatEditingStopSessionAction); - export class ChatEditingShowChangesAction extends Action2 { static readonly ID = 'chatEditing.openDiffs'; static readonly LABEL = localize('chatEditing.openDiffs', 'Open Diffs'); @@ -727,7 +649,11 @@ class ChatEditingSession extends Disposable implements IChatEditingSession { })); })); - this.dispose(); + if (this._state.get() !== ChatEditingSessionState.Disposed) { + // session got disposed while we were closing editors + this.dispose(); + } + } override dispose() { diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index b388ffd59d5da..13849afedf0ad 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -32,7 +32,7 @@ import { IThemeService } from '../../../../platform/theme/common/themeService.js import { TerminalChatController } from '../../terminal/terminalContribExports.js'; import { ChatAgentLocation, IChatAgentCommand, IChatAgentData, IChatAgentService, IChatWelcomeMessageContent, isChatWelcomeMessageContent } from '../common/chatAgents.js'; import { CONTEXT_CHAT_INPUT_HAS_AGENT, CONTEXT_CHAT_LOCATION, CONTEXT_CHAT_REQUEST_IN_PROGRESS, CONTEXT_IN_CHAT_SESSION, CONTEXT_IN_QUICK_CHAT, CONTEXT_LAST_ITEM_ID, CONTEXT_PARTICIPANT_SUPPORTS_MODEL_PICKER, CONTEXT_RESPONSE_FILTERED } from '../common/chatContextKeys.js'; -import { IChatEditingService, IChatEditingSession } from '../common/chatEditingService.js'; +import { ChatEditingSessionState, IChatEditingService, IChatEditingSession } from '../common/chatEditingService.js'; import { IChatModel, IChatRequestVariableEntry, IChatResponseModel } from '../common/chatModel.js'; import { ChatRequestAgentPart, IParsedChatRequest, chatAgentLeader, chatSubcommandLeader, formatChatQuestion } from '../common/chatParserTypes.js'; import { ChatRequestParser } from '../common/chatRequestParser.js'; @@ -271,6 +271,25 @@ export class ChatWidget extends Disposable implements IChatWidget { this.renderChatEditingSessionState(session); })); + if (this._location.location === ChatAgentLocation.EditingSession) { + let currentEditSession: IChatEditingSession | undefined = undefined; + this._register(this.onDidChangeViewModel(async () => { + + const sessionId = this._viewModel?.sessionId; + if (sessionId !== currentEditSession?.chatSessionId) { + if (currentEditSession && (currentEditSession.state.get() !== ChatEditingSessionState.Disposed)) { + await currentEditSession.stop(); + } + if (sessionId) { + currentEditSession = await this.chatEditingService.startOrContinueEditingSession(sessionId, { silent: true }); + } else { + currentEditSession = undefined; + } + } + + })); + } + this._register(codeEditorService.registerCodeEditorOpenHandler(async (input: ITextResourceEditorInput, _source: ICodeEditor | null, _sideBySide?: boolean): Promise => { const resource = input.resource; if (resource.scheme !== Schemas.vscodeChatCodeBlock) { @@ -808,14 +827,6 @@ export class ChatWidget extends Disposable implements IChatWidget { } })); - if (this._location.location === ChatAgentLocation.EditingSession) { - const currentSession = this.chatEditingService.currentEditingSession; - if (currentSession && (currentSession.chatSessionId !== model.sessionId)) { - currentSession?.stop(); - } - this.chatEditingService.startOrContinueEditingSession(model.sessionId); - } - if (this.tree) { this.onDidChangeItems(); revealLastElement(this.tree); diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index cb9b41173d22d..0454b567511f9 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -19,7 +19,7 @@ export interface IChatEditingService { readonly currentEditingSession: IChatEditingSession | null; - startOrContinueEditingSession(chatSessionId: string, options?: { silent: boolean }): Promise; + startOrContinueEditingSession(chatSessionId: string, options?: { silent: boolean }): Promise; triggerEditComputation(responseModel: IChatResponseModel): Promise; }