Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/chat/browser/chatViewPane.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 { $, getWindow } from '../../../../base/browser/dom.js';
7
import { CancellationToken } from '../../../../base/common/cancellation.js';
8
import { DisposableStore } from '../../../../base/common/lifecycle.js';
9
import { MarshalledId } from '../../../../base/common/marshallingIds.js';
10
import { Schemas } from '../../../../base/common/network.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 { IHoverService } from '../../../../platform/hover/browser/hover.js';
16
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
17
import { ServiceCollection } from '../../../../platform/instantiation/common/serviceCollection.js';
18
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
19
import { ILayoutService } from '../../../../platform/layout/browser/layoutService.js';
20
import { ILogService } from '../../../../platform/log/common/log.js';
21
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
22
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
23
import { editorBackground } from '../../../../platform/theme/common/colorRegistry.js';
24
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
25
import { IViewPaneOptions, ViewPane } from '../../../browser/parts/views/viewPane.js';
26
import { Memento } from '../../../common/memento.js';
27
import { SIDE_BAR_FOREGROUND } from '../../../common/theme.js';
28
import { IViewDescriptorService, ViewContainerLocation } from '../../../common/views.js';
29
import { IChatViewTitleActionContext } from '../common/chatActions.js';
30
import { IChatAgentService } from '../common/chatAgents.js';
31
import { ChatContextKeys } from '../common/chatContextKeys.js';
32
import { IChatModel } from '../common/chatModel.js';
33
import { CHAT_PROVIDER_ID } from '../common/chatParticipantContribTypes.js';
34
import { IChatService } from '../common/chatService.js';
35
import { IChatSessionsService, IChatSessionsExtensionPoint } from '../common/chatSessionsService.js';
36
import { ChatAgentLocation, ChatModeKind } from '../common/constants.js';
37
import { ChatSessionUri } from '../common/chatUri.js';
38
import { ChatWidget, IChatViewState } from './chatWidget.js';
39
import { ChatViewWelcomeController, IViewWelcomeDelegate } from './viewsWelcome/chatViewWelcomeController.js';
40
41
interface IViewPaneState extends IChatViewState {
42
sessionId?: string;
43
hasMigratedCurrentSession?: boolean;
44
}
45
46
export const CHAT_SIDEBAR_OLD_VIEW_PANEL_ID = 'workbench.panel.chatSidebar';
47
export const CHAT_SIDEBAR_PANEL_ID = 'workbench.panel.chat';
48
export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate {
49
private _widget!: ChatWidget;
50
get widget(): ChatWidget { return this._widget; }
51
52
private readonly modelDisposables = this._register(new DisposableStore());
53
private memento: Memento;
54
private readonly viewState: IViewPaneState;
55
56
private _restoringSession: Promise<void> | undefined;
57
58
constructor(
59
private readonly chatOptions: { location: ChatAgentLocation.Panel },
60
options: IViewPaneOptions,
61
@IKeybindingService keybindingService: IKeybindingService,
62
@IContextMenuService contextMenuService: IContextMenuService,
63
@IConfigurationService configurationService: IConfigurationService,
64
@IContextKeyService contextKeyService: IContextKeyService,
65
@IViewDescriptorService viewDescriptorService: IViewDescriptorService,
66
@IInstantiationService instantiationService: IInstantiationService,
67
@IOpenerService openerService: IOpenerService,
68
@IThemeService themeService: IThemeService,
69
@IHoverService hoverService: IHoverService,
70
@IStorageService private readonly storageService: IStorageService,
71
@IChatService private readonly chatService: IChatService,
72
@IChatAgentService private readonly chatAgentService: IChatAgentService,
73
@ILogService private readonly logService: ILogService,
74
@ILayoutService private readonly layoutService: ILayoutService,
75
@IChatSessionsService private readonly chatSessionsService: IChatSessionsService,
76
) {
77
super(options, keybindingService, contextMenuService, configurationService, contextKeyService, viewDescriptorService, instantiationService, openerService, themeService, hoverService);
78
79
// View state for the ViewPane is currently global per-provider basically, but some other strictly per-model state will require a separate memento.
80
this.memento = new Memento('interactive-session-view-' + CHAT_PROVIDER_ID, this.storageService);
81
this.viewState = this.memento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE) as IViewPaneState;
82
83
if (this.chatOptions.location === ChatAgentLocation.Panel && !this.viewState.hasMigratedCurrentSession) {
84
const editsMemento = new Memento('interactive-session-view-' + CHAT_PROVIDER_ID + `-edits`, this.storageService);
85
const lastEditsState = editsMemento.getMemento(StorageScope.WORKSPACE, StorageTarget.MACHINE) as IViewPaneState;
86
if (lastEditsState.sessionId) {
87
this.logService.trace(`ChatViewPane: last edits session was ${lastEditsState.sessionId}`);
88
if (!this.chatService.isPersistedSessionEmpty(lastEditsState.sessionId)) {
89
this.logService.info(`ChatViewPane: migrating ${lastEditsState.sessionId} to unified view`);
90
this.viewState.sessionId = lastEditsState.sessionId;
91
this.viewState.inputValue = lastEditsState.inputValue;
92
this.viewState.inputState = {
93
...lastEditsState.inputState,
94
chatMode: lastEditsState.inputState?.chatMode ?? ChatModeKind.Edit
95
};
96
this.viewState.hasMigratedCurrentSession = true;
97
}
98
}
99
}
100
101
this._register(this.chatAgentService.onDidChangeAgents(() => {
102
if (this.chatAgentService.getDefaultAgent(this.chatOptions?.location)) {
103
if (!this._widget?.viewModel && !this._restoringSession) {
104
const info = this.getTransferredOrPersistedSessionInfo();
105
this._restoringSession =
106
(info.sessionId ? this.chatService.getOrRestoreSession(info.sessionId) : Promise.resolve(undefined)).then(async model => {
107
if (!this._widget) {
108
// renderBody has not been called yet
109
return;
110
}
111
112
// The widget may be hidden at this point, because welcome views were allowed. Use setVisible to
113
// avoid doing a render while the widget is hidden. This is changing the condition in `shouldShowWelcome`
114
// so it should fire onDidChangeViewWelcomeState.
115
const wasVisible = this._widget.visible;
116
try {
117
this._widget.setVisible(false);
118
await this.updateModel(model, info.inputValue || info.mode ? { inputState: { chatMode: info.mode }, inputValue: info.inputValue } : undefined);
119
} finally {
120
this.widget.setVisible(wasVisible);
121
}
122
});
123
this._restoringSession.finally(() => this._restoringSession = undefined);
124
}
125
}
126
127
this._onDidChangeViewWelcomeState.fire();
128
}));
129
130
// Location context key
131
ChatContextKeys.panelLocation.bindTo(contextKeyService).set(viewDescriptorService.getViewLocationById(options.id) ?? ViewContainerLocation.AuxiliaryBar);
132
}
133
134
override getActionsContext(): IChatViewTitleActionContext | undefined {
135
return this.widget?.viewModel ? {
136
sessionId: this.widget.viewModel.sessionId,
137
$mid: MarshalledId.ChatViewContext
138
} : undefined;
139
}
140
141
private async updateModel(model?: IChatModel | undefined, viewState?: IChatViewState): Promise<void> {
142
this.modelDisposables.clear();
143
144
model = model ?? (this.chatService.transferredSessionData?.sessionId && this.chatService.transferredSessionData?.location === this.chatOptions.location
145
? await this.chatService.getOrRestoreSession(this.chatService.transferredSessionData.sessionId)
146
: this.chatService.startSession(this.chatOptions.location, CancellationToken.None));
147
if (!model) {
148
throw new Error('Could not start chat session');
149
}
150
151
if (viewState) {
152
this.updateViewState(viewState);
153
}
154
155
this.viewState.sessionId = model.sessionId;
156
this._widget.setModel(model, { ...this.viewState });
157
158
// Update the toolbar context with new sessionId
159
this.updateActions();
160
}
161
162
override shouldShowWelcome(): boolean {
163
const noPersistedSessions = !this.chatService.hasSessions();
164
const hasCoreAgent = this.chatAgentService.getAgents().some(agent => agent.isCore && agent.locations.includes(this.chatOptions.location));
165
const hasDefaultAgent = this.chatAgentService.getDefaultAgent(this.chatOptions.location) !== undefined; // only false when Hide Copilot has run and unregistered the setup agents
166
const shouldShow = !hasCoreAgent && (!hasDefaultAgent || !this._widget?.viewModel && noPersistedSessions);
167
this.logService.trace(`ChatViewPane#shouldShowWelcome(${this.chatOptions.location}) = ${shouldShow}: hasCoreAgent=${hasCoreAgent} hasDefaultAgent=${hasDefaultAgent} || noViewModel=${!this._widget?.viewModel} && noPersistedSessions=${noPersistedSessions}`);
168
return !!shouldShow;
169
}
170
171
private getTransferredOrPersistedSessionInfo(): { sessionId?: string; inputValue?: string; mode?: ChatModeKind } {
172
if (this.chatService.transferredSessionData?.location === this.chatOptions.location) {
173
const sessionId = this.chatService.transferredSessionData.sessionId;
174
return {
175
sessionId,
176
inputValue: this.chatService.transferredSessionData.inputValue,
177
mode: this.chatService.transferredSessionData.mode
178
};
179
} else {
180
return { sessionId: this.viewState.sessionId };
181
}
182
}
183
184
protected override async renderBody(parent: HTMLElement): Promise<void> {
185
super.renderBody(parent);
186
187
this._register(this.instantiationService.createInstance(ChatViewWelcomeController, parent, this, this.chatOptions.location));
188
189
const scopedInstantiationService = this._register(this.instantiationService.createChild(new ServiceCollection([IContextKeyService, this.scopedContextKeyService])));
190
const locationBasedColors = this.getLocationBasedColors();
191
const editorOverflowNode = this.layoutService.getContainer(getWindow(parent)).appendChild($('.chat-editor-overflow.monaco-editor'));
192
this._register({ dispose: () => editorOverflowNode.remove() });
193
194
this._widget = this._register(scopedInstantiationService.createInstance(
195
ChatWidget,
196
this.chatOptions.location,
197
{ viewId: this.id },
198
{
199
autoScroll: mode => mode !== ChatModeKind.Ask,
200
renderFollowups: this.chatOptions.location === ChatAgentLocation.Panel,
201
supportsFileReferences: true,
202
rendererOptions: {
203
renderTextEditsAsSummary: (uri) => {
204
return true;
205
},
206
referencesExpandedWhenEmptyResponse: false,
207
progressMessageAtBottomOfResponse: mode => mode !== ChatModeKind.Ask,
208
},
209
editorOverflowWidgetsDomNode: editorOverflowNode,
210
enableImplicitContext: this.chatOptions.location === ChatAgentLocation.Panel,
211
enableWorkingSet: 'explicit',
212
supportsChangingModes: true,
213
},
214
{
215
listForeground: SIDE_BAR_FOREGROUND,
216
listBackground: locationBasedColors.background,
217
overlayBackground: locationBasedColors.overlayBackground,
218
inputEditorBackground: locationBasedColors.background,
219
resultEditorBackground: editorBackground,
220
221
}));
222
this._register(this.onDidChangeBodyVisibility(visible => {
223
this._widget.setVisible(visible);
224
}));
225
this._register(this._widget.onDidClear(() => this.clear()));
226
this._widget.render(parent);
227
228
const info = this.getTransferredOrPersistedSessionInfo();
229
const model = info.sessionId ? await this.chatService.getOrRestoreSession(info.sessionId) : undefined;
230
231
await this.updateModel(model, info.inputValue || info.mode ? { inputState: { chatMode: info.mode }, inputValue: info.inputValue } : undefined);
232
}
233
234
acceptInput(query?: string): void {
235
this._widget.acceptInput(query);
236
}
237
238
private async clear(): Promise<void> {
239
if (this.widget.viewModel) {
240
await this.chatService.clearSession(this.widget.viewModel.sessionId);
241
}
242
243
// Grab the widget's latest view state because it will be loaded back into the widget
244
this.updateViewState();
245
await this.updateModel(undefined);
246
247
// Update the toolbar context with new sessionId
248
this.updateActions();
249
}
250
251
async loadSession(sessionId: string | URI, viewState?: IChatViewState): Promise<void> {
252
if (this.widget.viewModel) {
253
await this.chatService.clearSession(this.widget.viewModel.sessionId);
254
}
255
256
// Handle locking for contributed chat sessions
257
if (URI.isUri(sessionId) && sessionId.scheme === Schemas.vscodeChatSession) {
258
const parsed = ChatSessionUri.parse(sessionId);
259
if (parsed?.chatSessionType) {
260
await this.chatSessionsService.canResolveContentProvider(parsed.chatSessionType);
261
const contributions = this.chatSessionsService.getAllChatSessionContributions();
262
const contribution = contributions.find((c: IChatSessionsExtensionPoint) => c.type === parsed.chatSessionType);
263
if (contribution) {
264
this.widget.lockToCodingAgent(contribution.name, contribution.displayName, contribution.type);
265
}
266
}
267
}
268
269
const newModel = await (URI.isUri(sessionId) ? this.chatService.loadSessionForResource(sessionId, ChatAgentLocation.Panel, CancellationToken.None) : this.chatService.getOrRestoreSession(sessionId));
270
await this.updateModel(newModel, viewState);
271
}
272
273
focusInput(): void {
274
this._widget.focusInput();
275
}
276
277
override focus(): void {
278
super.focus();
279
this._widget.focusInput();
280
}
281
282
protected override layoutBody(height: number, width: number): void {
283
super.layoutBody(height, width);
284
this._widget.layout(height, width);
285
}
286
287
override saveState(): void {
288
if (this._widget) {
289
// Since input history is per-provider, this is handled by a separate service and not the memento here.
290
// TODO multiple chat views will overwrite each other
291
this._widget.saveState();
292
293
this.updateViewState();
294
this.memento.saveMemento();
295
}
296
297
super.saveState();
298
}
299
300
private updateViewState(viewState?: IChatViewState): void {
301
const newViewState = viewState ?? this._widget.getViewState();
302
for (const [key, value] of Object.entries(newViewState)) {
303
// Assign all props to the memento so they get saved
304
(this.viewState as any)[key] = value;
305
}
306
}
307
}
308
309