Path: blob/main/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts
4780 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 * as dom from '../../../../../base/browser/dom.js';6import { raceCancellablePromises, timeout } from '../../../../../base/common/async.js';7import { Emitter, Event } from '../../../../../base/common/event.js';8import { combinedDisposable, Disposable, IDisposable, toDisposable } from '../../../../../base/common/lifecycle.js';9import { isEqual } from '../../../../../base/common/resources.js';10import { URI } from '../../../../../base/common/uri.js';11import { ILayoutService } from '../../../../../platform/layout/browser/layoutService.js';12import { ACTIVE_GROUP, IEditorService, type PreferredGroup } from '../../../../services/editor/common/editorService.js';13import { IEditorGroup, IEditorGroupsService, isEditorGroup } from '../../../../services/editor/common/editorGroupsService.js';14import { IViewsService } from '../../../../services/views/common/viewsService.js';15import { IChatService } from '../../common/chatService/chatService.js';16import { ChatAgentLocation } from '../../common/constants.js';17import { ChatViewId, ChatViewPaneTarget, IChatWidget, IChatWidgetService, IQuickChatService, isIChatViewViewContext } from '../chat.js';18import { ChatEditor, IChatEditorOptions } from '../widgetHosts/editor/chatEditor.js';19import { ChatEditorInput } from '../widgetHosts/editor/chatEditorInput.js';20import { ChatViewPane } from '../widgetHosts/viewPane/chatViewPane.js';2122export class ChatWidgetService extends Disposable implements IChatWidgetService {2324declare readonly _serviceBrand: undefined;2526private _widgets: IChatWidget[] = [];27private _lastFocusedWidget: IChatWidget | undefined = undefined;2829private readonly _onDidAddWidget = this._register(new Emitter<IChatWidget>());30readonly onDidAddWidget = this._onDidAddWidget.event;3132private readonly _onDidBackgroundSession = this._register(new Emitter<URI>());33readonly onDidBackgroundSession = this._onDidBackgroundSession.event;3435constructor(36@IEditorGroupsService private readonly editorGroupsService: IEditorGroupsService,37@IViewsService private readonly viewsService: IViewsService,38@IQuickChatService private readonly quickChatService: IQuickChatService,39@ILayoutService private readonly layoutService: ILayoutService,40@IEditorService private readonly editorService: IEditorService,41@IChatService private readonly chatService: IChatService,42) {43super();44}4546get lastFocusedWidget(): IChatWidget | undefined {47return this._lastFocusedWidget;48}4950getAllWidgets(): ReadonlyArray<IChatWidget> {51return this._widgets;52}5354getWidgetsByLocations(location: ChatAgentLocation): ReadonlyArray<IChatWidget> {55return this._widgets.filter(w => w.location === location);56}5758getWidgetByInputUri(uri: URI): IChatWidget | undefined {59return this._widgets.find(w => isEqual(w.input.inputUri, uri));60}6162getWidgetBySessionResource(sessionResource: URI): IChatWidget | undefined {63return this._widgets.find(w => isEqual(w.viewModel?.sessionResource, sessionResource));64}6566async revealWidget(preserveFocus?: boolean): Promise<IChatWidget | undefined> {67const last = this.lastFocusedWidget;68if (last && await this.reveal(last, preserveFocus)) {69return last;70}7172return (await this.viewsService.openView<ChatViewPane>(ChatViewId, !preserveFocus))?.widget;73}7475async reveal(widget: IChatWidget, preserveFocus?: boolean): Promise<boolean> {76if (widget.viewModel?.sessionResource) {77const alreadyOpenWidget = await this.revealSessionIfAlreadyOpen(widget.viewModel.sessionResource, { preserveFocus });78if (alreadyOpenWidget) {79return true;80}81}8283if (isIChatViewViewContext(widget.viewContext)) {84const view = await this.viewsService.openView(widget.viewContext.viewId, !preserveFocus);85if (!preserveFocus) {86view?.focus();87}88return !!view;89}9091return false;92}9394/**95* Reveal the session if already open, otherwise open it.96*/97openSession(sessionResource: URI, target?: typeof ChatViewPaneTarget): Promise<IChatWidget | undefined>;98openSession(sessionResource: URI, target?: PreferredGroup, options?: IChatEditorOptions): Promise<IChatWidget | undefined>;99async openSession(sessionResource: URI, target?: typeof ChatViewPaneTarget | PreferredGroup, options?: IChatEditorOptions): Promise<IChatWidget | undefined> {100// Reveal if already open unless instructed otherwise101if (typeof target === 'undefined' || options?.revealIfOpened) {102const alreadyOpenWidget = await this.revealSessionIfAlreadyOpen(sessionResource, options);103if (alreadyOpenWidget) {104return alreadyOpenWidget;105}106} else {107await this.prepareSessionForMove(sessionResource, target);108}109110// Load this session in chat view111if (target === ChatViewPaneTarget) {112const chatView = await this.viewsService.openView<ChatViewPane>(ChatViewId, !options?.preserveFocus);113if (chatView) {114await chatView.loadSession(sessionResource);115if (!options?.preserveFocus) {116chatView.focusInput();117}118}119return chatView?.widget;120}121122// Open in chat editor123const pane = await this.editorService.openEditor({124resource: sessionResource,125options: {126...options,127revealIfOpened: options?.revealIfOpened ?? true // always try to reveal if already opened unless explicitly told not to128}129}, target);130return pane instanceof ChatEditor ? pane.widget : undefined;131}132133private async revealSessionIfAlreadyOpen(sessionResource: URI, options?: IChatEditorOptions): Promise<IChatWidget | undefined> {134// Already open in chat view?135const chatView = this.viewsService.getViewWithId<ChatViewPane>(ChatViewId);136if (chatView?.widget.viewModel?.sessionResource && isEqual(chatView.widget.viewModel.sessionResource, sessionResource)) {137const view = await this.viewsService.openView(ChatViewId, !options?.preserveFocus);138if (!options?.preserveFocus) {139view?.focus();140}141return chatView.widget;142}143144// Already open in an editor?145const existingEditor = this.findExistingChatEditorByUri(sessionResource);146if (existingEditor) {147const existingEditorWindowId = existingEditor.group.windowId;148149// focus transfer to other documents is async. If we depend on the focus150// being synchronously transferred in consuming code, this can fail, so151// wait for it to propagate152const isGroupActive = () => dom.getWindow(this.layoutService.activeContainer).vscodeWindowId === existingEditorWindowId;153154let ensureFocusTransfer: Promise<void> | undefined;155if (!isGroupActive()) {156ensureFocusTransfer = raceCancellablePromises([157timeout(500),158Event.toPromise(Event.once(Event.filter(this.layoutService.onDidChangeActiveContainer, isGroupActive))),159]);160}161162const pane = await existingEditor.group.openEditor(existingEditor.editor, options);163await ensureFocusTransfer;164return pane instanceof ChatEditor ? pane.widget : undefined;165}166167// Already open in quick chat?168if (isEqual(sessionResource, this.quickChatService.sessionResource)) {169this.quickChatService.focus();170return undefined;171}172173return undefined;174}175176private async prepareSessionForMove(sessionResource: URI, target: typeof ChatViewPaneTarget | PreferredGroup | undefined): Promise<void> {177const existingWidget = this.getWidgetBySessionResource(sessionResource);178if (existingWidget) {179const existingEditor = isIChatViewViewContext(existingWidget.viewContext) ?180undefined :181this.findExistingChatEditorByUri(sessionResource);182183if (isIChatViewViewContext(existingWidget.viewContext) && target === ChatViewPaneTarget) {184return;185}186187if (!isIChatViewViewContext(existingWidget.viewContext) && target !== ChatViewPaneTarget && existingEditor && this.isSameEditorTarget(existingEditor.group.id, target)) {188return;189}190191if (existingEditor) {192// widget.clear() on an editor leaves behind an empty chat editor193await this.editorService.closeEditor({ editor: existingEditor.editor, groupId: existingEditor.group.id }, { preserveFocus: true });194} else {195await existingWidget.clear();196}197}198}199200private findExistingChatEditorByUri(sessionUri: URI): { editor: ChatEditorInput; group: IEditorGroup } | undefined {201for (const group of this.editorGroupsService.groups) {202for (const editor of group.editors) {203if (editor instanceof ChatEditorInput && isEqual(editor.sessionResource, sessionUri)) {204return { editor, group };205}206}207}208return undefined;209}210211private isSameEditorTarget(currentGroupId: number, target?: PreferredGroup): boolean {212return typeof target === 'number' && target === currentGroupId ||213target === ACTIVE_GROUP && this.editorGroupsService.activeGroup?.id === currentGroupId ||214isEditorGroup(target) && target.id === currentGroupId;215}216217private setLastFocusedWidget(widget: IChatWidget | undefined): void {218if (widget === this._lastFocusedWidget) {219return;220}221222this._lastFocusedWidget = widget;223}224225register(newWidget: IChatWidget): IDisposable {226if (this._widgets.some(widget => widget === newWidget)) {227throw new Error('Cannot register the same widget multiple times');228}229230this._widgets.push(newWidget);231this._onDidAddWidget.fire(newWidget);232233if (!this._lastFocusedWidget) {234this.setLastFocusedWidget(newWidget);235}236237return combinedDisposable(238newWidget.onDidFocus(() => this.setLastFocusedWidget(newWidget)),239newWidget.onDidChangeViewModel(({ previousSessionResource, currentSessionResource }) => {240if (!previousSessionResource || (currentSessionResource && isEqual(previousSessionResource, currentSessionResource))) {241return;242}243244// Timeout to ensure it wasn't just moving somewhere else245void timeout(200).then(() => {246if (!this.getWidgetBySessionResource(previousSessionResource) && this.chatService.getSession(previousSessionResource)) {247this._onDidBackgroundSession.fire(previousSessionResource);248}249});250}),251toDisposable(() => {252this._widgets.splice(this._widgets.indexOf(newWidget), 1);253if (this._lastFocusedWidget === newWidget) {254this.setLastFocusedWidget(undefined);255}256})257);258}259}260261262