Skip to content

Commit 4dde670

Browse files
authored
On request-removal undo till the point at which the request was made (microsoft#213902)
* On request-removal undo till the point at which the request was made fixes microsoft/vscode-copilot#5736 * fixed EOL sequence which (hopefully) fixes tests * give up, don't use new lines...
1 parent e545871 commit 4dde670

File tree

3 files changed

+173
-11
lines changed

3 files changed

+173
-11
lines changed

src/vs/workbench/contrib/inlineChat/browser/inlineChatController.ts

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -439,9 +439,18 @@ export class InlineChatController implements IEditorContribution {
439439
}
440440
});
441441
} else if (e.kind === 'removeRequest') {
442-
// TODO@jrieken this is buggy/weird when having code changes from multiple turns because
443-
// logically they are all the same hunks
444-
this._strategy?.cancel();
442+
// TODO@jrieken there is still some work left for when a request "in the middle"
443+
// is removed. We will undo all changes till that point but not remove those
444+
// later request
445+
const exchange = this._session!.exchanges.find(candidate => candidate.prompt.request.id === e.requestId);
446+
if (exchange && this._editor.hasModel()) {
447+
// undo till this point
448+
const model = this._editor.getModel();
449+
const targetAltVersion = exchange.prompt.modelAltVersionId;
450+
while (targetAltVersion < model.getAlternativeVersionId() && model.canUndo()) {
451+
await model.undo();
452+
}
453+
}
445454
}
446455
}));
447456

@@ -601,7 +610,7 @@ export class InlineChatController implements IEditorContribution {
601610
const input = request.message.text;
602611
this._ui.value.zone.widget.value = input;
603612

604-
this._session.addInput(new SessionPrompt(request));
613+
this._session.addInput(new SessionPrompt(request, this._editor.getModel()!.getAlternativeVersionId()));
605614

606615
return State.SHOW_REQUEST;
607616
}

src/vs/workbench/contrib/inlineChat/browser/inlineChatSession.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -206,6 +206,10 @@ export class Session {
206206
// this._teldata.responseTypes += `${exchange.response instanceof ReplyResponse ? exchange.response.responseType : InlineChatResponseTypes.Empty}|`;
207207
}
208208

209+
get exchanges(): readonly SessionExchange[] {
210+
return this._exchange;
211+
}
212+
209213
get lastExchange(): SessionExchange | undefined {
210214
return this._exchange[this._exchange.length - 1];
211215
}
@@ -273,7 +277,8 @@ export class SessionPrompt {
273277
readonly value: string;
274278

275279
constructor(
276-
readonly request: IChatRequestModel
280+
readonly request: IChatRequestModel,
281+
readonly modelAltVersionId: number,
277282
) {
278283
this.value = request.message.text;
279284
}

src/vs/workbench/contrib/inlineChat/test/browser/inlineChatController.test.ts

Lines changed: 154 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ import { IActiveCodeEditor } from 'vs/editor/browser/editorBrowser';
1414
import { IDiffProviderFactoryService } from 'vs/editor/browser/widget/diffEditor/diffProviderFactoryService';
1515
import { EditOperation } from 'vs/editor/common/core/editOperation';
1616
import { Range } from 'vs/editor/common/core/range';
17-
import { ITextModel } from 'vs/editor/common/model';
17+
import { EndOfLineSequence, ITextModel } from 'vs/editor/common/model';
1818
import { IEditorWorkerService } from 'vs/editor/common/services/editorWorker';
1919
import { IModelService } from 'vs/editor/common/services/model';
2020
import { TestDiffProviderFactoryService } from 'vs/editor/test/browser/diff/testDiffProviderFactoryService';
@@ -27,16 +27,16 @@ import { ServiceCollection } from 'vs/platform/instantiation/common/serviceColle
2727
import { TestInstantiationService } from 'vs/platform/instantiation/test/common/instantiationServiceMock';
2828
import { MockContextKeyService } from 'vs/platform/keybinding/test/common/mockKeybindingService';
2929
import { IEditorProgressService, IProgressRunner } from 'vs/platform/progress/common/progress';
30-
import { IViewDescriptorService } from 'vs/workbench/common/views';
30+
import { IView, IViewDescriptorService } from 'vs/workbench/common/views';
3131
import { AccessibilityVerbositySettingId } from 'vs/workbench/contrib/accessibility/browser/accessibilityConfiguration';
3232
import { IAccessibleViewService } from 'vs/platform/accessibility/browser/accessibleView';
33-
import { IChatAccessibilityService, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
33+
import { IChatAccessibilityService, IChatWidget, IChatWidgetService } from 'vs/workbench/contrib/chat/browser/chat';
3434
import { ChatAgentLocation, ChatAgentService, IChatAgentService } from 'vs/workbench/contrib/chat/common/chatAgents';
3535
import { IChatResponseViewModel } from 'vs/workbench/contrib/chat/common/chatViewModel';
3636
import { InlineChatController, InlineChatRunOptions, State } from 'vs/workbench/contrib/inlineChat/browser/inlineChatController';
3737
import { Session } from 'vs/workbench/contrib/inlineChat/browser/inlineChatSession';
3838
import { CTX_INLINE_CHAT_USER_DID_EDIT, EditMode, InlineChatConfigKeys } from 'vs/workbench/contrib/inlineChat/common/inlineChat';
39-
import { workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices';
39+
import { TestViewsService, workbenchInstantiationService } from 'vs/workbench/test/browser/workbenchTestServices';
4040
import { IInlineChatSavingService } from '../../browser/inlineChatSavingService';
4141
import { IInlineChatSessionService } from '../../browser/inlineChatSessionService';
4242
import { InlineChatSessionServiceImpl } from '../../browser/inlineChatSessionServiceImpl';
@@ -61,6 +61,7 @@ import { ICommandService } from 'vs/platform/commands/common/commands';
6161
import { TestCommandService } from 'vs/editor/test/browser/editorTestServices';
6262
import { INotebookEditorService } from 'vs/workbench/contrib/notebook/browser/services/notebookEditorService';
6363
import { RerunAction } from 'vs/workbench/contrib/inlineChat/browser/inlineChatActions';
64+
import { CancellationToken } from 'vs/base/common/cancellation';
6465

6566
suite('InteractiveChatController', function () {
6667

@@ -127,10 +128,13 @@ suite('InteractiveChatController', function () {
127128
let model: ITextModel;
128129
let ctrl: TestController;
129130
let contextKeyService: MockContextKeyService;
131+
let chatService: IChatService;
130132
let chatAgentService: IChatAgentService;
131133
let inlineChatSessionService: IInlineChatSessionService;
132134
let instaService: TestInstantiationService;
133135

136+
let chatWidget: IChatWidget;
137+
134138
setup(function () {
135139

136140
const serviceCollection = new ServiceCollection(
@@ -141,7 +145,11 @@ suite('InteractiveChatController', function () {
141145
[IHoverService, NullHoverService],
142146
[IExtensionService, new TestExtensionService()],
143147
[IContextKeyService, new MockContextKeyService()],
144-
[IViewsService, new TestExtensionService()],
148+
[IViewsService, new class extends TestViewsService {
149+
override async openView<T extends IView>(id: string, focus?: boolean | undefined): Promise<T | null> {
150+
return { widget: chatWidget ?? null } as any;
151+
}
152+
}()],
145153
[IWorkspaceContextService, new TestContextService()],
146154
[IChatWidgetHistoryService, new SyncDescriptor(ChatWidgetHistoryService)],
147155
[IChatWidgetService, new SyncDescriptor(ChatWidgetService)],
@@ -193,12 +201,13 @@ suite('InteractiveChatController', function () {
193201
configurationService.setUserConfiguration('editor', {});
194202

195203
contextKeyService = instaService.get(IContextKeyService) as MockContextKeyService;
196-
204+
chatService = instaService.get(IChatService);
197205
chatAgentService = instaService.get(IChatAgentService);
198206

199207
inlineChatSessionService = store.add(instaService.get(IInlineChatSessionService));
200208

201209
model = store.add(instaService.get(IModelService).createModel('Hello\nWorld\nHello Again\nHello World\n', null));
210+
model.setEOL(EndOfLineSequence.LF);
202211
editor = store.add(instantiateTestCodeEditor(instaService, model));
203212

204213
store.add(chatAgentService.registerDynamicAgent({ id: 'testEditorAgent', ...agentData, }, {
@@ -494,4 +503,143 @@ suite('InteractiveChatController', function () {
494503
ctrl.finishExistingSession();
495504
await r;
496505
});
506+
507+
test('Retry undoes all changes, not just those from the request#5736', async function () {
508+
509+
const text = [
510+
'eins-',
511+
'zwei-',
512+
'drei-'
513+
];
514+
515+
store.add(chatAgentService.registerDynamicAgent({
516+
id: 'testEditorAgent2',
517+
...agentData
518+
}, {
519+
async invoke(request, progress, history, token) {
520+
progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.shift() ?? '' }] });
521+
return {};
522+
},
523+
}));
524+
525+
ctrl = instaService.createInstance(TestController, editor);
526+
const rerun = new RerunAction();
527+
528+
model.setValue('');
529+
530+
// REQUEST 1
531+
const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]);
532+
const r = ctrl.run({ message: '1', autoSend: true });
533+
await p;
534+
535+
assert.strictEqual(model.getValue(), 'eins-');
536+
537+
// REQUEST 2
538+
const p2 = ctrl.waitFor([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]);
539+
await ctrl.acceptInput();
540+
await p2;
541+
542+
assert.strictEqual(model.getValue(), 'zwei-eins-');
543+
544+
// REQUEST 2 - RERUN
545+
const p3 = ctrl.waitFor([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]);
546+
await instaService.invokeFunction(rerun.runInlineChatCommand, ctrl, editor);
547+
await p3;
548+
549+
assert.strictEqual(model.getValue(), 'drei-eins-');
550+
551+
ctrl.finishExistingSession();
552+
await r;
553+
554+
});
555+
556+
test('moving inline chat to another model undoes changes', async function () {
557+
const text = [
558+
'eins\n',
559+
'zwei\n'
560+
];
561+
562+
store.add(chatAgentService.registerDynamicAgent({
563+
id: 'testEditorAgent2',
564+
...agentData
565+
}, {
566+
async invoke(request, progress, history, token) {
567+
progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.shift() ?? '' }] });
568+
return {};
569+
},
570+
}));
571+
ctrl = instaService.createInstance(TestController, editor);
572+
573+
// REQUEST 1
574+
const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]);
575+
ctrl.run({ message: '1', autoSend: true });
576+
await p;
577+
578+
579+
assert.strictEqual(model.getValue(), 'eins\nHello\nWorld\nHello Again\nHello World\n');
580+
581+
const targetModel = chatService.startSession(ChatAgentLocation.Editor, CancellationToken.None)!;
582+
store.add(targetModel);
583+
chatWidget = new class extends mock<IChatWidget>() {
584+
override get viewModel() {
585+
return { model: targetModel } as any;
586+
}
587+
override focusLastMessage() { }
588+
};
589+
590+
const r = ctrl.joinCurrentRun();
591+
await ctrl.viewInChat();
592+
593+
assert.strictEqual(model.getValue(), 'Hello\nWorld\nHello Again\nHello World\n');
594+
await r;
595+
});
596+
597+
test('moving inline chat to another model undoes changes (2 requests)', async function () {
598+
const text = [
599+
'eins\n',
600+
'zwei\n'
601+
];
602+
603+
store.add(chatAgentService.registerDynamicAgent({
604+
id: 'testEditorAgent2',
605+
...agentData
606+
}, {
607+
async invoke(request, progress, history, token) {
608+
progress({ kind: 'textEdit', uri: model.uri, edits: [{ range: new Range(1, 1, 1, 1), text: text.shift() ?? '' }] });
609+
return {};
610+
},
611+
}));
612+
ctrl = instaService.createInstance(TestController, editor);
613+
614+
// REQUEST 1
615+
const p = ctrl.waitFor([...TestController.INIT_SEQUENCE, State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]);
616+
ctrl.run({ message: '1', autoSend: true });
617+
await p;
618+
619+
assert.strictEqual(model.getValue(), 'eins\nHello\nWorld\nHello Again\nHello World\n');
620+
621+
// REQUEST 2
622+
const p2 = ctrl.waitFor([State.SHOW_REQUEST, State.SHOW_RESPONSE, State.WAIT_FOR_INPUT]);
623+
await ctrl.acceptInput();
624+
await p2;
625+
626+
assert.strictEqual(model.getValue(), 'zwei\neins\nHello\nWorld\nHello Again\nHello World\n');
627+
628+
const targetModel = chatService.startSession(ChatAgentLocation.Editor, CancellationToken.None)!;
629+
store.add(targetModel);
630+
chatWidget = new class extends mock<IChatWidget>() {
631+
override get viewModel() {
632+
return { model: targetModel } as any;
633+
}
634+
override focusLastMessage() { }
635+
};
636+
637+
const r = ctrl.joinCurrentRun();
638+
639+
await ctrl.viewInChat();
640+
641+
assert.strictEqual(model.getValue(), 'Hello\nWorld\nHello Again\nHello World\n');
642+
643+
await r;
644+
});
497645
});

0 commit comments

Comments
 (0)