Path: blob/main/src/vs/sessions/contrib/chat/browser/newChatViewPane.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 * as dom from '../../../../base/browser/dom.js';7import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js';8import { derived } from '../../../../base/common/observable.js';9import { isWeb } from '../../../../base/common/platform.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 { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';15import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';16import { ILogService } from '../../../../platform/log/common/log.js';17import { IOpenerService } from '../../../../platform/opener/common/opener.js';18import { IThemeService } from '../../../../platform/theme/common/themeService.js';19import { IHoverService } from '../../../../platform/hover/browser/hover.js';20import { localize } from '../../../../nls.js';21import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';22import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js';23import { IAquariumService } from '../../aquarium/browser/aquariumOverlay.js';24import { IViewDescriptorService } from '../../../../workbench/common/views.js';25import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js';26import { IViewPaneOptions, ViewPane } from '../../../../workbench/browser/parts/views/viewPane.js';27import { WorkspacePicker, IWorkspaceSelection } from './sessionWorkspacePicker.js';28import { ScopedWorkspacePicker } from './scopedWorkspacePicker.js';29import { NewChatInputWidget } from './newChatInput.js';30import { IChatRequestVariableEntry } from '../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js';3132// #region --- New Chat Widget ---3334class NewChatWidget extends Disposable {3536private readonly _workspacePicker: WorkspacePicker;37private readonly _newChatInput: NewChatInputWidget;3839/** Tracks an in-flight wait for a provider's session types to become available. */40private readonly _pendingSessionTypeWait = new MutableDisposable<IDisposable>();4142constructor(43@IInstantiationService private readonly instantiationService: IInstantiationService,44@ILogService private readonly logService: ILogService,45@ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService,46@ISessionsProvidersService private readonly sessionsProvidersService: ISessionsProvidersService,47@IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService,48@IAquariumService private readonly aquariumService: IAquariumService,49) {50super();51const pickerCtor = isWeb ? ScopedWorkspacePicker : WorkspacePicker;52this._workspacePicker = this._register(this.instantiationService.createInstance(pickerCtor));53this._register(this._pendingSessionTypeWait);5455const canSendRequest = derived(reader => {56const session = this.sessionsManagementService.activeSession.read(reader);57if (!session) {58return false;59}60return !session.loading.read(reader);61});6263const loading = derived(reader => {64const session = this.sessionsManagementService.activeSession.read(reader);65return session?.loading.read(reader) ?? false;66});6768this._newChatInput = this._register(this.instantiationService.createInstance(NewChatInputWidget, {69getContextFolderUri: () => this._getContextFolderUri(),70sendRequest: async (text: string, attachedContext?: IChatRequestVariableEntry[]) => this._send(text, attachedContext),71canSendRequest,72loading,73}));7475this._register(this._workspacePicker.onDidSelectWorkspace(async workspace => {76if (workspace) {77const selectedSessionType = this._newChatInput.sessionTypePicker.selectedType;78const validSessionTypes = this.sessionsProvidersService.getProvider(workspace.providerId)?.getSessionTypes(workspace.workspace.repositories[0].uri);79const validSessionType = selectedSessionType ? validSessionTypes?.find(type => type.id === selectedSessionType) : validSessionTypes?.[0];80await this._onWorkspaceSelected(workspace, validSessionType?.id);81} else {82await this._onWorkspaceSelected(undefined, undefined);83}84this._newChatInput.focus();85}));86this._register(this._newChatInput.sessionTypePicker.onDidSelectSessionType(async sessionType => {87await this._onWorkspaceSelected(this._workspacePicker.selectedProject, sessionType);88this._newChatInput.focus();89}));90}9192// --- Rendering ---9394render(parent: HTMLElement): void {95const element = dom.append(parent, dom.$('.sessions-chat-widget'));96const chatWidgetContainer = dom.append(element, dom.$('.new-chat-widget-container'));97const chatWidgetContent = dom.append(chatWidgetContainer, dom.$('.new-chat-widget-content'));9899this._register(this.aquariumService.mountToggle(element));100101const workspacePickerContainer = dom.append(chatWidgetContent, dom.$('.new-session-workspace-picker-container'));102this._register(this._renderWorkspacePicker(workspacePickerContainer));103104this._newChatInput.render(chatWidgetContent, parent);105106// Create initial session for any workspace already selected at construct time.107// If the selection arrives later (provider registers asynchronously), the108// picker fires onDidSelectWorkspace and our listener handles it.109// Skip if an active session already exists (restored by openNewSessionView110// from a pending new session when navigating back from another session).111const restoredProject = this._workspacePicker.selectedProject;112if (!this._syncWorkspacePickerFromActiveSession() && restoredProject) {113this._createNewSession(restoredProject, this._newChatInput.sessionTypePicker.selectedType);114}115116chatWidgetContainer.classList.add('revealed');117}118119/**120* If a pending session was restored by {@link openNewSessionView}, sync121* the workspace picker to match the session's workspace. The picker may122* have restored a workspace from a different provider (e.g. remote vs123* local), so overwrite it with the session's actual workspace without124* firing the event (which would trigger {@link _onWorkspaceSelected} and125* create a new session).126*127* @returns `true` if an active session was found and the picker was synced.128*/129private _syncWorkspacePickerFromActiveSession(): boolean {130const activeSession = this.sessionsManagementService.activeSession.get();131if (!activeSession) {132return false;133}134135const sessionWorkspace = activeSession.workspace.get();136if (sessionWorkspace) {137this._workspacePicker.setSelectedWorkspace(138{ providerId: activeSession.providerId, workspace: sessionWorkspace },139/* fireEvent */ false,140);141}142143return true;144}145146private _createNewSession(selection: IWorkspaceSelection, sessionTypeId: string | undefined): void {147const provider = this.sessionsProvidersService.getProviders().find(p => p.id === selection.providerId);148const repoUri = selection.workspace.repositories[0].uri;149150// Drop the carried-over sessionTypeId if it doesn't apply to this provider —151// happens when the picker upgrades to a different provider after restore and152// the previous active session's type (e.g. EH CLI's "agents") doesn't exist153// on the new provider (e.g. agent host).154if (sessionTypeId && provider && !provider.getSessionTypes(repoUri).some(t => t.id === sessionTypeId)) {155sessionTypeId = undefined;156}157158// Session types may not be available yet (e.g., agent host still connecting).159// If so, wait for them before creating the session — otherwise createNewSession160// throws and the new chat view is left without an active session, which hides161// agent-host-specific UI (model picker etc.) until the user re-picks the workspace.162// If the connection fails, the picker fires onDidSelectWorkspace(undefined) which163// clears the pending wait via _onWorkspaceSelected.164if (provider && !sessionTypeId && provider.getSessionTypes(repoUri).length === 0 && provider.onDidChangeSessionTypes) {165const pendingStore = new DisposableStore();166this._pendingSessionTypeWait.value = pendingStore;167168pendingStore.add(provider.onDidChangeSessionTypes(() => {169if (provider.getSessionTypes(repoUri).length > 0) {170this._pendingSessionTypeWait.clear();171this._createNewSession(selection, sessionTypeId);172}173}));174175return;176}177178try {179this.sessionsManagementService.createNewSession(selection.providerId, repoUri, sessionTypeId);180} catch (e) {181this.logService.error('Failed to create new session:', e);182}183}184185/**186* Returns the workspace URI for the context picker based on the current workspace selection.187*/188private _getContextFolderUri(): URI | undefined {189return this._workspacePicker.selectedProject?.workspace.repositories[0]?.uri;190}191192private _renderWorkspacePicker(container: HTMLElement): IDisposable {193const pickersRow = dom.append(container, dom.$('.session-workspace-picker'));194const pickersLabel = dom.append(pickersRow, dom.$('.session-workspace-picker-label'));195pickersLabel.textContent = this._workspacePicker.selectedProject196? localize('newSessionIn', "New session in")197: localize('newSessionChooseWorkspace', "Start by picking a");198199this._workspacePicker.render(pickersRow);200return this._workspacePicker.onDidSelectWorkspace(() => {201const workspace = this._workspacePicker.selectedProject;202pickersLabel.textContent = workspace ? localize('newSessionIn', "New session in") : localize('newSessionChooseWorkspace', "Start by picking a");203});204}205206// --- Send ---207208private async _send(query: string, attachedContext?: IChatRequestVariableEntry[]): Promise<void> {209const session = this.sessionsManagementService.activeSession.get();210if (!session) {211this._workspacePicker.showPicker();212return;213}214try {215await this.sessionsManagementService.sendAndCreateChat(session, { query, attachedContext });216} catch (e) {217this.logService.error('Failed to send request:', e);218}219}220221private async _requestFolderTrust(folderUri: URI): Promise<boolean> {222const trusted = await this.workspaceTrustRequestService.requestResourcesTrust({223uri: folderUri,224message: localize('trustFolderMessage', "An agent session will be able to read files, run commands, and make changes in this folder."),225});226if (!trusted) {227this._workspacePicker.removeFromRecents(folderUri);228}229return !!trusted;230}231232saveState(): void {233this._newChatInput.saveState();234}235236layout(_height: number, _width: number): void {237this._newChatInput.layout(_height, _width);238}239240focusInput(): void {241this._newChatInput.focus();242}243244/**245* Handles a workspace selection from the workspace picker.246* Requests folder trust if needed and creates a new session.247*/248private async _onWorkspaceSelected(selection: IWorkspaceSelection | undefined, sessionTypeId: string | undefined): Promise<void> {249// Cancel any in-flight wait for a previous selection.250this._pendingSessionTypeWait.clear();251252if (!selection) {253this.sessionsManagementService.unsetNewSession();254return;255}256257if (selection.workspace.requiresWorkspaceTrust) {258const workspaceUri = selection.workspace.repositories[0]?.uri;259if (workspaceUri && !await this._requestFolderTrust(workspaceUri)) {260return;261}262}263264this._createNewSession(selection, sessionTypeId);265}266267prefillInput(text: string): void {268this._newChatInput.prefillInput(text);269}270271sendQuery(text: string): void {272this._newChatInput.sendQuery(text);273}274275selectWorkspace(workspace: IWorkspaceSelection): void {276this._workspacePicker.setSelectedWorkspace(workspace);277}278}279280// #endregion281282// #region --- New Chat View Pane ---283284export const SessionsViewId = 'workbench.view.sessions.chat';285286export class NewChatViewPane extends ViewPane {287288private _widget: NewChatWidget | undefined;289290constructor(291options: IViewPaneOptions,292@IKeybindingService keybindingService: IKeybindingService,293@IContextMenuService contextMenuService: IContextMenuService,294@IConfigurationService configurationService: IConfigurationService,295@IContextKeyService contextKeyService: IContextKeyService,296@IViewDescriptorService viewDescriptorService: IViewDescriptorService,297@IInstantiationService instantiationService: IInstantiationService,298@IOpenerService openerService: IOpenerService,299@IThemeService themeService: IThemeService,300@IHoverService hoverService: IHoverService,301) {302super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);303}304305protected override renderBody(container: HTMLElement): void {306super.renderBody(container);307308this._widget = this._register(this.instantiationService.createInstance(309NewChatWidget,310));311312this._widget.render(container);313this._widget.focusInput();314}315316protected override layoutBody(height: number, width: number): void {317super.layoutBody(height, width);318this._widget?.layout(height, width);319}320321override focus(): void {322super.focus();323this._widget?.focusInput();324}325326prefillInput(text: string): void {327this._widget?.prefillInput(text);328}329330sendQuery(text: string): void {331this._widget?.sendQuery(text);332}333334selectWorkspace(workspace: IWorkspaceSelection): void {335this._widget?.selectWorkspace(workspace);336}337338override setVisible(visible: boolean): void {339super.setVisible(visible);340if (visible) {341this._widget?.focusInput();342}343}344345override saveState(): void {346this._widget?.saveState();347}348349override dispose(): void {350this._widget?.saveState();351super.dispose();352}353}354355// #endregion356357358