Path: blob/main/src/vs/workbench/contrib/chat/browser/widget/chatWidgetService.ts
5272 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 { 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 view (preferred)111if (target === ChatViewPaneTarget || typeof target === 'undefined') {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() && !options?.preserveFocus) {156ensureFocusTransfer = Event.toPromise(Event.once(Event.filter(this.layoutService.onDidChangeActiveContainer, isGroupActive)));157}158159const pane = await existingEditor.group.openEditor(existingEditor.editor, options);160await ensureFocusTransfer;161return pane instanceof ChatEditor ? pane.widget : undefined;162}163164// Already open in quick chat?165if (isEqual(sessionResource, this.quickChatService.sessionResource)) {166this.quickChatService.focus();167return undefined;168}169170return undefined;171}172173private async prepareSessionForMove(sessionResource: URI, target: typeof ChatViewPaneTarget | PreferredGroup | undefined): Promise<void> {174const existingWidget = this.getWidgetBySessionResource(sessionResource);175if (existingWidget) {176const existingEditor = isIChatViewViewContext(existingWidget.viewContext) ?177undefined :178this.findExistingChatEditorByUri(sessionResource);179180if (isIChatViewViewContext(existingWidget.viewContext) && target === ChatViewPaneTarget) {181return;182}183184if (!isIChatViewViewContext(existingWidget.viewContext) && target !== ChatViewPaneTarget && existingEditor && this.isSameEditorTarget(existingEditor.group.id, target)) {185return;186}187188if (existingEditor) {189// widget.clear() on an editor leaves behind an empty chat editor190await this.editorService.closeEditor({ editor: existingEditor.editor, groupId: existingEditor.group.id }, { preserveFocus: true });191} else {192await existingWidget.clear();193}194}195}196197private findExistingChatEditorByUri(sessionUri: URI): { editor: ChatEditorInput; group: IEditorGroup } | undefined {198for (const group of this.editorGroupsService.groups) {199for (const editor of group.editors) {200if (editor instanceof ChatEditorInput && isEqual(editor.sessionResource, sessionUri)) {201return { editor, group };202}203}204}205return undefined;206}207208private isSameEditorTarget(currentGroupId: number, target?: PreferredGroup): boolean {209return typeof target === 'number' && target === currentGroupId ||210target === ACTIVE_GROUP && this.editorGroupsService.activeGroup?.id === currentGroupId ||211isEditorGroup(target) && target.id === currentGroupId;212}213214private setLastFocusedWidget(widget: IChatWidget | undefined): void {215if (widget === this._lastFocusedWidget) {216return;217}218219this._lastFocusedWidget = widget;220}221222register(newWidget: IChatWidget): IDisposable {223if (this._widgets.some(widget => widget === newWidget)) {224throw new Error('Cannot register the same widget multiple times');225}226227this._widgets.push(newWidget);228this._onDidAddWidget.fire(newWidget);229230if (!this._lastFocusedWidget) {231this.setLastFocusedWidget(newWidget);232}233234return combinedDisposable(235newWidget.onDidFocus(() => this.setLastFocusedWidget(newWidget)),236newWidget.onDidChangeViewModel(({ previousSessionResource, currentSessionResource }) => {237if (!previousSessionResource || (currentSessionResource && isEqual(previousSessionResource, currentSessionResource))) {238return;239}240241// Timeout to ensure it wasn't just moving somewhere else242void timeout(200).then(() => {243if (!this.getWidgetBySessionResource(previousSessionResource) && this.chatService.getSession(previousSessionResource)) {244this._onDidBackgroundSession.fire(previousSessionResource);245}246});247}),248toDisposable(() => {249this._widgets.splice(this._widgets.indexOf(newWidget), 1);250if (this._lastFocusedWidget === newWidget) {251this.setLastFocusedWidget(undefined);252}253})254);255}256}257258259