Path: blob/main/src/vs/sessions/contrib/chat/browser/newChatInSessionViewPane.ts
13401 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 './media/chatWidget.css';6import './media/newChatInSession.css';7import * as dom from '../../../../base/browser/dom.js';8import { Codicon } from '../../../../base/common/codicons.js';9import { Disposable, DisposableStore, MutableDisposable } from '../../../../base/common/lifecycle.js';10import { derived } from '../../../../base/common/observable.js';11import { Gesture, EventType as TouchEventType } from '../../../../base/browser/touch.js';12import { URI } from '../../../../base/common/uri.js';13import { localize } from '../../../../nls.js';14import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';15import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';16import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';17import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';18import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.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 { IThemeService } from '../../../../platform/theme/common/themeService.js';23import { IHoverService } from '../../../../platform/hover/browser/hover.js';24import { renderIcon } from '../../../../base/browser/ui/iconLabel/iconLabels.js';25import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';26import { IViewDescriptorService } from '../../../../workbench/common/views.js';27import { IViewPaneOptions, ViewPane } from '../../../../workbench/browser/parts/views/viewPane.js';28import { NewChatInputWidget } from './newChatInput.js';29import { IChatRequestVariableEntry } from '../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js';3031// #region --- New Chat In Session Widget ---3233const STORAGE_KEY_SUB_SESSION_TIP_DISMISSED = 'sessions.subSessionTipDismissed';3435/**36* A widget for composing a secondary chat within an existing session.37* Reuses {@link NewChatInputWidget} but without workspace/session type pickers,38* since the session already exists.39*/40class NewChatInSessionWidget extends Disposable {4142private readonly _newChatInput: NewChatInputWidget;43private readonly _tipDisposable = this._register(new MutableDisposable());4445constructor(46@IInstantiationService private readonly instantiationService: IInstantiationService,47@ILogService private readonly logService: ILogService,48@ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService,49@IStorageService private readonly storageService: IStorageService,50) {51super();5253const canSendRequest = derived(reader => {54const session = this.sessionsManagementService.activeSession.read(reader);55return !!session;56});5758const loading = derived(_reader => false);5960this._newChatInput = this._register(this.instantiationService.createInstance(NewChatInputWidget, {61getContextFolderUri: () => this._getContextFolderUri(),62sendRequest: async (text: string, attachedContext?: IChatRequestVariableEntry[]) => this._send(text, attachedContext),63canSendRequest,64loading,65minEditorHeight: 64,66placeholder: localize('newChatInSessionPlaceholder', 'Ask a follow-up question or start a new topic within this session...'),67}));68}6970// --- Rendering ---7172render(parent: HTMLElement): void {73const element = dom.append(parent, dom.$('.sessions-chat-widget.new-chat-in-session'));74const chatWidgetContainer = dom.append(element, dom.$('.new-chat-widget-container'));75const chatWidgetContent = dom.append(chatWidgetContainer, dom.$('.new-chat-widget-content'));7677this._renderSubSessionTip(chatWidgetContent);78this._newChatInput.render(chatWidgetContent, parent);7980chatWidgetContainer.classList.add('revealed');81}8283private _renderSubSessionTip(container: HTMLElement): void {84if (this.storageService.getBoolean(STORAGE_KEY_SUB_SESSION_TIP_DISMISSED, StorageScope.PROFILE, false)) {85return;86}8788const tipContainer = dom.append(container, dom.$('.sub-session-tip-container'));89const tipWidget = dom.append(tipContainer, dom.$('.sub-session-tip-widget'));90tipWidget.setAttribute('role', 'status');91tipWidget.setAttribute('aria-label', localize('subSessionTip.ariaLabel', "Sub-session tip"));9293// Tip icon94const iconEl = dom.append(tipWidget, renderIcon(Codicon.lightbulb));95iconEl.classList.add('sub-session-tip-icon');9697// Tip text98const textEl = dom.append(tipWidget, dom.$('span.sub-session-tip-text'));99textEl.textContent = localize(100'subSessionTip.message',101"This is a sub-session, a new chat in the same workspace. Use it to ask questions, run tasks, or explore ideas with fresh context."102);103104// Dismiss button105const dismissBtn = dom.append(tipWidget, dom.$('button.sub-session-tip-dismiss')) as HTMLButtonElement;106dismissBtn.type = 'button';107dismissBtn.setAttribute('aria-label', localize('subSessionTip.dismiss', "Dismiss tip"));108dom.append(dismissBtn, renderIcon(Codicon.close));109110const dismiss = () => {111this.storageService.store(STORAGE_KEY_SUB_SESSION_TIP_DISMISSED, true, StorageScope.PROFILE, StorageTarget.USER);112tipContainer.remove();113this._tipDisposable.clear();114};115116const handleDismiss = (e: Event) => {117dom.EventHelper.stop(e, true);118dismiss();119};120121const store = new DisposableStore();122store.add(Gesture.addTarget(dismissBtn));123store.add(dom.addDisposableListener(dismissBtn, dom.EventType.CLICK, handleDismiss));124store.add(dom.addDisposableListener(dismissBtn, TouchEventType.Tap, handleDismiss));125this._tipDisposable.value = store;126}127128/**129* Returns the workspace URI from the active session's workspace.130*/131private _getContextFolderUri(): URI | undefined {132const session = this.sessionsManagementService.activeSession.get();133const workspace = session?.workspace.get();134return workspace?.repositories[0]?.workingDirectory ?? workspace?.repositories[0]?.uri;135}136137// --- Send ---138139private async _send(query: string, attachedContext?: IChatRequestVariableEntry[]): Promise<void> {140const activeSession = this.sessionsManagementService.activeSession.get();141if (!activeSession) {142return;143}144const activeChat = activeSession.activeChat.get();145try {146await this.sessionsManagementService.sendRequest(activeSession, activeChat, { query, attachedContext });147} catch (e) {148this.logService.error('Failed to send secondary chat request:', e);149}150}151152layout(height: number, width: number): void {153this._newChatInput.layout(height, width);154}155156focusInput(): void {157this._newChatInput.focus();158}159}160161// #endregion162163// #region --- New Chat In Session View Pane ---164165export const NewChatInSessionViewId = 'workbench.view.sessions.newChatInSession';166167/**168* A view pane that hosts the new-chat-in-session widget.169* Shown when the user wants to compose a secondary chat within the active session.170*/171export class NewChatInSessionViewPane extends ViewPane {172173private _widget: NewChatInSessionWidget | undefined;174175constructor(176options: IViewPaneOptions,177@IKeybindingService keybindingService: IKeybindingService,178@IContextMenuService contextMenuService: IContextMenuService,179@IConfigurationService configurationService: IConfigurationService,180@IContextKeyService contextKeyService: IContextKeyService,181@IViewDescriptorService viewDescriptorService: IViewDescriptorService,182@IInstantiationService instantiationService: IInstantiationService,183@IOpenerService openerService: IOpenerService,184@IThemeService themeService: IThemeService,185@IHoverService hoverService: IHoverService,186) {187super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);188}189190protected override renderBody(container: HTMLElement): void {191super.renderBody(container);192193this._widget = this._register(this.instantiationService.createInstance(194NewChatInSessionWidget,195));196197this._widget.render(container);198this._widget.focusInput();199}200201protected override layoutBody(height: number, width: number): void {202super.layoutBody(height, width);203this._widget?.layout(height, width);204}205206override focus(): void {207super.focus();208this._widget?.focusInput();209}210211override setVisible(visible: boolean): void {212super.setVisible(visible);213if (visible) {214this._widget?.focusInput();215}216}217}218219// #endregion220221222