Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/sessions/contrib/chat/browser/newChatViewPane.ts
13401 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
import './media/chatWidget.css';
7
import * as dom from '../../../../base/browser/dom.js';
8
import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
9
import { derived } from '../../../../base/common/observable.js';
10
import { isWeb } from '../../../../base/common/platform.js';
11
import { URI } from '../../../../base/common/uri.js';
12
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
13
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
14
import { IContextMenuService } from '../../../../platform/contextview/browser/contextView.js';
15
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
16
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
17
import { ILogService } from '../../../../platform/log/common/log.js';
18
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
19
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
20
import { IHoverService } from '../../../../platform/hover/browser/hover.js';
21
import { localize } from '../../../../nls.js';
22
import { ISessionsManagementService } from '../../../services/sessions/common/sessionsManagement.js';
23
import { ISessionsProvidersService } from '../../../services/sessions/browser/sessionsProvidersService.js';
24
import { IAquariumService } from '../../aquarium/browser/aquariumOverlay.js';
25
import { IViewDescriptorService } from '../../../../workbench/common/views.js';
26
import { IWorkspaceTrustRequestService } from '../../../../platform/workspace/common/workspaceTrust.js';
27
import { IViewPaneOptions, ViewPane } from '../../../../workbench/browser/parts/views/viewPane.js';
28
import { WorkspacePicker, IWorkspaceSelection } from './sessionWorkspacePicker.js';
29
import { ScopedWorkspacePicker } from './scopedWorkspacePicker.js';
30
import { NewChatInputWidget } from './newChatInput.js';
31
import { IChatRequestVariableEntry } from '../../../../workbench/contrib/chat/common/attachments/chatVariableEntries.js';
32
33
// #region --- New Chat Widget ---
34
35
class NewChatWidget extends Disposable {
36
37
private readonly _workspacePicker: WorkspacePicker;
38
private readonly _newChatInput: NewChatInputWidget;
39
40
/** Tracks an in-flight wait for a provider's session types to become available. */
41
private readonly _pendingSessionTypeWait = new MutableDisposable<IDisposable>();
42
43
constructor(
44
@IInstantiationService private readonly instantiationService: IInstantiationService,
45
@ILogService private readonly logService: ILogService,
46
@ISessionsManagementService private readonly sessionsManagementService: ISessionsManagementService,
47
@ISessionsProvidersService private readonly sessionsProvidersService: ISessionsProvidersService,
48
@IWorkspaceTrustRequestService private readonly workspaceTrustRequestService: IWorkspaceTrustRequestService,
49
@IAquariumService private readonly aquariumService: IAquariumService,
50
) {
51
super();
52
const pickerCtor = isWeb ? ScopedWorkspacePicker : WorkspacePicker;
53
this._workspacePicker = this._register(this.instantiationService.createInstance(pickerCtor));
54
this._register(this._pendingSessionTypeWait);
55
56
const canSendRequest = derived(reader => {
57
const session = this.sessionsManagementService.activeSession.read(reader);
58
if (!session) {
59
return false;
60
}
61
return !session.loading.read(reader);
62
});
63
64
const loading = derived(reader => {
65
const session = this.sessionsManagementService.activeSession.read(reader);
66
return session?.loading.read(reader) ?? false;
67
});
68
69
this._newChatInput = this._register(this.instantiationService.createInstance(NewChatInputWidget, {
70
getContextFolderUri: () => this._getContextFolderUri(),
71
sendRequest: async (text: string, attachedContext?: IChatRequestVariableEntry[]) => this._send(text, attachedContext),
72
canSendRequest,
73
loading,
74
}));
75
76
this._register(this._workspacePicker.onDidSelectWorkspace(async workspace => {
77
if (workspace) {
78
const selectedSessionType = this._newChatInput.sessionTypePicker.selectedType;
79
const validSessionTypes = this.sessionsProvidersService.getProvider(workspace.providerId)?.getSessionTypes(workspace.workspace.repositories[0].uri);
80
const validSessionType = selectedSessionType ? validSessionTypes?.find(type => type.id === selectedSessionType) : validSessionTypes?.[0];
81
await this._onWorkspaceSelected(workspace, validSessionType?.id);
82
} else {
83
await this._onWorkspaceSelected(undefined, undefined);
84
}
85
this._newChatInput.focus();
86
}));
87
this._register(this._newChatInput.sessionTypePicker.onDidSelectSessionType(async sessionType => {
88
await this._onWorkspaceSelected(this._workspacePicker.selectedProject, sessionType);
89
this._newChatInput.focus();
90
}));
91
}
92
93
// --- Rendering ---
94
95
render(parent: HTMLElement): void {
96
const element = dom.append(parent, dom.$('.sessions-chat-widget'));
97
const chatWidgetContainer = dom.append(element, dom.$('.new-chat-widget-container'));
98
const chatWidgetContent = dom.append(chatWidgetContainer, dom.$('.new-chat-widget-content'));
99
100
this._register(this.aquariumService.mountToggle(element));
101
102
const workspacePickerContainer = dom.append(chatWidgetContent, dom.$('.new-session-workspace-picker-container'));
103
this._register(this._renderWorkspacePicker(workspacePickerContainer));
104
105
this._newChatInput.render(chatWidgetContent, parent);
106
107
// Create initial session for any workspace already selected at construct time.
108
// If the selection arrives later (provider registers asynchronously), the
109
// picker fires onDidSelectWorkspace and our listener handles it.
110
// Skip if an active session already exists (restored by openNewSessionView
111
// from a pending new session when navigating back from another session).
112
const restoredProject = this._workspacePicker.selectedProject;
113
if (!this._syncWorkspacePickerFromActiveSession() && restoredProject) {
114
this._createNewSession(restoredProject, this._newChatInput.sessionTypePicker.selectedType);
115
}
116
117
chatWidgetContainer.classList.add('revealed');
118
}
119
120
/**
121
* If a pending session was restored by {@link openNewSessionView}, sync
122
* the workspace picker to match the session's workspace. The picker may
123
* have restored a workspace from a different provider (e.g. remote vs
124
* local), so overwrite it with the session's actual workspace without
125
* firing the event (which would trigger {@link _onWorkspaceSelected} and
126
* create a new session).
127
*
128
* @returns `true` if an active session was found and the picker was synced.
129
*/
130
private _syncWorkspacePickerFromActiveSession(): boolean {
131
const activeSession = this.sessionsManagementService.activeSession.get();
132
if (!activeSession) {
133
return false;
134
}
135
136
const sessionWorkspace = activeSession.workspace.get();
137
if (sessionWorkspace) {
138
this._workspacePicker.setSelectedWorkspace(
139
{ providerId: activeSession.providerId, workspace: sessionWorkspace },
140
/* fireEvent */ false,
141
);
142
}
143
144
return true;
145
}
146
147
private _createNewSession(selection: IWorkspaceSelection, sessionTypeId: string | undefined): void {
148
const provider = this.sessionsProvidersService.getProviders().find(p => p.id === selection.providerId);
149
const repoUri = selection.workspace.repositories[0].uri;
150
151
// Drop the carried-over sessionTypeId if it doesn't apply to this provider —
152
// happens when the picker upgrades to a different provider after restore and
153
// the previous active session's type (e.g. EH CLI's "agents") doesn't exist
154
// on the new provider (e.g. agent host).
155
if (sessionTypeId && provider && !provider.getSessionTypes(repoUri).some(t => t.id === sessionTypeId)) {
156
sessionTypeId = undefined;
157
}
158
159
// Session types may not be available yet (e.g., agent host still connecting).
160
// If so, wait for them before creating the session — otherwise createNewSession
161
// throws and the new chat view is left without an active session, which hides
162
// agent-host-specific UI (model picker etc.) until the user re-picks the workspace.
163
// If the connection fails, the picker fires onDidSelectWorkspace(undefined) which
164
// clears the pending wait via _onWorkspaceSelected.
165
if (provider && !sessionTypeId && provider.getSessionTypes(repoUri).length === 0 && provider.onDidChangeSessionTypes) {
166
const pendingStore = new DisposableStore();
167
this._pendingSessionTypeWait.value = pendingStore;
168
169
pendingStore.add(provider.onDidChangeSessionTypes(() => {
170
if (provider.getSessionTypes(repoUri).length > 0) {
171
this._pendingSessionTypeWait.clear();
172
this._createNewSession(selection, sessionTypeId);
173
}
174
}));
175
176
return;
177
}
178
179
try {
180
this.sessionsManagementService.createNewSession(selection.providerId, repoUri, sessionTypeId);
181
} catch (e) {
182
this.logService.error('Failed to create new session:', e);
183
}
184
}
185
186
/**
187
* Returns the workspace URI for the context picker based on the current workspace selection.
188
*/
189
private _getContextFolderUri(): URI | undefined {
190
return this._workspacePicker.selectedProject?.workspace.repositories[0]?.uri;
191
}
192
193
private _renderWorkspacePicker(container: HTMLElement): IDisposable {
194
const pickersRow = dom.append(container, dom.$('.session-workspace-picker'));
195
const pickersLabel = dom.append(pickersRow, dom.$('.session-workspace-picker-label'));
196
pickersLabel.textContent = this._workspacePicker.selectedProject
197
? localize('newSessionIn', "New session in")
198
: localize('newSessionChooseWorkspace', "Start by picking a");
199
200
this._workspacePicker.render(pickersRow);
201
return this._workspacePicker.onDidSelectWorkspace(() => {
202
const workspace = this._workspacePicker.selectedProject;
203
pickersLabel.textContent = workspace ? localize('newSessionIn', "New session in") : localize('newSessionChooseWorkspace', "Start by picking a");
204
});
205
}
206
207
// --- Send ---
208
209
private async _send(query: string, attachedContext?: IChatRequestVariableEntry[]): Promise<void> {
210
const session = this.sessionsManagementService.activeSession.get();
211
if (!session) {
212
this._workspacePicker.showPicker();
213
return;
214
}
215
try {
216
await this.sessionsManagementService.sendAndCreateChat(session, { query, attachedContext });
217
} catch (e) {
218
this.logService.error('Failed to send request:', e);
219
}
220
}
221
222
private async _requestFolderTrust(folderUri: URI): Promise<boolean> {
223
const trusted = await this.workspaceTrustRequestService.requestResourcesTrust({
224
uri: folderUri,
225
message: localize('trustFolderMessage', "An agent session will be able to read files, run commands, and make changes in this folder."),
226
});
227
if (!trusted) {
228
this._workspacePicker.removeFromRecents(folderUri);
229
}
230
return !!trusted;
231
}
232
233
saveState(): void {
234
this._newChatInput.saveState();
235
}
236
237
layout(_height: number, _width: number): void {
238
this._newChatInput.layout(_height, _width);
239
}
240
241
focusInput(): void {
242
this._newChatInput.focus();
243
}
244
245
/**
246
* Handles a workspace selection from the workspace picker.
247
* Requests folder trust if needed and creates a new session.
248
*/
249
private async _onWorkspaceSelected(selection: IWorkspaceSelection | undefined, sessionTypeId: string | undefined): Promise<void> {
250
// Cancel any in-flight wait for a previous selection.
251
this._pendingSessionTypeWait.clear();
252
253
if (!selection) {
254
this.sessionsManagementService.unsetNewSession();
255
return;
256
}
257
258
if (selection.workspace.requiresWorkspaceTrust) {
259
const workspaceUri = selection.workspace.repositories[0]?.uri;
260
if (workspaceUri && !await this._requestFolderTrust(workspaceUri)) {
261
return;
262
}
263
}
264
265
this._createNewSession(selection, sessionTypeId);
266
}
267
268
prefillInput(text: string): void {
269
this._newChatInput.prefillInput(text);
270
}
271
272
sendQuery(text: string): void {
273
this._newChatInput.sendQuery(text);
274
}
275
276
selectWorkspace(workspace: IWorkspaceSelection): void {
277
this._workspacePicker.setSelectedWorkspace(workspace);
278
}
279
}
280
281
// #endregion
282
283
// #region --- New Chat View Pane ---
284
285
export const SessionsViewId = 'workbench.view.sessions.chat';
286
287
export class NewChatViewPane extends ViewPane {
288
289
private _widget: NewChatWidget | undefined;
290
291
constructor(
292
options: IViewPaneOptions,
293
@IKeybindingService keybindingService: IKeybindingService,
294
@IContextMenuService contextMenuService: IContextMenuService,
295
@IConfigurationService configurationService: IConfigurationService,
296
@IContextKeyService contextKeyService: IContextKeyService,
297
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
298
@IInstantiationService instantiationService: IInstantiationService,
299
@IOpenerService openerService: IOpenerService,
300
@IThemeService themeService: IThemeService,
301
@IHoverService hoverService: IHoverService,
302
) {
303
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
304
}
305
306
protected override renderBody(container: HTMLElement): void {
307
super.renderBody(container);
308
309
this._widget = this._register(this.instantiationService.createInstance(
310
NewChatWidget,
311
));
312
313
this._widget.render(container);
314
this._widget.focusInput();
315
}
316
317
protected override layoutBody(height: number, width: number): void {
318
super.layoutBody(height, width);
319
this._widget?.layout(height, width);
320
}
321
322
override focus(): void {
323
super.focus();
324
this._widget?.focusInput();
325
}
326
327
prefillInput(text: string): void {
328
this._widget?.prefillInput(text);
329
}
330
331
sendQuery(text: string): void {
332
this._widget?.sendQuery(text);
333
}
334
335
selectWorkspace(workspace: IWorkspaceSelection): void {
336
this._widget?.selectWorkspace(workspace);
337
}
338
339
override setVisible(visible: boolean): void {
340
super.setVisible(visible);
341
if (visible) {
342
this._widget?.focusInput();
343
}
344
}
345
346
override saveState(): void {
347
this._widget?.saveState();
348
}
349
350
override dispose(): void {
351
this._widget?.saveState();
352
super.dispose();
353
}
354
}
355
356
// #endregion
357
358