Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatQuick.ts
3296 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 * as dom from '../../../../base/browser/dom.js';
7
import { Orientation, Sash } from '../../../../base/browser/ui/sash/sash.js';
8
import { disposableTimeout } from '../../../../base/common/async.js';
9
import { CancellationToken } from '../../../../base/common/cancellation.js';
10
import { Emitter, Event } from '../../../../base/common/event.js';
11
import { Disposable, DisposableStore, IDisposable, MutableDisposable } from '../../../../base/common/lifecycle.js';
12
import { Selection } from '../../../../editor/common/core/selection.js';
13
import { MenuId } from '../../../../platform/actions/common/actions.js';
14
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
15
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
16
import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
17
import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js';
18
import { IQuickInputService, IQuickWidget } from '../../../../platform/quickinput/common/quickInput.js';
19
import { editorBackground, inputBackground, quickInputBackground, quickInputForeground } from '../../../../platform/theme/common/colorRegistry.js';
20
import { IQuickChatOpenOptions, IQuickChatService, showChatView } from './chat.js';
21
import { ChatWidget } from './chatWidget.js';
22
import { ChatModel, isCellTextEditOperation } from '../common/chatModel.js';
23
import { IParsedChatRequest } from '../common/chatParserTypes.js';
24
import { IChatProgress, IChatService } from '../common/chatService.js';
25
import { IViewsService } from '../../../services/views/common/viewsService.js';
26
import { EDITOR_DRAG_AND_DROP_BACKGROUND } from '../../../common/theme.js';
27
import { ChatAgentLocation } from '../common/constants.js';
28
29
export class QuickChatService extends Disposable implements IQuickChatService {
30
readonly _serviceBrand: undefined;
31
32
private readonly _onDidClose = this._register(new Emitter<void>());
33
get onDidClose() { return this._onDidClose.event; }
34
35
private _input: IQuickWidget | undefined;
36
// TODO@TylerLeonhardt: support multiple chat providers eventually
37
private _currentChat: QuickChat | undefined;
38
private _container: HTMLElement | undefined;
39
40
constructor(
41
@IQuickInputService private readonly quickInputService: IQuickInputService,
42
@IChatService private readonly chatService: IChatService,
43
@IInstantiationService private readonly instantiationService: IInstantiationService,
44
) {
45
super();
46
}
47
48
get enabled(): boolean {
49
return !!this.chatService.isEnabled(ChatAgentLocation.Panel);
50
}
51
52
get focused(): boolean {
53
const widget = this._input?.widget as HTMLElement | undefined;
54
if (!widget) {
55
return false;
56
}
57
return dom.isAncestorOfActiveElement(widget);
58
}
59
60
toggle(options?: IQuickChatOpenOptions): void {
61
// If the input is already shown, hide it. This provides a toggle behavior of the quick
62
// pick. This should not happen when there is a query.
63
if (this.focused && !options?.query) {
64
this.close();
65
} else {
66
this.open(options);
67
// If this is a partial query, the value should be cleared when closed as otherwise it
68
// would remain for the next time the quick chat is opened in any context.
69
if (options?.isPartialQuery) {
70
const disposable = this._store.add(Event.once(this.onDidClose)(() => {
71
this._currentChat?.clearValue();
72
this._store.delete(disposable);
73
}));
74
}
75
}
76
}
77
78
open(options?: IQuickChatOpenOptions): void {
79
if (this._input) {
80
if (this._currentChat && options?.query) {
81
this._currentChat.focus();
82
this._currentChat.setValue(options.query, options.selection);
83
if (!options.isPartialQuery) {
84
this._currentChat.acceptInput();
85
}
86
return;
87
}
88
return this.focus();
89
}
90
91
const disposableStore = new DisposableStore();
92
93
this._input = this.quickInputService.createQuickWidget();
94
this._input.contextKey = 'chatInputVisible';
95
this._input.ignoreFocusOut = true;
96
disposableStore.add(this._input);
97
98
this._container ??= dom.$('.interactive-session');
99
this._input.widget = this._container;
100
101
this._input.show();
102
if (!this._currentChat) {
103
this._currentChat = this.instantiationService.createInstance(QuickChat);
104
105
// show needs to come after the quickpick is shown
106
this._currentChat.render(this._container);
107
} else {
108
this._currentChat.show();
109
}
110
111
disposableStore.add(this._input.onDidHide(() => {
112
disposableStore.dispose();
113
this._currentChat!.hide();
114
this._input = undefined;
115
this._onDidClose.fire();
116
}));
117
118
this._currentChat.focus();
119
120
if (options?.query) {
121
this._currentChat.setValue(options.query, options.selection);
122
if (!options.isPartialQuery) {
123
this._currentChat.acceptInput();
124
}
125
}
126
}
127
focus(): void {
128
this._currentChat?.focus();
129
}
130
close(): void {
131
this._input?.dispose();
132
this._input = undefined;
133
}
134
async openInChatView(): Promise<void> {
135
await this._currentChat?.openChatView();
136
this.close();
137
}
138
}
139
140
class QuickChat extends Disposable {
141
// TODO@TylerLeonhardt: be responsive to window size
142
static DEFAULT_MIN_HEIGHT = 200;
143
private static readonly DEFAULT_HEIGHT_OFFSET = 100;
144
145
private widget!: ChatWidget;
146
private sash!: Sash;
147
private model: ChatModel | undefined;
148
private _currentQuery: string | undefined;
149
private readonly maintainScrollTimer: MutableDisposable<IDisposable> = this._register(new MutableDisposable<IDisposable>());
150
private _deferUpdatingDynamicLayout: boolean = false;
151
152
constructor(
153
@IInstantiationService private readonly instantiationService: IInstantiationService,
154
@IContextKeyService private readonly contextKeyService: IContextKeyService,
155
@IChatService private readonly chatService: IChatService,
156
@ILayoutService private readonly layoutService: ILayoutService,
157
@IViewsService private readonly viewsService: IViewsService,
158
) {
159
super();
160
}
161
162
clear() {
163
this.model?.dispose();
164
this.model = undefined;
165
this.updateModel();
166
this.widget.inputEditor.setValue('');
167
}
168
169
focus(selection?: Selection): void {
170
if (this.widget) {
171
this.widget.focusInput();
172
const value = this.widget.inputEditor.getValue();
173
if (value) {
174
this.widget.inputEditor.setSelection(selection ?? {
175
startLineNumber: 1,
176
startColumn: 1,
177
endLineNumber: 1,
178
endColumn: value.length + 1
179
});
180
}
181
}
182
}
183
184
hide(): void {
185
this.widget.setVisible(false);
186
// Maintain scroll position for a short time so that if the user re-shows the chat
187
// the same scroll position will be used.
188
this.maintainScrollTimer.value = disposableTimeout(() => {
189
// At this point, clear this mutable disposable which will be our signal that
190
// the timer has expired and we should stop maintaining scroll position
191
this.maintainScrollTimer.clear();
192
}, 30 * 1000); // 30 seconds
193
}
194
195
show(): void {
196
this.widget.setVisible(true);
197
// If the mutable disposable is set, then we are keeping the existing scroll position
198
// so we should not update the layout.
199
if (this._deferUpdatingDynamicLayout) {
200
this._deferUpdatingDynamicLayout = false;
201
this.widget.updateDynamicChatTreeItemLayout(2, this.maxHeight);
202
}
203
if (!this.maintainScrollTimer.value) {
204
this.widget.layoutDynamicChatTreeItemMode();
205
}
206
}
207
208
render(parent: HTMLElement): void {
209
if (this.widget) {
210
// NOTE: if this changes, we need to make sure disposables in this function are tracked differently.
211
throw new Error('Cannot render quick chat twice');
212
}
213
const scopedInstantiationService = this._register(this.instantiationService.createChild(
214
new ServiceCollection([
215
IContextKeyService,
216
this._register(this.contextKeyService.createScoped(parent))
217
])
218
));
219
this.widget = this._register(
220
scopedInstantiationService.createInstance(
221
ChatWidget,
222
ChatAgentLocation.Panel,
223
{ isQuickChat: true },
224
{ autoScroll: true, renderInputOnTop: true, renderStyle: 'compact', menus: { inputSideToolbar: MenuId.ChatInputSide, telemetrySource: 'chatQuick' }, enableImplicitContext: true },
225
{
226
listForeground: quickInputForeground,
227
listBackground: quickInputBackground,
228
overlayBackground: EDITOR_DRAG_AND_DROP_BACKGROUND,
229
inputEditorBackground: inputBackground,
230
resultEditorBackground: editorBackground
231
}));
232
this.widget.render(parent);
233
this.widget.setVisible(true);
234
this.widget.setDynamicChatTreeItemLayout(2, this.maxHeight);
235
this.updateModel();
236
this.sash = this._register(new Sash(parent, { getHorizontalSashTop: () => parent.offsetHeight }, { orientation: Orientation.HORIZONTAL }));
237
this.registerListeners(parent);
238
}
239
240
private get maxHeight(): number {
241
return this.layoutService.mainContainerDimension.height - QuickChat.DEFAULT_HEIGHT_OFFSET;
242
}
243
244
private registerListeners(parent: HTMLElement): void {
245
this._register(this.layoutService.onDidLayoutMainContainer(() => {
246
if (this.widget.visible) {
247
this.widget.updateDynamicChatTreeItemLayout(2, this.maxHeight);
248
} else {
249
// If the chat is not visible, then we should defer updating the layout
250
// because it relies on offsetHeight which only works correctly
251
// when the chat is visible.
252
this._deferUpdatingDynamicLayout = true;
253
}
254
}));
255
this._register(this.widget.inputEditor.onDidChangeModelContent((e) => {
256
this._currentQuery = this.widget.inputEditor.getValue();
257
}));
258
this._register(this.widget.onDidClear(() => this.clear()));
259
this._register(this.widget.onDidChangeHeight((e) => this.sash.layout()));
260
const width = parent.offsetWidth;
261
this._register(this.sash.onDidStart(() => {
262
this.widget.isDynamicChatTreeItemLayoutEnabled = false;
263
}));
264
this._register(this.sash.onDidChange((e) => {
265
if (e.currentY < QuickChat.DEFAULT_MIN_HEIGHT || e.currentY > this.maxHeight) {
266
return;
267
}
268
this.widget.layout(e.currentY, width);
269
this.sash.layout();
270
}));
271
this._register(this.sash.onDidReset(() => {
272
this.widget.isDynamicChatTreeItemLayoutEnabled = true;
273
this.widget.layoutDynamicChatTreeItemMode();
274
}));
275
}
276
277
async acceptInput() {
278
return this.widget.acceptInput();
279
}
280
281
async openChatView(): Promise<void> {
282
const widget = await showChatView(this.viewsService);
283
if (!widget?.viewModel || !this.model) {
284
return;
285
}
286
287
for (const request of this.model.getRequests()) {
288
if (request.response?.response.value || request.response?.result) {
289
290
291
const message: IChatProgress[] = [];
292
for (const item of request.response.response.value) {
293
if (item.kind === 'textEditGroup') {
294
for (const group of item.edits) {
295
message.push({
296
kind: 'textEdit',
297
edits: group,
298
uri: item.uri
299
});
300
}
301
} else if (item.kind === 'notebookEditGroup') {
302
for (const group of item.edits) {
303
if (isCellTextEditOperation(group)) {
304
message.push({
305
kind: 'textEdit',
306
edits: [group.edit],
307
uri: group.uri
308
});
309
} else {
310
message.push({
311
kind: 'notebookEdit',
312
edits: [group],
313
uri: item.uri
314
});
315
}
316
}
317
} else {
318
message.push(item);
319
}
320
}
321
322
this.chatService.addCompleteRequest(widget.viewModel.sessionId,
323
request.message as IParsedChatRequest,
324
request.variableData,
325
request.attempt,
326
{
327
message,
328
result: request.response.result,
329
followups: request.response.followups
330
});
331
} else if (request.message) {
332
333
}
334
}
335
336
const value = this.widget.inputEditor.getValue();
337
if (value) {
338
widget.inputEditor.setValue(value);
339
}
340
widget.focusInput();
341
}
342
343
setValue(value: string, selection?: Selection): void {
344
this.widget.inputEditor.setValue(value);
345
this.focus(selection);
346
}
347
348
clearValue(): void {
349
this.widget.inputEditor.setValue('');
350
}
351
352
private updateModel(): void {
353
this.model ??= this.chatService.startSession(ChatAgentLocation.Panel, CancellationToken.None);
354
if (!this.model) {
355
throw new Error('Could not start chat session');
356
}
357
358
this.widget.setModel(this.model, { inputValue: this._currentQuery });
359
}
360
}
361
362