Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts
3297 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 assert from 'assert';
7
import { equals } from '../../../../../base/common/arrays.js';
8
import { DeferredPromise, raceCancellation, timeout } from '../../../../../base/common/async.js';
9
import { CancellationToken } from '../../../../../base/common/cancellation.js';
10
import { Emitter, Event } from '../../../../../base/common/event.js';
11
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
12
import { constObservable, IObservable } from '../../../../../base/common/observable.js';
13
import { assertType } from '../../../../../base/common/types.js';
14
import { mock } from '../../../../../base/test/common/mock.js';
15
import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';
16
import { IActiveCodeEditor } from '../../../../../editor/browser/editorBrowser.js';
17
import { IDiffProviderFactoryService } from '../../../../../editor/browser/widget/diffEditor/diffProviderFactoryService.js';
18
import { EditOperation } from '../../../../../editor/common/core/editOperation.js';
19
import { Range } from '../../../../../editor/common/core/range.js';
20
import { EndOfLineSequence, ITextModel } from '../../../../../editor/common/model.js';
21
import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js';
22
import { IModelService } from '../../../../../editor/common/services/model.js';
23
import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';
24
import { TestDiffProviderFactoryService } from '../../../../../editor/test/browser/diff/testDiffProviderFactoryService.js';
25
import { TestCommandService } from '../../../../../editor/test/browser/editorTestServices.js';
26
import { instantiateTestCodeEditor } from '../../../../../editor/test/browser/testCodeEditor.js';
27
import { IAccessibleViewService } from '../../../../../platform/accessibility/browser/accessibleView.js';
28
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
29
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
30
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
31
import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';
32
import { IHoverService } from '../../../../../platform/hover/browser/hover.js';
33
import { NullHoverService } from '../../../../../platform/hover/test/browser/nullHoverService.js';
34
import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js';
35
import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';
36
import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';
37
import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js';
38
import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js';
39
import { IEditorProgressService, IProgressRunner } from '../../../../../platform/progress/common/progress.js';
40
import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';
41
import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js';
42
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
43
import { IView, IViewDescriptorService } from '../../../../common/views.js';
44
import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js';
45
import { NullWorkbenchAssignmentService } from '../../../../services/assignment/test/common/nullAssignmentService.js';
46
import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js';
47
import { TextModelResolverService } from '../../../../services/textmodelResolver/common/textModelResolverService.js';
48
import { IViewsService } from '../../../../services/views/common/viewsService.js';
49
import { TestViewsService, workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js';
50
import { TestContextService, TestExtensionService } from '../../../../test/common/workbenchTestServices.js';
51
import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js';
52
import { IChatAccessibilityService, IChatWidget, IChatWidgetService } from '../../../chat/browser/chat.js';
53
import { ChatInputBoxContentProvider } from '../../../chat/browser/chatEdinputInputContentProvider.js';
54
import { ChatVariablesService } from '../../../chat/browser/chatVariables.js';
55
import { ChatWidgetService } from '../../../chat/browser/chatWidget.js';
56
import { ChatAgentService, IChatAgentData, IChatAgentNameService, IChatAgentService } from '../../../chat/common/chatAgents.js';
57
import { IChatEditingService, IChatEditingSession } from '../../../chat/common/chatEditingService.js';
58
import { IChatEntitlementService } from '../../../chat/common/chatEntitlementService.js';
59
import { IChatModeService } from '../../../chat/common/chatModes.js';
60
import { IChatProgress, IChatService } from '../../../chat/common/chatService.js';
61
import { ChatService } from '../../../chat/common/chatServiceImpl.js';
62
import { ChatSlashCommandService, IChatSlashCommandService } from '../../../chat/common/chatSlashCommands.js';
63
import { IChatVariablesService } from '../../../chat/common/chatVariables.js';
64
import { IChatResponseViewModel } from '../../../chat/common/chatViewModel.js';
65
import { ChatWidgetHistoryService, IChatWidgetHistoryService } from '../../../chat/common/chatWidgetHistoryService.js';
66
import { ChatAgentLocation, ChatModeKind } from '../../../chat/common/constants.js';
67
import { ILanguageModelsService, LanguageModelsService } from '../../../chat/common/languageModels.js';
68
import { ILanguageModelToolsService } from '../../../chat/common/languageModelToolsService.js';
69
import { IPromptPath, IPromptsService } from '../../../chat/common/promptSyntax/service/promptsService.js';
70
import { MockChatModeService } from '../../../chat/test/common/mockChatModeService.js';
71
import { MockLanguageModelToolsService } from '../../../chat/test/common/mockLanguageModelToolsService.js';
72
import { INotebookEditorService } from '../../../notebook/browser/services/notebookEditorService.js';
73
import { RerunAction } from '../../browser/inlineChatActions.js';
74
import { InlineChatController1, State } from '../../browser/inlineChatController.js';
75
import { IInlineChatSessionService } from '../../browser/inlineChatSessionService.js';
76
import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl.js';
77
import { CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatConfigKeys, InlineChatResponseType } from '../../common/inlineChat.js';
78
import { TestWorkerService } from './testWorkerService.js';
79
import { PromptsType } from '../../../chat/common/promptSyntax/promptTypes.js';
80
import { ChatTransferService, IChatTransferService } from '../../../chat/common/chatTransferService.js';
81
import { IMcpService } from '../../../mcp/common/mcpTypes.js';
82
import { TestMcpService } from '../../../mcp/test/common/testMcpService.js';
83
import { IChatLayoutService } from '../../../chat/common/chatLayoutService.js';
84
import { ChatLayoutService } from '../../../chat/browser/chatLayoutService.js';
85
86
suite('InlineChatController', function () {
87
88
const agentData = {
89
extensionId: nullExtensionDescription.identifier,
90
extensionVersion: undefined,
91
publisherDisplayName: '',
92
extensionDisplayName: '',
93
extensionPublisherId: '',
94
// id: 'testEditorAgent',
95
name: 'testEditorAgent',
96
isDefault: true,
97
locations: [ChatAgentLocation.Editor],
98
modes: [ChatModeKind.Ask],
99
metadata: {},
100
slashCommands: [],
101
disambiguation: [],
102
};
103
104
class TestController extends InlineChatController1 {
105
106
static INIT_SEQUENCE: readonly State[] = [State.CREATE_SESSION, State.INIT_UI, State.WAIT_FOR_INPUT];
107
static INIT_SEQUENCE_AUTO_SEND: readonly State[] = [...this.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT];
108
109
110
readonly onDidChangeState: Event<State> = this._onDidEnterState.event;
111
112
readonly states: readonly State[] = [];
113
114
awaitStates(states: readonly State[]): Promise<string | undefined> {
115
const actual: State[] = [];
116
117
return new Promise<string | undefined>((resolve, reject) => {
118
const d = this.onDidChangeState(state => {
119
actual.push(state);
120
if (equals(states, actual)) {
121
d.dispose();
122
resolve(undefined);
123
}
124
});
125
126
setTimeout(() => {
127
d.dispose();
128
resolve(`[${states.join(',')}] <> [${actual.join(',')}]`);
129
}, 1000);
130
});
131
}
132
}
133
134
const store = new DisposableStore();
135
let configurationService: TestConfigurationService;
136
let editor: IActiveCodeEditor;
137
let model: ITextModel;
138
let ctrl: TestController;
139
let contextKeyService: MockContextKeyService;
140
let chatService: IChatService;
141
let chatAgentService: IChatAgentService;
142
let inlineChatSessionService: IInlineChatSessionService;
143
let instaService: TestInstantiationService;
144
145
let chatWidget: IChatWidget;
146
147
setup(function () {
148
149
const serviceCollection = new ServiceCollection(
150
[IConfigurationService, new TestConfigurationService()],
151
[IChatVariablesService, new SyncDescriptor(ChatVariablesService)],
152
[ILogService, new NullLogService()],
153
[ITelemetryService, NullTelemetryService],
154
[IHoverService, NullHoverService],
155
[IExtensionService, new TestExtensionService()],
156
[IContextKeyService, new MockContextKeyService()],
157
[IViewsService, new class extends TestViewsService {
158
override async openView<T extends IView>(id: string, focus?: boolean | undefined): Promise<T | null> {
159
return { widget: chatWidget ?? null } as any;
160
}
161
}()],
162
[IWorkspaceContextService, new TestContextService()],
163
[IChatWidgetHistoryService, new SyncDescriptor(ChatWidgetHistoryService)],
164
[IChatWidgetService, new SyncDescriptor(ChatWidgetService)],
165
[IChatSlashCommandService, new SyncDescriptor(ChatSlashCommandService)],
166
[IChatTransferService, new SyncDescriptor(ChatTransferService)],
167
[IChatService, new SyncDescriptor(ChatService)],
168
[IMcpService, new TestMcpService()],
169
[IChatAgentNameService, new class extends mock<IChatAgentNameService>() {
170
override getAgentNameRestriction(chatAgentData: IChatAgentData): boolean {
171
return false;
172
}
173
}],
174
[IEditorWorkerService, new SyncDescriptor(TestWorkerService)],
175
[IContextKeyService, contextKeyService],
176
[IChatAgentService, new SyncDescriptor(ChatAgentService)],
177
[IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)],
178
[IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)],
179
[ICommandService, new SyncDescriptor(TestCommandService)],
180
[IChatEditingService, new class extends mock<IChatEditingService>() {
181
override editingSessionsObs: IObservable<readonly IChatEditingSession[]> = constObservable([]);
182
}],
183
[IEditorProgressService, new class extends mock<IEditorProgressService>() {
184
override show(total: unknown, delay?: unknown): IProgressRunner {
185
return {
186
total() { },
187
worked(value) { },
188
done() { },
189
};
190
}
191
}],
192
[IChatAccessibilityService, new class extends mock<IChatAccessibilityService>() {
193
override acceptResponse(response: IChatResponseViewModel | undefined, requestId: number): void { }
194
override acceptRequest(): number { return -1; }
195
override acceptElicitation(): void { }
196
}],
197
[IAccessibleViewService, new class extends mock<IAccessibleViewService>() {
198
override getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null {
199
return null;
200
}
201
}],
202
[IConfigurationService, configurationService],
203
[IViewDescriptorService, new class extends mock<IViewDescriptorService>() {
204
override onDidChangeLocation = Event.None;
205
}],
206
[INotebookEditorService, new class extends mock<INotebookEditorService>() {
207
override listNotebookEditors() { return []; }
208
}],
209
[IWorkbenchAssignmentService, new NullWorkbenchAssignmentService()],
210
[ILanguageModelsService, new SyncDescriptor(LanguageModelsService)],
211
[ITextModelService, new SyncDescriptor(TextModelResolverService)],
212
[ILanguageModelToolsService, new SyncDescriptor(MockLanguageModelToolsService)],
213
[IPromptsService, new class extends mock<IPromptsService>() {
214
override async listPromptFiles(type: PromptsType, token: CancellationToken): Promise<readonly IPromptPath[]> {
215
return [];
216
}
217
}],
218
[IChatEntitlementService, new class extends mock<IChatEntitlementService>() { }],
219
[IChatModeService, new SyncDescriptor(MockChatModeService)],
220
[IChatLayoutService, new SyncDescriptor(ChatLayoutService)],
221
);
222
223
instaService = store.add((store.add(workbenchInstantiationService(undefined, store))).createChild(serviceCollection));
224
225
configurationService = instaService.get(IConfigurationService) as TestConfigurationService;
226
configurationService.setUserConfiguration('chat', { editor: { fontSize: 14, fontFamily: 'default' } });
227
228
configurationService.setUserConfiguration('editor', {});
229
230
contextKeyService = instaService.get(IContextKeyService) as MockContextKeyService;
231
chatService = instaService.get(IChatService);
232
chatAgentService = instaService.get(IChatAgentService);
233
234
inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService));
235
236
store.add(instaService.get(ILanguageModelsService) as LanguageModelsService);
237
store.add(instaService.get(IEditorWorkerService) as TestWorkerService);
238
239
store.add(instaService.createInstance(ChatInputBoxContentProvider));
240
241
model = store.add(instaService.get(IModelService).createModel('Hello\nWorld\nHello Again\nHello World\n', null));
242
model.setEOL(EndOfLineSequence.LF);
243
editor = store.add(instantiateTestCodeEditor(instaService, model));
244
245
store.add(chatAgentService.registerDynamicAgent({ id: 'testEditorAgent', ...agentData, }, {
246
async invoke(request, progress, history, token) {
247
progress([{
248
kind: 'textEdit',
249
uri: model.uri,
250
edits: [{
251
range: new Range(1, 1, 1, 1),
252
text: request.message
253
}]
254
}]);
255
return {};
256
},
257
}));
258
259
});
260
261
teardown(function () {
262
store.clear();
263
ctrl?.dispose();
264
});
265
266
// TODO@jrieken re-enable, looks like List/ChatWidget is leaking
267
// ensureNoDisposablesAreLeakedInTestSuite();
268
269
test('creation, not showing anything', function () {
270
ctrl = instaService.createInstance(TestController, editor);
271
assert.ok(ctrl);
272
assert.strictEqual(ctrl.getWidgetPosition(), undefined);
273
});
274
275
test('run (show/hide)', async function () {
276
ctrl = instaService.createInstance(TestController, editor);
277
const actualStates = ctrl.awaitStates(TestController.INIT_SEQUENCE_AUTO_SEND);
278
const run = ctrl.run({ message: 'Hello', autoSend: true });
279
assert.strictEqual(await actualStates, undefined);
280
assert.ok(ctrl.getWidgetPosition() !== undefined);
281
await ctrl.cancelSession();
282
283
await run;
284
285
assert.ok(ctrl.getWidgetPosition() === undefined);
286
});
287
288
test('wholeRange does not expand to whole lines, editor selection default', async function () {
289
290
editor.setSelection(new Range(1, 1, 1, 3));
291
ctrl = instaService.createInstance(TestController, editor);
292
293
ctrl.run({});
294
await Event.toPromise(Event.filter(ctrl.onDidChangeState, e => e === State.WAIT_FOR_INPUT));
295
296
const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri);
297
assert.ok(session);
298
assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 1, 3));
299
300
await ctrl.cancelSession();
301
});
302
303
test('typing outside of wholeRange finishes session', async function () {
304
305
configurationService.setUserConfiguration(InlineChatConfigKeys.FinishOnType, true);
306
307
ctrl = instaService.createInstance(TestController, editor);
308
const actualStates = ctrl.awaitStates(TestController.INIT_SEQUENCE_AUTO_SEND);
309
const r = ctrl.run({ message: 'Hello', autoSend: true });
310
311
assert.strictEqual(await actualStates, undefined);
312
313
const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri);
314
assert.ok(session);
315
assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 1, 11 /* line length */));
316
317
editor.setSelection(new Range(2, 1, 2, 1));
318
editor.trigger('test', 'type', { text: 'a' });
319
320
assert.strictEqual(await ctrl.awaitStates([State.ACCEPT]), undefined);
321
await r;
322
});
323
324
test('\'whole range\' isn\'t updated for edits outside whole range #4346', async function () {
325
326
editor.setSelection(new Range(3, 1, 3, 3));
327
328
store.add(chatAgentService.registerDynamicAgent({
329
id: 'testEditorAgent2',
330
...agentData
331
}, {
332
async invoke(request, progress, history, token) {
333
progress([{
334
kind: 'textEdit',
335
uri: editor.getModel().uri,
336
edits: [{
337
range: new Range(1, 1, 1, 1), // EDIT happens outside of whole range
338
text: `${request.message}\n${request.message}`
339
}]
340
}]);
341
342
return {};
343
},
344
}));
345
346
ctrl = instaService.createInstance(TestController, editor);
347
const p = ctrl.awaitStates(TestController.INIT_SEQUENCE);
348
const r = ctrl.run({ message: 'GENGEN', autoSend: false });
349
350
assert.strictEqual(await p, undefined);
351
352
353
const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri);
354
assert.ok(session);
355
assert.deepStrictEqual(session.wholeRange.value, new Range(3, 1, 3, 3)); // initial
356
357
ctrl.chatWidget.setInput('GENGEN');
358
ctrl.chatWidget.acceptInput();
359
assert.strictEqual(await ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]), undefined);
360
361
assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 4, 3));
362
363
await ctrl.cancelSession();
364
await r;
365
});
366
367
test('Stuck inline chat widget #211', async function () {
368
369
store.add(chatAgentService.registerDynamicAgent({
370
id: 'testEditorAgent2',
371
...agentData
372
}, {
373
async invoke(request, progress, history, token) {
374
return new Promise<never>(() => { });
375
},
376
}));
377
378
ctrl = instaService.createInstance(TestController, editor);
379
const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]);
380
const r = ctrl.run({ message: 'Hello', autoSend: true });
381
382
assert.strictEqual(await p, undefined);
383
384
ctrl.acceptSession();
385
386
await r;
387
assert.strictEqual(ctrl.getWidgetPosition(), undefined);
388
});
389
390
test('[Bug] Inline Chat\'s streaming pushed broken iterations to the undo stack #2403', async function () {
391
392
store.add(chatAgentService.registerDynamicAgent({
393
id: 'testEditorAgent2',
394
...agentData
395
}, {
396
async invoke(request, progress, history, token) {
397
398
progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'hEllo1\n' }] }]);
399
progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(2, 1, 2, 1), text: 'hEllo2\n' }] }]);
400
progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1000, 1), text: 'Hello1\nHello2\n' }] }]);
401
402
return {};
403
},
404
}));
405
406
const valueThen = editor.getModel().getValue();
407
408
ctrl = instaService.createInstance(TestController, editor);
409
const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);
410
const r = ctrl.run({ message: 'Hello', autoSend: true });
411
assert.strictEqual(await p, undefined);
412
ctrl.acceptSession();
413
await r;
414
415
assert.strictEqual(editor.getModel().getValue(), 'Hello1\nHello2\n');
416
417
editor.getModel().undo();
418
assert.strictEqual(editor.getModel().getValue(), valueThen);
419
});
420
421
422
423
test.skip('UI is streaming edits minutes after the response is finished #3345', async function () {
424
425
426
return runWithFakedTimers({ maxTaskCount: Number.MAX_SAFE_INTEGER }, async () => {
427
428
store.add(chatAgentService.registerDynamicAgent({
429
id: 'testEditorAgent2',
430
...agentData
431
}, {
432
async invoke(request, progress, history, token) {
433
434
const text = '${CSI}#a\n${CSI}#b\n${CSI}#c\n';
435
436
await timeout(10);
437
progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text }] }]);
438
439
await timeout(10);
440
progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.repeat(1000) + 'DONE' }] }]);
441
442
throw new Error('Too long');
443
},
444
}));
445
446
447
// let modelChangeCounter = 0;
448
// store.add(editor.getModel().onDidChangeContent(() => { modelChangeCounter++; }));
449
450
ctrl = instaService.createInstance(TestController, editor);
451
const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);
452
const r = ctrl.run({ message: 'Hello', autoSend: true });
453
assert.strictEqual(await p, undefined);
454
455
// assert.ok(modelChangeCounter > 0, modelChangeCounter.toString()); // some changes have been made
456
// const modelChangeCounterNow = modelChangeCounter;
457
458
assert.ok(!editor.getModel().getValue().includes('DONE'));
459
await timeout(10);
460
461
// assert.strictEqual(modelChangeCounterNow, modelChangeCounter);
462
assert.ok(!editor.getModel().getValue().includes('DONE'));
463
464
await ctrl.cancelSession();
465
await r;
466
});
467
});
468
469
test('escape doesn\'t remove code added from inline editor chat #3523 1/2', async function () {
470
471
472
// NO manual edits -> cancel
473
ctrl = instaService.createInstance(TestController, editor);
474
const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);
475
const r = ctrl.run({ message: 'GENERATED', autoSend: true });
476
assert.strictEqual(await p, undefined);
477
478
assert.ok(model.getValue().includes('GENERATED'));
479
ctrl.cancelSession();
480
await r;
481
assert.ok(!model.getValue().includes('GENERATED'));
482
483
});
484
485
test('escape doesn\'t remove code added from inline editor chat #3523, 2/2', async function () {
486
487
// manual edits -> finish
488
ctrl = instaService.createInstance(TestController, editor);
489
const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);
490
const r = ctrl.run({ message: 'GENERATED', autoSend: true });
491
assert.strictEqual(await p, undefined);
492
493
assert.ok(model.getValue().includes('GENERATED'));
494
495
editor.executeEdits('test', [EditOperation.insert(model.getFullModelRange().getEndPosition(), 'MANUAL')]);
496
497
ctrl.acceptSession();
498
await r;
499
assert.ok(model.getValue().includes('GENERATED'));
500
assert.ok(model.getValue().includes('MANUAL'));
501
502
});
503
504
test('re-run should discard pending edits', async function () {
505
506
let count = 1;
507
508
store.add(chatAgentService.registerDynamicAgent({
509
id: 'testEditorAgent2',
510
...agentData
511
}, {
512
async invoke(request, progress, history, token) {
513
progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: request.message + (count++) }] }]);
514
return {};
515
},
516
}));
517
518
ctrl = instaService.createInstance(TestController, editor);
519
const rerun = new RerunAction();
520
521
model.setValue('');
522
523
const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);
524
const r = ctrl.run({ message: 'PROMPT_', autoSend: true });
525
assert.strictEqual(await p, undefined);
526
527
528
assert.strictEqual(model.getValue(), 'PROMPT_1');
529
530
const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);
531
await instaService.invokeFunction(rerun.runInlineChatCommand, ctrl, editor);
532
533
assert.strictEqual(await p2, undefined);
534
535
assert.strictEqual(model.getValue(), 'PROMPT_2');
536
ctrl.acceptSession();
537
await r;
538
});
539
540
test('Retry undoes all changes, not just those from the request#5736', async function () {
541
542
const text = [
543
'eins-',
544
'zwei-',
545
'drei-'
546
];
547
548
store.add(chatAgentService.registerDynamicAgent({
549
id: 'testEditorAgent2',
550
...agentData
551
}, {
552
async invoke(request, progress, history, token) {
553
progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.shift() ?? '' }] }]);
554
return {};
555
},
556
}));
557
558
ctrl = instaService.createInstance(TestController, editor);
559
const rerun = new RerunAction();
560
561
model.setValue('');
562
563
// REQUEST 1
564
const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);
565
const r = ctrl.run({ message: '1', autoSend: true });
566
assert.strictEqual(await p, undefined);
567
568
assert.strictEqual(model.getValue(), 'eins-');
569
570
// REQUEST 2
571
const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);
572
ctrl.chatWidget.setInput('1');
573
await ctrl.chatWidget.acceptInput();
574
assert.strictEqual(await p2, undefined);
575
576
assert.strictEqual(model.getValue(), 'zwei-eins-');
577
578
// REQUEST 2 - RERUN
579
const p3 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);
580
await instaService.invokeFunction(rerun.runInlineChatCommand, ctrl, editor);
581
assert.strictEqual(await p3, undefined);
582
583
assert.strictEqual(model.getValue(), 'drei-eins-');
584
585
ctrl.acceptSession();
586
await r;
587
588
});
589
590
test('moving inline chat to another model undoes changes', async function () {
591
const text = [
592
'eins\n',
593
'zwei\n'
594
];
595
596
store.add(chatAgentService.registerDynamicAgent({
597
id: 'testEditorAgent2',
598
...agentData
599
}, {
600
async invoke(request, progress, history, token) {
601
progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.shift() ?? '' }] }]);
602
return {};
603
},
604
}));
605
ctrl = instaService.createInstance(TestController, editor);
606
607
// REQUEST 1
608
const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);
609
ctrl.run({ message: '1', autoSend: true });
610
assert.strictEqual(await p, undefined);
611
612
assert.strictEqual(model.getValue(), 'eins\nHello\nWorld\nHello Again\nHello World\n');
613
614
const targetModel = chatService.startSession(ChatAgentLocation.Editor, CancellationToken.None)!;
615
store.add(targetModel);
616
chatWidget = new class extends mock<IChatWidget>() {
617
override get viewModel() {
618
return { model: targetModel } as any;
619
}
620
override focusLastMessage() { }
621
};
622
623
const r = ctrl.joinCurrentRun();
624
await ctrl.viewInChat();
625
626
assert.strictEqual(model.getValue(), 'Hello\nWorld\nHello Again\nHello World\n');
627
await r;
628
});
629
630
test('moving inline chat to another model undoes changes (2 requests)', async function () {
631
const text = [
632
'eins\n',
633
'zwei\n'
634
];
635
636
store.add(chatAgentService.registerDynamicAgent({
637
id: 'testEditorAgent2',
638
...agentData
639
}, {
640
async invoke(request, progress, history, token) {
641
progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.shift() ?? '' }] }]);
642
return {};
643
},
644
}));
645
ctrl = instaService.createInstance(TestController, editor);
646
647
// REQUEST 1
648
const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);
649
ctrl.run({ message: '1', autoSend: true });
650
assert.strictEqual(await p, undefined);
651
652
assert.strictEqual(model.getValue(), 'eins\nHello\nWorld\nHello Again\nHello World\n');
653
654
// REQUEST 2
655
const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);
656
ctrl.chatWidget.setInput('1');
657
await ctrl.chatWidget.acceptInput();
658
assert.strictEqual(await p2, undefined);
659
660
assert.strictEqual(model.getValue(), 'zwei\neins\nHello\nWorld\nHello Again\nHello World\n');
661
662
const targetModel = chatService.startSession(ChatAgentLocation.Editor, CancellationToken.None)!;
663
store.add(targetModel);
664
chatWidget = new class extends mock<IChatWidget>() {
665
override get viewModel() {
666
return { model: targetModel } as any;
667
}
668
override focusLastMessage() { }
669
};
670
671
const r = ctrl.joinCurrentRun();
672
673
await ctrl.viewInChat();
674
675
assert.strictEqual(model.getValue(), 'Hello\nWorld\nHello Again\nHello World\n');
676
677
await r;
678
});
679
680
// TODO@jrieken https://github.com/microsoft/vscode/issues/251429
681
test.skip('Clicking "re-run without /doc" while a request is in progress closes the widget #5997', async function () {
682
683
model.setValue('');
684
685
let count = 0;
686
const commandDetection: (boolean | undefined)[] = [];
687
688
const onDidInvoke = new Emitter<void>();
689
690
store.add(chatAgentService.registerDynamicAgent({
691
id: 'testEditorAgent2',
692
...agentData
693
}, {
694
async invoke(request, progress, history, token) {
695
queueMicrotask(() => onDidInvoke.fire());
696
commandDetection.push(request.enableCommandDetection);
697
progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: request.message + (count++) }] }]);
698
699
if (count === 1) {
700
// FIRST call waits for cancellation
701
await raceCancellation(new Promise<never>(() => { }), token);
702
} else {
703
await timeout(10);
704
}
705
706
return {};
707
},
708
}));
709
ctrl = instaService.createInstance(TestController, editor);
710
711
// REQUEST 1
712
// const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]);
713
const p = Event.toPromise(onDidInvoke.event);
714
ctrl.run({ message: 'Hello-', autoSend: true });
715
716
await p;
717
718
// assert.strictEqual(await p, undefined);
719
720
// resend pending request without command detection
721
const request = ctrl.chatWidget.viewModel?.model.getRequests().at(-1);
722
assertType(request);
723
const p2 = Event.toPromise(onDidInvoke.event);
724
const p3 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);
725
chatService.resendRequest(request, { noCommandDetection: true, attempt: request.attempt + 1, location: ChatAgentLocation.Editor });
726
727
await p2;
728
assert.strictEqual(await p3, undefined);
729
730
assert.deepStrictEqual(commandDetection, [true, false]);
731
assert.strictEqual(model.getValue(), 'Hello-1');
732
});
733
734
test('Re-run without after request is done', async function () {
735
736
model.setValue('');
737
738
let count = 0;
739
const commandDetection: (boolean | undefined)[] = [];
740
741
store.add(chatAgentService.registerDynamicAgent({
742
id: 'testEditorAgent2',
743
...agentData
744
}, {
745
async invoke(request, progress, history, token) {
746
commandDetection.push(request.enableCommandDetection);
747
progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: request.message + (count++) }] }]);
748
return {};
749
},
750
}));
751
ctrl = instaService.createInstance(TestController, editor);
752
753
// REQUEST 1
754
const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);
755
ctrl.run({ message: 'Hello-', autoSend: true });
756
assert.strictEqual(await p, undefined);
757
758
// resend pending request without command detection
759
const request = ctrl.chatWidget.viewModel?.model.getRequests().at(-1);
760
assertType(request);
761
const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);
762
chatService.resendRequest(request, { noCommandDetection: true, attempt: request.attempt + 1, location: ChatAgentLocation.Editor });
763
764
assert.strictEqual(await p2, undefined);
765
766
assert.deepStrictEqual(commandDetection, [true, false]);
767
assert.strictEqual(model.getValue(), 'Hello-1');
768
});
769
770
771
test('Inline: Pressing Rerun request while the response streams breaks the response #5442', async function () {
772
773
model.setValue('two\none\n');
774
775
const attempts: (number | undefined)[] = [];
776
777
const deferred = new DeferredPromise<void>();
778
779
store.add(chatAgentService.registerDynamicAgent({
780
id: 'testEditorAgent2',
781
...agentData
782
}, {
783
async invoke(request, progress, history, token) {
784
785
attempts.push(request.attempt);
786
787
progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: `TRY:${request.attempt}\n` }] }]);
788
await raceCancellation(deferred.p, token);
789
deferred.complete();
790
await timeout(10);
791
return {};
792
},
793
}));
794
795
ctrl = instaService.createInstance(TestController, editor);
796
797
// REQUEST 1
798
const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]);
799
ctrl.run({ message: 'Hello-', autoSend: true });
800
assert.strictEqual(await p, undefined);
801
await timeout(10);
802
assert.deepStrictEqual(attempts, [0]);
803
804
// RERUN (cancel, undo, redo)
805
const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);
806
const rerun = new RerunAction();
807
await instaService.invokeFunction(rerun.runInlineChatCommand, ctrl, editor);
808
assert.strictEqual(await p2, undefined);
809
810
assert.deepStrictEqual(attempts, [0, 1]);
811
812
assert.strictEqual(model.getValue(), 'TRY:1\ntwo\none\n');
813
814
});
815
816
test('Stopping/cancelling a request should NOT undo its changes', async function () {
817
818
model.setValue('World');
819
820
const deferred = new DeferredPromise<void>();
821
let progress: ((parts: IChatProgress[]) => void) | undefined;
822
823
store.add(chatAgentService.registerDynamicAgent({
824
id: 'testEditorAgent2',
825
...agentData
826
}, {
827
async invoke(request, _progress, history, token) {
828
829
progress = _progress;
830
await deferred.p;
831
return {};
832
},
833
}));
834
835
ctrl = instaService.createInstance(TestController, editor);
836
837
// REQUEST 1
838
const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]);
839
ctrl.run({ message: 'Hello', autoSend: true });
840
await timeout(10);
841
assert.strictEqual(await p, undefined);
842
843
assertType(progress);
844
845
const modelChange = new Promise<void>(resolve => model.onDidChangeContent(() => resolve()));
846
847
progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'Hello-Hello' }] }]);
848
849
await modelChange;
850
assert.strictEqual(model.getValue(), 'HelloWorld'); // first word has been streamed
851
852
const p2 = ctrl.awaitStates([State.WAIT_FOR_INPUT]);
853
chatService.cancelCurrentRequestForSession(ctrl.chatWidget.viewModel!.model.sessionId);
854
assert.strictEqual(await p2, undefined);
855
856
assert.strictEqual(model.getValue(), 'HelloWorld'); // CANCEL just stops the request and progressive typing but doesn't undo
857
858
});
859
860
test('Apply Edits from existing session w/ edits', async function () {
861
862
model.setValue('');
863
864
const newSession = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None);
865
assertType(newSession);
866
867
await (await chatService.sendRequest(newSession.chatModel.sessionId, 'Existing', { location: ChatAgentLocation.Editor }))?.responseCreatedPromise;
868
869
assert.strictEqual(newSession.chatModel.requestInProgress, true);
870
871
const response = newSession.chatModel.lastRequest?.response;
872
assertType(response);
873
874
await new Promise(resolve => {
875
if (response.isComplete) {
876
resolve(undefined);
877
}
878
const d = response.onDidChange(() => {
879
if (response.isComplete) {
880
d.dispose();
881
resolve(undefined);
882
}
883
});
884
});
885
886
ctrl = instaService.createInstance(TestController, editor);
887
const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE]);
888
ctrl.run({ existingSession: newSession });
889
890
assert.strictEqual(await p, undefined);
891
892
assert.strictEqual(model.getValue(), 'Existing');
893
894
});
895
896
test('Undo on error (2 rounds)', async function () {
897
898
return runWithFakedTimers({}, async () => {
899
900
901
store.add(chatAgentService.registerDynamicAgent({ id: 'testEditorAgent', ...agentData, }, {
902
async invoke(request, progress, history, token) {
903
904
progress([{
905
kind: 'textEdit',
906
uri: model.uri,
907
edits: [{
908
range: new Range(1, 1, 1, 1),
909
text: request.message
910
}]
911
}]);
912
913
if (request.message === 'two') {
914
await timeout(100); // give edit a chance
915
return {
916
errorDetails: { message: 'FAILED' }
917
};
918
}
919
return {};
920
},
921
}));
922
923
model.setValue('');
924
925
// ROUND 1
926
927
ctrl = instaService.createInstance(TestController, editor);
928
const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);
929
ctrl.run({ autoSend: true, message: 'one' });
930
assert.strictEqual(await p, undefined);
931
assert.strictEqual(model.getValue(), 'one');
932
933
934
// ROUND 2
935
936
const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);
937
const values = new Set<string>();
938
store.add(model.onDidChangeContent(() => values.add(model.getValue())));
939
ctrl.chatWidget.acceptInput('two'); // WILL Trigger a failure
940
assert.strictEqual(await p2, undefined);
941
assert.strictEqual(model.getValue(), 'one'); // undone
942
assert.ok(values.has('twoone')); // we had but the change got undone
943
});
944
});
945
946
test('Inline chat "discard" button does not always appear if response is stopped #228030', async function () {
947
948
model.setValue('World');
949
950
const deferred = new DeferredPromise<void>();
951
952
store.add(chatAgentService.registerDynamicAgent({
953
id: 'testEditorAgent2',
954
...agentData
955
}, {
956
async invoke(request, progress, history, token) {
957
958
progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'Hello-Hello' }] }]);
959
await deferred.p;
960
return {};
961
},
962
}));
963
964
ctrl = instaService.createInstance(TestController, editor);
965
966
// REQUEST 1
967
const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]);
968
ctrl.run({ message: 'Hello', autoSend: true });
969
970
971
assert.strictEqual(await p, undefined);
972
973
const p2 = ctrl.awaitStates([State.WAIT_FOR_INPUT]);
974
chatService.cancelCurrentRequestForSession(ctrl.chatWidget.viewModel!.model.sessionId);
975
assert.strictEqual(await p2, undefined);
976
977
978
const value = contextKeyService.getContextKeyValue(CTX_INLINE_CHAT_RESPONSE_TYPE.key);
979
assert.notStrictEqual(value, InlineChatResponseType.None);
980
});
981
982
test('Restore doesn\'t edit on errored result', async function () {
983
return runWithFakedTimers({ useFakeTimers: true }, async () => {
984
985
const model2 = store.add(instaService.get(IModelService).createModel('ABC', null));
986
987
model.setValue('World');
988
989
store.add(chatAgentService.registerDynamicAgent({
990
id: 'testEditorAgent2',
991
...agentData
992
}, {
993
async invoke(request, progress, history, token) {
994
995
progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'Hello1' }] }]);
996
await timeout(100);
997
progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'Hello2' }] }]);
998
await timeout(100);
999
progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'Hello3' }] }]);
1000
await timeout(100);
1001
1002
return {
1003
errorDetails: { message: 'FAILED' }
1004
};
1005
},
1006
}));
1007
1008
ctrl = instaService.createInstance(TestController, editor);
1009
1010
// REQUEST 1
1011
const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);
1012
ctrl.run({ message: 'Hello', autoSend: true });
1013
1014
assert.strictEqual(await p, undefined);
1015
1016
const p2 = ctrl.awaitStates([State.PAUSE]);
1017
editor.setModel(model2);
1018
assert.strictEqual(await p2, undefined);
1019
1020
const p3 = ctrl.awaitStates([...TestController.INIT_SEQUENCE]);
1021
editor.setModel(model);
1022
assert.strictEqual(await p3, undefined);
1023
1024
assert.strictEqual(model.getValue(), 'World');
1025
});
1026
});
1027
});
1028
1029