Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/inlineChat/browser/inlineChatSessionServiceImpl.ts
5221 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
import { Emitter, Event } from '../../../../base/common/event.js';
6
import { Disposable, dispose, DisposableStore, toDisposable } from '../../../../base/common/lifecycle.js';
7
import { ResourceMap } from '../../../../base/common/map.js';
8
import { autorun, observableFromEvent } from '../../../../base/common/observable.js';
9
import { isEqual } from '../../../../base/common/resources.js';
10
import { URI } from '../../../../base/common/uri.js';
11
import { IActiveCodeEditor, isCodeEditor, isCompositeEditor, isDiffEditor } from '../../../../editor/browser/editorBrowser.js';
12
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
13
import { localize, localize2 } from '../../../../nls.js';
14
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
15
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
16
import { IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
17
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
18
import { IInstantiationService, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
19
import { ILogService } from '../../../../platform/log/common/log.js';
20
import { observableConfigValue } from '../../../../platform/observable/common/platformObservableUtils.js';
21
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
22
import { IEditorService } from '../../../services/editor/common/editorService.js';
23
import { IChatAgentService } from '../../chat/common/participants/chatAgents.js';
24
import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js';
25
import { ModifiedFileEntryState } from '../../chat/common/editing/chatEditingService.js';
26
import { IChatService } from '../../chat/common/chatService/chatService.js';
27
import { ChatAgentLocation } from '../../chat/common/constants.js';
28
import { ILanguageModelToolsService, IToolData, ToolDataSource } from '../../chat/common/tools/languageModelToolsService.js';
29
import { CTX_INLINE_CHAT_HAS_AGENT2, CTX_INLINE_CHAT_HAS_NOTEBOOK_AGENT, CTX_INLINE_CHAT_POSSIBLE, InlineChatConfigKeys } from '../common/inlineChat.js';
30
import { askInPanelChat, IInlineChatSession2, IInlineChatSessionService } from './inlineChatSessionService.js';
31
32
export class InlineChatError extends Error {
33
static readonly code = 'InlineChatError';
34
constructor(message: string) {
35
super(message);
36
this.name = InlineChatError.code;
37
}
38
}
39
40
export class InlineChatSessionServiceImpl implements IInlineChatSessionService {
41
42
declare _serviceBrand: undefined;
43
44
private readonly _store = new DisposableStore();
45
private readonly _sessions = new ResourceMap<IInlineChatSession2>();
46
47
private readonly _onWillStartSession = this._store.add(new Emitter<IActiveCodeEditor>());
48
readonly onWillStartSession: Event<IActiveCodeEditor> = this._onWillStartSession.event;
49
50
private readonly _onDidChangeSessions = this._store.add(new Emitter<this>());
51
readonly onDidChangeSessions: Event<this> = this._onDidChangeSessions.event;
52
53
constructor(
54
@IChatService private readonly _chatService: IChatService,
55
@IChatAgentService chatAgentService: IChatAgentService,
56
) {
57
// Listen for agent changes and dispose all sessions when there is no agent
58
const agentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.EditorInline));
59
this._store.add(autorun(r => {
60
const agent = agentObs.read(r);
61
if (!agent) {
62
// No agent available, dispose all sessions
63
dispose(this._sessions.values());
64
this._sessions.clear();
65
}
66
}));
67
}
68
69
dispose() {
70
this._store.dispose();
71
}
72
73
74
createSession(editor: IActiveCodeEditor): IInlineChatSession2 {
75
const uri = editor.getModel().uri;
76
77
if (this._sessions.has(uri)) {
78
throw new Error('Session already exists');
79
}
80
81
this._onWillStartSession.fire(editor);
82
83
const chatModelRef = this._chatService.startSession(ChatAgentLocation.EditorInline, { canUseTools: false /* SEE https://github.com/microsoft/vscode/issues/279946 */ });
84
const chatModel = chatModelRef.object;
85
chatModel.startEditingSession(false);
86
87
const store = new DisposableStore();
88
store.add(toDisposable(() => {
89
this._chatService.cancelCurrentRequestForSession(chatModel.sessionResource);
90
chatModel.editingSession?.reject();
91
this._sessions.delete(uri);
92
this._onDidChangeSessions.fire(this);
93
}));
94
store.add(chatModelRef);
95
96
store.add(autorun(r => {
97
98
const entries = chatModel.editingSession?.entries.read(r);
99
if (!entries?.length) {
100
return;
101
}
102
103
const state = entries.find(entry => isEqual(entry.modifiedURI, uri))?.state.read(r);
104
if (state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected) {
105
const response = chatModel.getRequests().at(-1)?.response;
106
if (response) {
107
this._chatService.notifyUserAction({
108
sessionResource: response.session.sessionResource,
109
requestId: response.requestId,
110
agentId: response.agent?.id,
111
command: response.slashCommand?.name,
112
result: response.result,
113
action: {
114
kind: 'inlineChat',
115
action: state === ModifiedFileEntryState.Accepted ? 'accepted' : 'discarded'
116
}
117
});
118
}
119
}
120
121
const allSettled = entries.every(entry => {
122
const state = entry.state.read(r);
123
return (state === ModifiedFileEntryState.Accepted || state === ModifiedFileEntryState.Rejected)
124
&& !entry.isCurrentlyBeingModifiedBy.read(r);
125
});
126
127
if (allSettled && !chatModel.requestInProgress.read(undefined)) {
128
// self terminate
129
store.dispose();
130
}
131
}));
132
133
const result: IInlineChatSession2 = {
134
uri,
135
initialPosition: editor.getSelection().getStartPosition().delta(-1), /* one line above selection start */
136
initialSelection: editor.getSelection(),
137
chatModel,
138
editingSession: chatModel.editingSession!,
139
dispose: store.dispose.bind(store)
140
};
141
this._sessions.set(uri, result);
142
this._onDidChangeSessions.fire(this);
143
return result;
144
}
145
146
getSessionByTextModel(uri: URI): IInlineChatSession2 | undefined {
147
let result = this._sessions.get(uri);
148
if (!result) {
149
// no direct session, try to find an editing session which has a file entry for the uri
150
for (const [_, candidate] of this._sessions) {
151
const entry = candidate.editingSession.getEntry(uri);
152
if (entry) {
153
result = candidate;
154
break;
155
}
156
}
157
}
158
return result;
159
}
160
161
getSessionBySessionUri(sessionResource: URI): IInlineChatSession2 | undefined {
162
for (const session of this._sessions.values()) {
163
if (isEqual(session.chatModel.sessionResource, sessionResource)) {
164
return session;
165
}
166
}
167
return undefined;
168
}
169
}
170
171
export class InlineChatEnabler {
172
173
static Id = 'inlineChat.enabler';
174
175
private readonly _ctxHasProvider2: IContextKey<boolean>;
176
private readonly _ctxHasNotebookProvider: IContextKey<boolean>;
177
private readonly _ctxPossible: IContextKey<boolean>;
178
179
private readonly _store = new DisposableStore();
180
181
constructor(
182
@IContextKeyService contextKeyService: IContextKeyService,
183
@IChatAgentService chatAgentService: IChatAgentService,
184
@IEditorService editorService: IEditorService,
185
@IConfigurationService configService: IConfigurationService,
186
) {
187
this._ctxHasProvider2 = CTX_INLINE_CHAT_HAS_AGENT2.bindTo(contextKeyService);
188
this._ctxHasNotebookProvider = CTX_INLINE_CHAT_HAS_NOTEBOOK_AGENT.bindTo(contextKeyService);
189
this._ctxPossible = CTX_INLINE_CHAT_POSSIBLE.bindTo(contextKeyService);
190
191
const agentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.EditorInline));
192
const notebookAgentObs = observableFromEvent(this, chatAgentService.onDidChangeAgents, () => chatAgentService.getDefaultAgent(ChatAgentLocation.Notebook));
193
const notebookAgentConfigObs = observableConfigValue(InlineChatConfigKeys.notebookAgent, false, configService);
194
195
this._store.add(autorun(r => {
196
const agent = agentObs.read(r);
197
if (!agent) {
198
this._ctxHasProvider2.reset();
199
} else {
200
this._ctxHasProvider2.set(true);
201
}
202
}));
203
204
this._store.add(autorun(r => {
205
this._ctxHasNotebookProvider.set(notebookAgentConfigObs.read(r) && !!notebookAgentObs.read(r));
206
}));
207
208
const updateEditor = () => {
209
const ctrl = editorService.activeEditorPane?.getControl();
210
const isCodeEditorLike = isCodeEditor(ctrl) || isDiffEditor(ctrl) || isCompositeEditor(ctrl);
211
this._ctxPossible.set(isCodeEditorLike);
212
};
213
214
this._store.add(editorService.onDidActiveEditorChange(updateEditor));
215
updateEditor();
216
}
217
218
dispose() {
219
this._ctxPossible.reset();
220
this._ctxHasProvider2.reset();
221
this._store.dispose();
222
}
223
}
224
225
226
export class InlineChatEscapeToolContribution extends Disposable {
227
228
static readonly Id = 'inlineChat.escapeTool';
229
230
static readonly DONT_ASK_AGAIN_KEY = 'inlineChat.dontAskMoveToPanelChat';
231
232
private static readonly _data: IToolData = {
233
id: 'inline_chat_exit',
234
source: ToolDataSource.Internal,
235
canBeReferencedInPrompt: false,
236
alwaysDisplayInputOutput: false,
237
displayName: localize('name', "Inline Chat to Panel Chat"),
238
modelDescription: 'Moves the inline chat session to the richer panel chat which supports edits across files, creating and deleting files, multi-turn conversations between the user and the assistant, and access to more IDE tools, like retrieve problems, interact with source control, run terminal commands etc.',
239
};
240
241
constructor(
242
@ILanguageModelToolsService lmTools: ILanguageModelToolsService,
243
@IInlineChatSessionService inlineChatSessionService: IInlineChatSessionService,
244
@IDialogService dialogService: IDialogService,
245
@ICodeEditorService codeEditorService: ICodeEditorService,
246
@IChatService chatService: IChatService,
247
@ILogService logService: ILogService,
248
@IStorageService storageService: IStorageService,
249
@IInstantiationService instaService: IInstantiationService,
250
) {
251
252
super();
253
254
this._store.add(lmTools.registerTool(InlineChatEscapeToolContribution._data, {
255
invoke: async (invocation, _tokenCountFn, _progress, _token) => {
256
257
const sessionResource = invocation.context?.sessionResource;
258
259
if (!sessionResource) {
260
logService.warn('InlineChatEscapeToolContribution: no sessionId in tool invocation context');
261
return { content: [{ kind: 'text', value: 'Cancel' }] };
262
}
263
264
const session = inlineChatSessionService.getSessionBySessionUri(sessionResource);
265
266
if (!session) {
267
logService.warn(`InlineChatEscapeToolContribution: no session found for id ${sessionResource}`);
268
return { content: [{ kind: 'text', value: 'Cancel' }] };
269
}
270
271
const dontAskAgain = storageService.getBoolean(InlineChatEscapeToolContribution.DONT_ASK_AGAIN_KEY, StorageScope.PROFILE);
272
273
let result: { confirmed: boolean; checkboxChecked?: boolean };
274
if (dontAskAgain !== undefined) {
275
// Use previously stored user preference: true = 'Continue in Chat view', false = 'Rephrase' (Cancel)
276
result = { confirmed: dontAskAgain, checkboxChecked: false };
277
} else {
278
result = await dialogService.confirm({
279
type: 'question',
280
title: localize('confirm.title', "Do you want to continue in Chat view?"),
281
message: localize('confirm', "Do you want to continue in Chat view?"),
282
detail: localize('confirm.detail', "Inline chat is designed for making single-file code changes. Continue your request in the Chat view or rephrase it for inline chat."),
283
primaryButton: localize('confirm.yes', "Continue in Chat view"),
284
cancelButton: localize('confirm.cancel', "Cancel"),
285
checkbox: { label: localize('chat.remove.confirmation.checkbox', "Don't ask again"), checked: false },
286
});
287
}
288
289
const editor = codeEditorService.getFocusedCodeEditor();
290
291
if (!editor || result.confirmed) {
292
logService.trace('InlineChatEscapeToolContribution: moving session to panel chat');
293
await instaService.invokeFunction(askInPanelChat, session.chatModel.getRequests().at(-1)!, session.chatModel.inputModel.state.get());
294
session.dispose();
295
296
} else {
297
logService.trace('InlineChatEscapeToolContribution: rephrase prompt');
298
const lastRequest = session.chatModel.getRequests().at(-1)!;
299
chatService.removeRequest(session.chatModel.sessionResource, lastRequest.id);
300
session.chatModel.inputModel.setState({ inputText: lastRequest.message.text });
301
}
302
303
if (result.checkboxChecked) {
304
storageService.store(InlineChatEscapeToolContribution.DONT_ASK_AGAIN_KEY, result.confirmed, StorageScope.PROFILE, StorageTarget.USER);
305
logService.trace('InlineChatEscapeToolContribution: stored don\'t ask again preference');
306
}
307
308
return { content: [{ kind: 'text', value: 'Success' }] };
309
}
310
}));
311
}
312
}
313
314
registerAction2(class ResetMoveToPanelChatChoice extends Action2 {
315
constructor() {
316
super({
317
id: 'inlineChat.resetMoveToPanelChatChoice',
318
precondition: ChatContextKeys.Setup.hidden.negate(),
319
title: localize2('resetChoice.label', "Reset Choice for 'Move Inline Chat to Panel Chat'"),
320
f1: true
321
});
322
}
323
run(accessor: ServicesAccessor) {
324
accessor.get(IStorageService).remove(InlineChatEscapeToolContribution.DONT_ASK_AGAIN_KEY, StorageScope.PROFILE);
325
}
326
});
327
328