Path: blob/main/src/vs/workbench/contrib/chat/browser/chatViewPane.ts
3296 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { $, getWindow } from '../../../../base/browser/dom.js';6import { CancellationToken } from '../../../../base/common/cancellation.js';7import { DisposableStore } from '../../../../base/common/lifecycle.js';8import { MarshalledId } from '../../../../base/common/marshallingIds.js';9import { Schemas } from '../../../../base/common/network.js';10import { URI } from '../../../../base/common/uri.js';11import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';12import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';13import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';14import { IHoverService } from '../../../../platform/hover/browser/hover.js';15import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';16import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';17import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';18import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js';19import { ILogService } from '../../../../platform/log/common/log.js';20import { IOpenerService } from '../../../../platform/opener/common/opener.js';21import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';22import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js';23import { IThemeService } from '../../../../platform/theme/common/themeService.js';24import { IViewPaneOptions, ViewPane } from '../../../browser/parts/views/viewPane.js';25import { Memento } from '../../../common/memento.js';26import { SIDE_BAR_FOREGROUND } from '../../../common/theme.js';27import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js';28import { IChatViewTitleActionContext } from '../common/chatActions.js';29import { IChatAgentService } from '../common/chatAgents.js';30import { ChatContextKeys } from '../common/chatContextKeys.js';31import { IChatModel } from '../common/chatModel.js';32import { CHAT_PROVIDER_ID } from '../common/chatParticipantContribTypes.js';33import { IChatService } from '../common/chatService.js';34import { IChatSessionsService, IChatSessionsExtensionPoint } from '../common/chatSessionsService.js';35import { ChatAgentLocation, ChatModeKind } from '../common/constants.js';36import { ChatSessionUri } from '../common/chatUri.js';37import { ChatWidget, IChatViewState } from './chatWidget.js';38import { ChatViewWelcomeController, IViewWelcomeDelegate } from './viewsWelcome/chatViewWelcomeController.js';3940interface IViewPaneState extends IChatViewState {41sessionId?: string;42hasMigratedCurrentSession?: boolean;43}4445export const CHAT_SIDEBAR_OLD_VIEW_PANEL_ID = 'workbench.panel.chatSidebar';46export const CHAT_SIDEBAR_PANEL_ID = 'workbench.panel.chat';47export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate {48private _widget!: ChatWidget;49get widget(): ChatWidget { return this._widget; }5051private readonly modelDisposables = this._register(new DisposableStore());52private memento: Memento;53private readonly viewState: IViewPaneState;5455private _restoringSession: Promise<void> | undefined;5657constructor(58private readonly chatOptions: { location: ChatAgentLocation.Panel },59options: IViewPaneOptions,60@IKeybindingService keybindingService: IKeybindingService,61@IContextMenuService contextMenuService: IContextMenuService,62@IConfigurationService configurationService: IConfigurationService,63@IContextKeyService contextKeyService: IContextKeyService,64@IViewDescriptorService viewDescriptorService: IViewDescriptorService,65@IInstantiationService instantiationService: IInstantiationService,66@IOpenerService openerService: IOpenerService,67@IThemeService themeService: IThemeService,68@IHoverService hoverService: IHoverService,69@IStorageService private readonly storageService: IStorageService,70@IChatService private readonly chatService: IChatService,71@IChatAgentService private readonly chatAgentService: IChatAgentService,72@ILogService private readonly logService: ILogService,73@ILayoutService private readonly layoutService: ILayoutService,74@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,75) {76super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);7778// View state for the ViewPane is currently global per-provider basically, but some other strictly per-model state will require a separate memento.79this.memento = new Memento('interactive-session-view-' + CHAT_PROVIDER_ID, this.storageService);80this.viewState = this.memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE) as IViewPaneState;8182if (this.chatOptions.location === ChatAgentLocation.Panel && !this.viewState.hasMigratedCurrentSession) {83const editsMemento = new Memento('interactive-session-view-' + CHAT_PROVIDER_ID + `-edits`, this.storageService);84const lastEditsState = editsMemento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE) as IViewPaneState;85if (lastEditsState.sessionId) {86this.logService.trace(`ChatViewPane: last edits session was ${lastEditsState.sessionId}`);87if (!this.chatService.isPersistedSessionEmpty(lastEditsState.sessionId)) {88this.logService.info(`ChatViewPane: migrating ${lastEditsState.sessionId} to unified view`);89this.viewState.sessionId = lastEditsState.sessionId;90this.viewState.inputValue = lastEditsState.inputValue;91this.viewState.inputState = {92...lastEditsState.inputState,93chatMode: lastEditsState.inputState?.chatMode ?? ChatModeKind.Edit94};95this.viewState.hasMigratedCurrentSession = true;96}97}98}99100this._register(this.chatAgentService.onDidChangeAgents(() => {101if (this.chatAgentService.getDefaultAgent(this.chatOptions?.location)) {102if (!this._widget?.viewModel && !this._restoringSession) {103const info = this.getTransferredOrPersistedSessionInfo();104this._restoringSession =105(info.sessionId ? this.chatService.getOrRestoreSession(info.sessionId) : Promise.resolve(undefined)).then(async model => {106if (!this._widget) {107// renderBody has not been called yet108return;109}110111// The widget may be hidden at this point, because welcome views were allowed. Use setVisible to112// avoid doing a render while the widget is hidden. This is changing the condition in `shouldShowWelcome`113// so it should fire onDidChangeViewWelcomeState.114const wasVisible = this._widget.visible;115try {116this._widget.setVisible(false);117await this.updateModel(model, info.inputValue || info.mode ? { inputState: { chatMode: info.mode }, inputValue: info.inputValue } : undefined);118} finally {119this.widget.setVisible(wasVisible);120}121});122this._restoringSession.finally(() => this._restoringSession = undefined);123}124}125126this._onDidChangeViewWelcomeState.fire();127}));128129// Location context key130ChatContextKeys.panelLocation.bindTo(contextKeyService).set(viewDescriptorService.getViewLocationById(options.id) ?? ViewContainerLocation.AuxiliaryBar);131}132133override getActionsContext(): IChatViewTitleActionContext | undefined {134return this.widget?.viewModel ? {135sessionId: this.widget.viewModel.sessionId,136$mid: MarshalledId.ChatViewContext137} : undefined;138}139140private async updateModel(model?: IChatModel | undefined, viewState?: IChatViewState): Promise<void> {141this.modelDisposables.clear();142143model = model ?? (this.chatService.transferredSessionData?.sessionId && this.chatService.transferredSessionData?.location === this.chatOptions.location144? await this.chatService.getOrRestoreSession(this.chatService.transferredSessionData.sessionId)145: this.chatService.startSession(this.chatOptions.location, CancellationToken.None));146if (!model) {147throw new Error('Could not start chat session');148}149150if (viewState) {151this.updateViewState(viewState);152}153154this.viewState.sessionId = model.sessionId;155this._widget.setModel(model, { ...this.viewState });156157// Update the toolbar context with new sessionId158this.updateActions();159}160161override shouldShowWelcome(): boolean {162const noPersistedSessions = !this.chatService.hasSessions();163const hasCoreAgent = this.chatAgentService.getAgents().some(agent => agent.isCore && agent.locations.includes(this.chatOptions.location));164const hasDefaultAgent = this.chatAgentService.getDefaultAgent(this.chatOptions.location) !== undefined; // only false when Hide Copilot has run and unregistered the setup agents165const shouldShow = !hasCoreAgent && (!hasDefaultAgent || !this._widget?.viewModel && noPersistedSessions);166this.logService.trace(`ChatViewPane#shouldShowWelcome(${this.chatOptions.location}) = ${shouldShow}: hasCoreAgent=${hasCoreAgent} hasDefaultAgent=${hasDefaultAgent} || noViewModel=${!this._widget?.viewModel} && noPersistedSessions=${noPersistedSessions}`);167return !!shouldShow;168}169170private getTransferredOrPersistedSessionInfo(): { sessionId?: string; inputValue?: string; mode?: ChatModeKind } {171if (this.chatService.transferredSessionData?.location === this.chatOptions.location) {172const sessionId = this.chatService.transferredSessionData.sessionId;173return {174sessionId,175inputValue: this.chatService.transferredSessionData.inputValue,176mode: this.chatService.transferredSessionData.mode177};178} else {179return { sessionId: this.viewState.sessionId };180}181}182183protected override async renderBody(parent: HTMLElement): Promise<void> {184super.renderBody(parent);185186this._register(this.instantiationService.createInstance(ChatViewWelcomeController, parent, this, this.chatOptions.location));187188const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])));189const locationBasedColors = this.getLocationBasedColors();190const editorOverflowNode = this.layoutService.getContainer(getWindow(parent)).appendChild($('.chat-editor-overflow.monaco-editor'));191this._register({ dispose: () => editorOverflowNode.remove() });192193this._widget = this._register(scopedInstantiationService.createInstance(194ChatWidget,195this.chatOptions.location,196{ viewId: this.id },197{198autoScroll: mode => mode !== ChatModeKind.Ask,199renderFollowups: this.chatOptions.location === ChatAgentLocation.Panel,200supportsFileReferences: true,201rendererOptions: {202renderTextEditsAsSummary: (uri) => {203return true;204},205referencesExpandedWhenEmptyResponse: false,206progressMessageAtBottomOfResponse: mode => mode !== ChatModeKind.Ask,207},208editorOverflowWidgetsDomNode: editorOverflowNode,209enableImplicitContext: this.chatOptions.location === ChatAgentLocation.Panel,210enableWorkingSet: 'explicit',211supportsChangingModes: true,212},213{214listForeground: SIDE_BAR_FOREGROUND,215listBackground: locationBasedColors.background,216overlayBackground: locationBasedColors.overlayBackground,217inputEditorBackground: locationBasedColors.background,218resultEditorBackground: editorBackground,219220}));221this._register(this.onDidChangeBodyVisibility(visible => {222this._widget.setVisible(visible);223}));224this._register(this._widget.onDidClear(() => this.clear()));225this._widget.render(parent);226227const info = this.getTransferredOrPersistedSessionInfo();228const model = info.sessionId ? await this.chatService.getOrRestoreSession(info.sessionId) : undefined;229230await this.updateModel(model, info.inputValue || info.mode ? { inputState: { chatMode: info.mode }, inputValue: info.inputValue } : undefined);231}232233acceptInput(query?: string): void {234this._widget.acceptInput(query);235}236237private async clear(): Promise<void> {238if (this.widget.viewModel) {239await this.chatService.clearSession(this.widget.viewModel.sessionId);240}241242// Grab the widget's latest view state because it will be loaded back into the widget243this.updateViewState();244await this.updateModel(undefined);245246// Update the toolbar context with new sessionId247this.updateActions();248}249250async loadSession(sessionId: string | URI, viewState?: IChatViewState): Promise<void> {251if (this.widget.viewModel) {252await this.chatService.clearSession(this.widget.viewModel.sessionId);253}254255// Handle locking for contributed chat sessions256if (URI.isUri(sessionId) && sessionId.scheme === Schemas.vscodeChatSession) {257const parsed = ChatSessionUri.parse(sessionId);258if (parsed?.chatSessionType) {259await this.chatSessionsService.canResolveContentProvider(parsed.chatSessionType);260const contributions = this.chatSessionsService.getAllChatSessionContributions();261const contribution = contributions.find((c: IChatSessionsExtensionPoint) => c.type === parsed.chatSessionType);262if (contribution) {263this.widget.lockToCodingAgent(contribution.name, contribution.displayName, contribution.type);264}265}266}267268const newModel = await (URI.isUri(sessionId) ? this.chatService.loadSessionForResource(sessionId, ChatAgentLocation.Panel, CancellationToken.None) : this.chatService.getOrRestoreSession(sessionId));269await this.updateModel(newModel, viewState);270}271272focusInput(): void {273this._widget.focusInput();274}275276override focus(): void {277super.focus();278this._widget.focusInput();279}280281protected override layoutBody(height: number, width: number): void {282super.layoutBody(height, width);283this._widget.layout(height, width);284}285286override saveState(): void {287if (this._widget) {288// Since input history is per-provider, this is handled by a separate service and not the memento here.289// TODO multiple chat views will overwrite each other290this._widget.saveState();291292this.updateViewState();293this.memento.saveMemento();294}295296super.saveState();297}298299private updateViewState(viewState?: IChatViewState): void {300const newViewState = viewState ?? this._widget.getViewState();301for (const [key, value] of Object.entries(newViewState)) {302// Assign all props to the memento so they get saved303(this.viewState as any)[key] = value;304}305}306}307308309