Path: blob/main/src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts
3297 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import assert from 'assert';6import { equals } from '../../../../../base/common/arrays.js';7import { DeferredPromise, raceCancellation, timeout } from '../../../../../base/common/async.js';8import { CancellationToken } from '../../../../../base/common/cancellation.js';9import { Emitter, Event } from '../../../../../base/common/event.js';10import { DisposableStore } from '../../../../../base/common/lifecycle.js';11import { constObservable, IObservable } from '../../../../../base/common/observable.js';12import { assertType } from '../../../../../base/common/types.js';13import { mock } from '../../../../../base/test/common/mock.js';14import { runWithFakedTimers } from '../../../../../base/test/common/timeTravelScheduler.js';15import { IActiveCodeEditor } from '../../../../../editor/browser/editorBrowser.js';16import { IDiffProviderFactoryService } from '../../../../../editor/browser/widget/diffEditor/diffProviderFactoryService.js';17import { EditOperation } from '../../../../../editor/common/core/editOperation.js';18import { Range } from '../../../../../editor/common/core/range.js';19import { EndOfLineSequence, ITextModel } from '../../../../../editor/common/model.js';20import { IEditorWorkerService } from '../../../../../editor/common/services/editorWorker.js';21import { IModelService } from '../../../../../editor/common/services/model.js';22import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';23import { TestDiffProviderFactoryService } from '../../../../../editor/test/browser/diff/testDiffProviderFactoryService.js';24import { TestCommandService } from '../../../../../editor/test/browser/editorTestServices.js';25import { instantiateTestCodeEditor } from '../../../../../editor/test/browser/testCodeEditor.js';26import { IAccessibleViewService } from '../../../../../platform/accessibility/browser/accessibleView.js';27import { ICommandService } from '../../../../../platform/commands/common/commands.js';28import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';29import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';30import { IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';31import { IHoverService } from '../../../../../platform/hover/browser/hover.js';32import { NullHoverService } from '../../../../../platform/hover/test/browser/nullHoverService.js';33import { SyncDescriptor } from '../../../../../platform/instantiation/common/descriptors.js';34import { ServiceCollection } from '../../../../../platform/instantiation/common/serviceCollection.js';35import { TestInstantiationService } from '../../../../../platform/instantiation/test/common/instantiationServiceMock.js';36import { MockContextKeyService } from '../../../../../platform/keybinding/test/common/mockKeybindingService.js';37import { ILogService, NullLogService } from '../../../../../platform/log/common/log.js';38import { IEditorProgressService, IProgressRunner } from '../../../../../platform/progress/common/progress.js';39import { ITelemetryService } from '../../../../../platform/telemetry/common/telemetry.js';40import { NullTelemetryService } from '../../../../../platform/telemetry/common/telemetryUtils.js';41import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';42import { IView, IViewDescriptorService } from '../../../../common/views.js';43import { IWorkbenchAssignmentService } from '../../../../services/assignment/common/assignmentService.js';44import { NullWorkbenchAssignmentService } from '../../../../services/assignment/test/common/nullAssignmentService.js';45import { IExtensionService, nullExtensionDescription } from '../../../../services/extensions/common/extensions.js';46import { TextModelResolverService } from '../../../../services/textmodelResolver/common/textModelResolverService.js';47import { IViewsService } from '../../../../services/views/common/viewsService.js';48import { TestViewsService, workbenchInstantiationService } from '../../../../test/browser/workbenchTestServices.js';49import { TestContextService, TestExtensionService } from '../../../../test/common/workbenchTestServices.js';50import { AccessibilityVerbositySettingId } from '../../../accessibility/browser/accessibilityConfiguration.js';51import { IChatAccessibilityService, IChatWidget, IChatWidgetService } from '../../../chat/browser/chat.js';52import { ChatInputBoxContentProvider } from '../../../chat/browser/chatEdinputInputContentProvider.js';53import { ChatVariablesService } from '../../../chat/browser/chatVariables.js';54import { ChatWidgetService } from '../../../chat/browser/chatWidget.js';55import { ChatAgentService, IChatAgentData, IChatAgentNameService, IChatAgentService } from '../../../chat/common/chatAgents.js';56import { IChatEditingService, IChatEditingSession } from '../../../chat/common/chatEditingService.js';57import { IChatEntitlementService } from '../../../chat/common/chatEntitlementService.js';58import { IChatModeService } from '../../../chat/common/chatModes.js';59import { IChatProgress, IChatService } from '../../../chat/common/chatService.js';60import { ChatService } from '../../../chat/common/chatServiceImpl.js';61import { ChatSlashCommandService, IChatSlashCommandService } from '../../../chat/common/chatSlashCommands.js';62import { IChatVariablesService } from '../../../chat/common/chatVariables.js';63import { IChatResponseViewModel } from '../../../chat/common/chatViewModel.js';64import { ChatWidgetHistoryService, IChatWidgetHistoryService } from '../../../chat/common/chatWidgetHistoryService.js';65import { ChatAgentLocation, ChatModeKind } from '../../../chat/common/constants.js';66import { ILanguageModelsService, LanguageModelsService } from '../../../chat/common/languageModels.js';67import { ILanguageModelToolsService } from '../../../chat/common/languageModelToolsService.js';68import { IPromptPath, IPromptsService } from '../../../chat/common/promptSyntax/service/promptsService.js';69import { MockChatModeService } from '../../../chat/test/common/mockChatModeService.js';70import { MockLanguageModelToolsService } from '../../../chat/test/common/mockLanguageModelToolsService.js';71import { INotebookEditorService } from '../../../notebook/browser/services/notebookEditorService.js';72import { RerunAction } from '../../browser/inlineChatActions.js';73import { InlineChatController1, State } from '../../browser/inlineChatController.js';74import { IInlineChatSessionService } from '../../browser/inlineChatSessionService.js';75import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl.js';76import { CTX_INLINE_CHAT_RESPONSE_TYPE, InlineChatConfigKeys, InlineChatResponseType } from '../../common/inlineChat.js';77import { TestWorkerService } from './testWorkerService.js';78import { PromptsType } from '../../../chat/common/promptSyntax/promptTypes.js';79import { ChatTransferService, IChatTransferService } from '../../../chat/common/chatTransferService.js';80import { IMcpService } from '../../../mcp/common/mcpTypes.js';81import { TestMcpService } from '../../../mcp/test/common/testMcpService.js';82import { IChatLayoutService } from '../../../chat/common/chatLayoutService.js';83import { ChatLayoutService } from '../../../chat/browser/chatLayoutService.js';8485suite('InlineChatController', function () {8687const agentData = {88extensionId: nullExtensionDescription.identifier,89extensionVersion: undefined,90publisherDisplayName: '',91extensionDisplayName: '',92extensionPublisherId: '',93// id: 'testEditorAgent',94name: 'testEditorAgent',95isDefault: true,96locations: [ChatAgentLocation.Editor],97modes: [ChatModeKind.Ask],98metadata: {},99slashCommands: [],100disambiguation: [],101};102103class TestController extends InlineChatController1 {104105static INIT_SEQUENCE: readonly State[] = [State.CREATE_SESSION, State.INIT_UI, State.WAIT_FOR_INPUT];106static INIT_SEQUENCE_AUTO_SEND: readonly State[] = [...this.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT];107108109readonly onDidChangeState: Event<State> = this._onDidEnterState.event;110111readonly states: readonly State[] = [];112113awaitStates(states: readonly State[]): Promise<string | undefined> {114const actual: State[] = [];115116return new Promise<string | undefined>((resolve, reject) => {117const d = this.onDidChangeState(state => {118actual.push(state);119if (equals(states, actual)) {120d.dispose();121resolve(undefined);122}123});124125setTimeout(() => {126d.dispose();127resolve(`[${states.join(',')}] <> [${actual.join(',')}]`);128}, 1000);129});130}131}132133const store = new DisposableStore();134let configurationService: TestConfigurationService;135let editor: IActiveCodeEditor;136let model: ITextModel;137let ctrl: TestController;138let contextKeyService: MockContextKeyService;139let chatService: IChatService;140let chatAgentService: IChatAgentService;141let inlineChatSessionService: IInlineChatSessionService;142let instaService: TestInstantiationService;143144let chatWidget: IChatWidget;145146setup(function () {147148const serviceCollection = new ServiceCollection(149[IConfigurationService, new TestConfigurationService()],150[IChatVariablesService, new SyncDescriptor(ChatVariablesService)],151[ILogService, new NullLogService()],152[ITelemetryService, NullTelemetryService],153[IHoverService, NullHoverService],154[IExtensionService, new TestExtensionService()],155[IContextKeyService, new MockContextKeyService()],156[IViewsService, new class extends TestViewsService {157override async openView<T extends IView>(id: string, focus?: boolean | undefined): Promise<T | null> {158return { widget: chatWidget ?? null } as any;159}160}()],161[IWorkspaceContextService, new TestContextService()],162[IChatWidgetHistoryService, new SyncDescriptor(ChatWidgetHistoryService)],163[IChatWidgetService, new SyncDescriptor(ChatWidgetService)],164[IChatSlashCommandService, new SyncDescriptor(ChatSlashCommandService)],165[IChatTransferService, new SyncDescriptor(ChatTransferService)],166[IChatService, new SyncDescriptor(ChatService)],167[IMcpService, new TestMcpService()],168[IChatAgentNameService, new class extends mock<IChatAgentNameService>() {169override getAgentNameRestriction(chatAgentData: IChatAgentData): boolean {170return false;171}172}],173[IEditorWorkerService, new SyncDescriptor(TestWorkerService)],174[IContextKeyService, contextKeyService],175[IChatAgentService, new SyncDescriptor(ChatAgentService)],176[IDiffProviderFactoryService, new SyncDescriptor(TestDiffProviderFactoryService)],177[IInlineChatSessionService, new SyncDescriptor(InlineChatSessionServiceImpl)],178[ICommandService, new SyncDescriptor(TestCommandService)],179[IChatEditingService, new class extends mock<IChatEditingService>() {180override editingSessionsObs: IObservable<readonly IChatEditingSession[]> = constObservable([]);181}],182[IEditorProgressService, new class extends mock<IEditorProgressService>() {183override show(total: unknown, delay?: unknown): IProgressRunner {184return {185total() { },186worked(value) { },187done() { },188};189}190}],191[IChatAccessibilityService, new class extends mock<IChatAccessibilityService>() {192override acceptResponse(response: IChatResponseViewModel | undefined, requestId: number): void { }193override acceptRequest(): number { return -1; }194override acceptElicitation(): void { }195}],196[IAccessibleViewService, new class extends mock<IAccessibleViewService>() {197override getOpenAriaHint(verbositySettingKey: AccessibilityVerbositySettingId): string | null {198return null;199}200}],201[IConfigurationService, configurationService],202[IViewDescriptorService, new class extends mock<IViewDescriptorService>() {203override onDidChangeLocation = Event.None;204}],205[INotebookEditorService, new class extends mock<INotebookEditorService>() {206override listNotebookEditors() { return []; }207}],208[IWorkbenchAssignmentService, new NullWorkbenchAssignmentService()],209[ILanguageModelsService, new SyncDescriptor(LanguageModelsService)],210[ITextModelService, new SyncDescriptor(TextModelResolverService)],211[ILanguageModelToolsService, new SyncDescriptor(MockLanguageModelToolsService)],212[IPromptsService, new class extends mock<IPromptsService>() {213override async listPromptFiles(type: PromptsType, token: CancellationToken): Promise<readonly IPromptPath[]> {214return [];215}216}],217[IChatEntitlementService, new class extends mock<IChatEntitlementService>() { }],218[IChatModeService, new SyncDescriptor(MockChatModeService)],219[IChatLayoutService, new SyncDescriptor(ChatLayoutService)],220);221222instaService = store.add((store.add(workbenchInstantiationService(undefined, store))).createChild(serviceCollection));223224configurationService = instaService.get(IConfigurationService) as TestConfigurationService;225configurationService.setUserConfiguration('chat', { editor: { fontSize: 14, fontFamily: 'default' } });226227configurationService.setUserConfiguration('editor', {});228229contextKeyService = instaService.get(IContextKeyService) as MockContextKeyService;230chatService = instaService.get(IChatService);231chatAgentService = instaService.get(IChatAgentService);232233inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService));234235store.add(instaService.get(ILanguageModelsService) as LanguageModelsService);236store.add(instaService.get(IEditorWorkerService) as TestWorkerService);237238store.add(instaService.createInstance(ChatInputBoxContentProvider));239240model = store.add(instaService.get(IModelService).createModel('Hello\nWorld\nHello Again\nHello World\n', null));241model.setEOL(EndOfLineSequence.LF);242editor = store.add(instantiateTestCodeEditor(instaService, model));243244store.add(chatAgentService.registerDynamicAgent({ id: 'testEditorAgent', ...agentData, }, {245async invoke(request, progress, history, token) {246progress([{247kind: 'textEdit',248uri: model.uri,249edits: [{250range: new Range(1, 1, 1, 1),251text: request.message252}]253}]);254return {};255},256}));257258});259260teardown(function () {261store.clear();262ctrl?.dispose();263});264265// TODO@jrieken re-enable, looks like List/ChatWidget is leaking266// ensureNoDisposablesAreLeakedInTestSuite();267268test('creation, not showing anything', function () {269ctrl = instaService.createInstance(TestController, editor);270assert.ok(ctrl);271assert.strictEqual(ctrl.getWidgetPosition(), undefined);272});273274test('run (show/hide)', async function () {275ctrl = instaService.createInstance(TestController, editor);276const actualStates = ctrl.awaitStates(TestController.INIT_SEQUENCE_AUTO_SEND);277const run = ctrl.run({ message: 'Hello', autoSend: true });278assert.strictEqual(await actualStates, undefined);279assert.ok(ctrl.getWidgetPosition() !== undefined);280await ctrl.cancelSession();281282await run;283284assert.ok(ctrl.getWidgetPosition() === undefined);285});286287test('wholeRange does not expand to whole lines, editor selection default', async function () {288289editor.setSelection(new Range(1, 1, 1, 3));290ctrl = instaService.createInstance(TestController, editor);291292ctrl.run({});293await Event.toPromise(Event.filter(ctrl.onDidChangeState, e => e === State.WAIT_FOR_INPUT));294295const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri);296assert.ok(session);297assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 1, 3));298299await ctrl.cancelSession();300});301302test('typing outside of wholeRange finishes session', async function () {303304configurationService.setUserConfiguration(InlineChatConfigKeys.FinishOnType, true);305306ctrl = instaService.createInstance(TestController, editor);307const actualStates = ctrl.awaitStates(TestController.INIT_SEQUENCE_AUTO_SEND);308const r = ctrl.run({ message: 'Hello', autoSend: true });309310assert.strictEqual(await actualStates, undefined);311312const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri);313assert.ok(session);314assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 1, 11 /* line length */));315316editor.setSelection(new Range(2, 1, 2, 1));317editor.trigger('test', 'type', { text: 'a' });318319assert.strictEqual(await ctrl.awaitStates([State.ACCEPT]), undefined);320await r;321});322323test('\'whole range\' isn\'t updated for edits outside whole range #4346', async function () {324325editor.setSelection(new Range(3, 1, 3, 3));326327store.add(chatAgentService.registerDynamicAgent({328id: 'testEditorAgent2',329...agentData330}, {331async invoke(request, progress, history, token) {332progress([{333kind: 'textEdit',334uri: editor.getModel().uri,335edits: [{336range: new Range(1, 1, 1, 1), // EDIT happens outside of whole range337text: `${request.message}\n${request.message}`338}]339}]);340341return {};342},343}));344345ctrl = instaService.createInstance(TestController, editor);346const p = ctrl.awaitStates(TestController.INIT_SEQUENCE);347const r = ctrl.run({ message: 'GENGEN', autoSend: false });348349assert.strictEqual(await p, undefined);350351352const session = inlineChatSessionService.getSession(editor, editor.getModel()!.uri);353assert.ok(session);354assert.deepStrictEqual(session.wholeRange.value, new Range(3, 1, 3, 3)); // initial355356ctrl.chatWidget.setInput('GENGEN');357ctrl.chatWidget.acceptInput();358assert.strictEqual(await ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]), undefined);359360assert.deepStrictEqual(session.wholeRange.value, new Range(1, 1, 4, 3));361362await ctrl.cancelSession();363await r;364});365366test('Stuck inline chat widget #211', async function () {367368store.add(chatAgentService.registerDynamicAgent({369id: 'testEditorAgent2',370...agentData371}, {372async invoke(request, progress, history, token) {373return new Promise<never>(() => { });374},375}));376377ctrl = instaService.createInstance(TestController, editor);378const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]);379const r = ctrl.run({ message: 'Hello', autoSend: true });380381assert.strictEqual(await p, undefined);382383ctrl.acceptSession();384385await r;386assert.strictEqual(ctrl.getWidgetPosition(), undefined);387});388389test('[Bug] Inline Chat\'s streaming pushed broken iterations to the undo stack #2403', async function () {390391store.add(chatAgentService.registerDynamicAgent({392id: 'testEditorAgent2',393...agentData394}, {395async invoke(request, progress, history, token) {396397progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'hEllo1\n' }] }]);398progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(2, 1, 2, 1), text: 'hEllo2\n' }] }]);399progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1000, 1), text: 'Hello1\nHello2\n' }] }]);400401return {};402},403}));404405const valueThen = editor.getModel().getValue();406407ctrl = instaService.createInstance(TestController, editor);408const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);409const r = ctrl.run({ message: 'Hello', autoSend: true });410assert.strictEqual(await p, undefined);411ctrl.acceptSession();412await r;413414assert.strictEqual(editor.getModel().getValue(), 'Hello1\nHello2\n');415416editor.getModel().undo();417assert.strictEqual(editor.getModel().getValue(), valueThen);418});419420421422test.skip('UI is streaming edits minutes after the response is finished #3345', async function () {423424425return runWithFakedTimers({ maxTaskCount: Number.MAX_SAFE_INTEGER }, async () => {426427store.add(chatAgentService.registerDynamicAgent({428id: 'testEditorAgent2',429...agentData430}, {431async invoke(request, progress, history, token) {432433const text = '${CSI}#a\n${CSI}#b\n${CSI}#c\n';434435await timeout(10);436progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text }] }]);437438await timeout(10);439progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.repeat(1000) + 'DONE' }] }]);440441throw new Error('Too long');442},443}));444445446// let modelChangeCounter = 0;447// store.add(editor.getModel().onDidChangeContent(() => { modelChangeCounter++; }));448449ctrl = instaService.createInstance(TestController, editor);450const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);451const r = ctrl.run({ message: 'Hello', autoSend: true });452assert.strictEqual(await p, undefined);453454// assert.ok(modelChangeCounter > 0, modelChangeCounter.toString()); // some changes have been made455// const modelChangeCounterNow = modelChangeCounter;456457assert.ok(!editor.getModel().getValue().includes('DONE'));458await timeout(10);459460// assert.strictEqual(modelChangeCounterNow, modelChangeCounter);461assert.ok(!editor.getModel().getValue().includes('DONE'));462463await ctrl.cancelSession();464await r;465});466});467468test('escape doesn\'t remove code added from inline editor chat #3523 1/2', async function () {469470471// NO manual edits -> cancel472ctrl = instaService.createInstance(TestController, editor);473const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);474const r = ctrl.run({ message: 'GENERATED', autoSend: true });475assert.strictEqual(await p, undefined);476477assert.ok(model.getValue().includes('GENERATED'));478ctrl.cancelSession();479await r;480assert.ok(!model.getValue().includes('GENERATED'));481482});483484test('escape doesn\'t remove code added from inline editor chat #3523, 2/2', async function () {485486// manual edits -> finish487ctrl = instaService.createInstance(TestController, editor);488const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);489const r = ctrl.run({ message: 'GENERATED', autoSend: true });490assert.strictEqual(await p, undefined);491492assert.ok(model.getValue().includes('GENERATED'));493494editor.executeEdits('test', [EditOperation.insert(model.getFullModelRange().getEndPosition(), 'MANUAL')]);495496ctrl.acceptSession();497await r;498assert.ok(model.getValue().includes('GENERATED'));499assert.ok(model.getValue().includes('MANUAL'));500501});502503test('re-run should discard pending edits', async function () {504505let count = 1;506507store.add(chatAgentService.registerDynamicAgent({508id: 'testEditorAgent2',509...agentData510}, {511async invoke(request, progress, history, token) {512progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: request.message + (count++) }] }]);513return {};514},515}));516517ctrl = instaService.createInstance(TestController, editor);518const rerun = new RerunAction();519520model.setValue('');521522const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);523const r = ctrl.run({ message: 'PROMPT_', autoSend: true });524assert.strictEqual(await p, undefined);525526527assert.strictEqual(model.getValue(), 'PROMPT_1');528529const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);530await instaService.invokeFunction(rerun.runInlineChatCommand, ctrl, editor);531532assert.strictEqual(await p2, undefined);533534assert.strictEqual(model.getValue(), 'PROMPT_2');535ctrl.acceptSession();536await r;537});538539test('Retry undoes all changes, not just those from the request#5736', async function () {540541const text = [542'eins-',543'zwei-',544'drei-'545];546547store.add(chatAgentService.registerDynamicAgent({548id: 'testEditorAgent2',549...agentData550}, {551async invoke(request, progress, history, token) {552progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.shift() ?? '' }] }]);553return {};554},555}));556557ctrl = instaService.createInstance(TestController, editor);558const rerun = new RerunAction();559560model.setValue('');561562// REQUEST 1563const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);564const r = ctrl.run({ message: '1', autoSend: true });565assert.strictEqual(await p, undefined);566567assert.strictEqual(model.getValue(), 'eins-');568569// REQUEST 2570const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);571ctrl.chatWidget.setInput('1');572await ctrl.chatWidget.acceptInput();573assert.strictEqual(await p2, undefined);574575assert.strictEqual(model.getValue(), 'zwei-eins-');576577// REQUEST 2 - RERUN578const p3 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);579await instaService.invokeFunction(rerun.runInlineChatCommand, ctrl, editor);580assert.strictEqual(await p3, undefined);581582assert.strictEqual(model.getValue(), 'drei-eins-');583584ctrl.acceptSession();585await r;586587});588589test('moving inline chat to another model undoes changes', async function () {590const text = [591'eins\n',592'zwei\n'593];594595store.add(chatAgentService.registerDynamicAgent({596id: 'testEditorAgent2',597...agentData598}, {599async invoke(request, progress, history, token) {600progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.shift() ?? '' }] }]);601return {};602},603}));604ctrl = instaService.createInstance(TestController, editor);605606// REQUEST 1607const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);608ctrl.run({ message: '1', autoSend: true });609assert.strictEqual(await p, undefined);610611assert.strictEqual(model.getValue(), 'eins\nHello\nWorld\nHello Again\nHello World\n');612613const targetModel = chatService.startSession(ChatAgentLocation.Editor, CancellationToken.None)!;614store.add(targetModel);615chatWidget = new class extends mock<IChatWidget>() {616override get viewModel() {617return { model: targetModel } as any;618}619override focusLastMessage() { }620};621622const r = ctrl.joinCurrentRun();623await ctrl.viewInChat();624625assert.strictEqual(model.getValue(), 'Hello\nWorld\nHello Again\nHello World\n');626await r;627});628629test('moving inline chat to another model undoes changes (2 requests)', async function () {630const text = [631'eins\n',632'zwei\n'633];634635store.add(chatAgentService.registerDynamicAgent({636id: 'testEditorAgent2',637...agentData638}, {639async invoke(request, progress, history, token) {640progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.shift() ?? '' }] }]);641return {};642},643}));644ctrl = instaService.createInstance(TestController, editor);645646// REQUEST 1647const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);648ctrl.run({ message: '1', autoSend: true });649assert.strictEqual(await p, undefined);650651assert.strictEqual(model.getValue(), 'eins\nHello\nWorld\nHello Again\nHello World\n');652653// REQUEST 2654const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);655ctrl.chatWidget.setInput('1');656await ctrl.chatWidget.acceptInput();657assert.strictEqual(await p2, undefined);658659assert.strictEqual(model.getValue(), 'zwei\neins\nHello\nWorld\nHello Again\nHello World\n');660661const targetModel = chatService.startSession(ChatAgentLocation.Editor, CancellationToken.None)!;662store.add(targetModel);663chatWidget = new class extends mock<IChatWidget>() {664override get viewModel() {665return { model: targetModel } as any;666}667override focusLastMessage() { }668};669670const r = ctrl.joinCurrentRun();671672await ctrl.viewInChat();673674assert.strictEqual(model.getValue(), 'Hello\nWorld\nHello Again\nHello World\n');675676await r;677});678679// TODO@jrieken https://github.com/microsoft/vscode/issues/251429680test.skip('Clicking "re-run without /doc" while a request is in progress closes the widget #5997', async function () {681682model.setValue('');683684let count = 0;685const commandDetection: (boolean | undefined)[] = [];686687const onDidInvoke = new Emitter<void>();688689store.add(chatAgentService.registerDynamicAgent({690id: 'testEditorAgent2',691...agentData692}, {693async invoke(request, progress, history, token) {694queueMicrotask(() => onDidInvoke.fire());695commandDetection.push(request.enableCommandDetection);696progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: request.message + (count++) }] }]);697698if (count === 1) {699// FIRST call waits for cancellation700await raceCancellation(new Promise<never>(() => { }), token);701} else {702await timeout(10);703}704705return {};706},707}));708ctrl = instaService.createInstance(TestController, editor);709710// REQUEST 1711// const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]);712const p = Event.toPromise(onDidInvoke.event);713ctrl.run({ message: 'Hello-', autoSend: true });714715await p;716717// assert.strictEqual(await p, undefined);718719// resend pending request without command detection720const request = ctrl.chatWidget.viewModel?.model.getRequests().at(-1);721assertType(request);722const p2 = Event.toPromise(onDidInvoke.event);723const p3 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);724chatService.resendRequest(request, { noCommandDetection: true, attempt: request.attempt + 1, location: ChatAgentLocation.Editor });725726await p2;727assert.strictEqual(await p3, undefined);728729assert.deepStrictEqual(commandDetection, [true, false]);730assert.strictEqual(model.getValue(), 'Hello-1');731});732733test('Re-run without after request is done', async function () {734735model.setValue('');736737let count = 0;738const commandDetection: (boolean | undefined)[] = [];739740store.add(chatAgentService.registerDynamicAgent({741id: 'testEditorAgent2',742...agentData743}, {744async invoke(request, progress, history, token) {745commandDetection.push(request.enableCommandDetection);746progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: request.message + (count++) }] }]);747return {};748},749}));750ctrl = instaService.createInstance(TestController, editor);751752// REQUEST 1753const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);754ctrl.run({ message: 'Hello-', autoSend: true });755assert.strictEqual(await p, undefined);756757// resend pending request without command detection758const request = ctrl.chatWidget.viewModel?.model.getRequests().at(-1);759assertType(request);760const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);761chatService.resendRequest(request, { noCommandDetection: true, attempt: request.attempt + 1, location: ChatAgentLocation.Editor });762763assert.strictEqual(await p2, undefined);764765assert.deepStrictEqual(commandDetection, [true, false]);766assert.strictEqual(model.getValue(), 'Hello-1');767});768769770test('Inline: Pressing Rerun request while the response streams breaks the response #5442', async function () {771772model.setValue('two\none\n');773774const attempts: (number | undefined)[] = [];775776const deferred = new DeferredPromise<void>();777778store.add(chatAgentService.registerDynamicAgent({779id: 'testEditorAgent2',780...agentData781}, {782async invoke(request, progress, history, token) {783784attempts.push(request.attempt);785786progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: `TRY:${request.attempt}\n` }] }]);787await raceCancellation(deferred.p, token);788deferred.complete();789await timeout(10);790return {};791},792}));793794ctrl = instaService.createInstance(TestController, editor);795796// REQUEST 1797const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]);798ctrl.run({ message: 'Hello-', autoSend: true });799assert.strictEqual(await p, undefined);800await timeout(10);801assert.deepStrictEqual(attempts, [0]);802803// RERUN (cancel, undo, redo)804const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);805const rerun = new RerunAction();806await instaService.invokeFunction(rerun.runInlineChatCommand, ctrl, editor);807assert.strictEqual(await p2, undefined);808809assert.deepStrictEqual(attempts, [0, 1]);810811assert.strictEqual(model.getValue(), 'TRY:1\ntwo\none\n');812813});814815test('Stopping/cancelling a request should NOT undo its changes', async function () {816817model.setValue('World');818819const deferred = new DeferredPromise<void>();820let progress: ((parts: IChatProgress[]) => void) | undefined;821822store.add(chatAgentService.registerDynamicAgent({823id: 'testEditorAgent2',824...agentData825}, {826async invoke(request, _progress, history, token) {827828progress = _progress;829await deferred.p;830return {};831},832}));833834ctrl = instaService.createInstance(TestController, editor);835836// REQUEST 1837const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]);838ctrl.run({ message: 'Hello', autoSend: true });839await timeout(10);840assert.strictEqual(await p, undefined);841842assertType(progress);843844const modelChange = new Promise<void>(resolve => model.onDidChangeContent(() => resolve()));845846progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'Hello-Hello' }] }]);847848await modelChange;849assert.strictEqual(model.getValue(), 'HelloWorld'); // first word has been streamed850851const p2 = ctrl.awaitStates([State.WAIT_FOR_INPUT]);852chatService.cancelCurrentRequestForSession(ctrl.chatWidget.viewModel!.model.sessionId);853assert.strictEqual(await p2, undefined);854855assert.strictEqual(model.getValue(), 'HelloWorld'); // CANCEL just stops the request and progressive typing but doesn't undo856857});858859test('Apply Edits from existing session w/ edits', async function () {860861model.setValue('');862863const newSession = await inlineChatSessionService.createSession(editor, {}, CancellationToken.None);864assertType(newSession);865866await (await chatService.sendRequest(newSession.chatModel.sessionId, 'Existing', { location: ChatAgentLocation.Editor }))?.responseCreatedPromise;867868assert.strictEqual(newSession.chatModel.requestInProgress, true);869870const response = newSession.chatModel.lastRequest?.response;871assertType(response);872873await new Promise(resolve => {874if (response.isComplete) {875resolve(undefined);876}877const d = response.onDidChange(() => {878if (response.isComplete) {879d.dispose();880resolve(undefined);881}882});883});884885ctrl = instaService.createInstance(TestController, editor);886const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE]);887ctrl.run({ existingSession: newSession });888889assert.strictEqual(await p, undefined);890891assert.strictEqual(model.getValue(), 'Existing');892893});894895test('Undo on error (2 rounds)', async function () {896897return runWithFakedTimers({}, async () => {898899900store.add(chatAgentService.registerDynamicAgent({ id: 'testEditorAgent', ...agentData, }, {901async invoke(request, progress, history, token) {902903progress([{904kind: 'textEdit',905uri: model.uri,906edits: [{907range: new Range(1, 1, 1, 1),908text: request.message909}]910}]);911912if (request.message === 'two') {913await timeout(100); // give edit a chance914return {915errorDetails: { message: 'FAILED' }916};917}918return {};919},920}));921922model.setValue('');923924// ROUND 1925926ctrl = instaService.createInstance(TestController, editor);927const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);928ctrl.run({ autoSend: true, message: 'one' });929assert.strictEqual(await p, undefined);930assert.strictEqual(model.getValue(), 'one');931932933// ROUND 2934935const p2 = ctrl.awaitStates([State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);936const values = new Set<string>();937store.add(model.onDidChangeContent(() => values.add(model.getValue())));938ctrl.chatWidget.acceptInput('two'); // WILL Trigger a failure939assert.strictEqual(await p2, undefined);940assert.strictEqual(model.getValue(), 'one'); // undone941assert.ok(values.has('twoone')); // we had but the change got undone942});943});944945test('Inline chat "discard" button does not always appear if response is stopped #228030', async function () {946947model.setValue('World');948949const deferred = new DeferredPromise<void>();950951store.add(chatAgentService.registerDynamicAgent({952id: 'testEditorAgent2',953...agentData954}, {955async invoke(request, progress, history, token) {956957progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'Hello-Hello' }] }]);958await deferred.p;959return {};960},961}));962963ctrl = instaService.createInstance(TestController, editor);964965// REQUEST 1966const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST]);967ctrl.run({ message: 'Hello', autoSend: true });968969970assert.strictEqual(await p, undefined);971972const p2 = ctrl.awaitStates([State.WAIT_FOR_INPUT]);973chatService.cancelCurrentRequestForSession(ctrl.chatWidget.viewModel!.model.sessionId);974assert.strictEqual(await p2, undefined);975976977const value = contextKeyService.getContextKeyValue(CTX_INLINE_CHAT_RESPONSE_TYPE.key);978assert.notStrictEqual(value, InlineChatResponseType.None);979});980981test('Restore doesn\'t edit on errored result', async function () {982return runWithFakedTimers({ useFakeTimers: true }, async () => {983984const model2 = store.add(instaService.get(IModelService).createModel('ABC', null));985986model.setValue('World');987988store.add(chatAgentService.registerDynamicAgent({989id: 'testEditorAgent2',990...agentData991}, {992async invoke(request, progress, history, token) {993994progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'Hello1' }] }]);995await timeout(100);996progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'Hello2' }] }]);997await timeout(100);998progress([{ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: 'Hello3' }] }]);999await timeout(100);10001001return {1002errorDetails: { message: 'FAILED' }1003};1004},1005}));10061007ctrl = instaService.createInstance(TestController, editor);10081009// REQUEST 11010const p = ctrl.awaitStates([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.WAIT_FOR_INPUT]);1011ctrl.run({ message: 'Hello', autoSend: true });10121013assert.strictEqual(await p, undefined);10141015const p2 = ctrl.awaitStates([State.PAUSE]);1016editor.setModel(model2);1017assert.strictEqual(await p2, undefined);10181019const p3 = ctrl.awaitStates([...TestController.INIT_SEQUENCE]);1020editor.setModel(model);1021assert.strictEqual(await p3, undefined);10221023assert.strictEqual(model.getValue(), 'World');1024});1025});1026});102710281029