Skip to content

Commit 437775d

Browse files
authored
first pass checkpoints inline in chat (microsoft#254564)
* first pass checkpoints inline in chat: * add undo redo buttons back * quieter version * functionally, we are good here. lots to clean up asdfasdfasdfasdfafdasf * nice, some cleanup poggersgit add .git add . * better css * cleanup * remove extra instance service
1 parent 4845921 commit 437775d

File tree

9 files changed

+302
-77
lines changed

9 files changed

+302
-77
lines changed

src/vs/platform/actions/common/actions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,8 @@ export class MenuId {
247247
static readonly ChatInputSymbolAttachmentContext = new MenuId('ChatInputSymbolAttachmentContext');
248248
static readonly ChatInlineResourceAnchorContext = new MenuId('ChatInlineResourceAnchorContext');
249249
static readonly ChatInlineSymbolAnchorContext = new MenuId('ChatInlineSymbolAnchorContext');
250+
static readonly ChatMessageCheckpoint: MenuId = new MenuId('ChatMessageCheckpoint');
251+
static readonly ChatMessageRestoreCheckpoint: MenuId = new MenuId('ChatMessageRestoreCheckpoint');
250252
static readonly ChatEditingCodeBlockContext = new MenuId('ChatEditingCodeBlockContext');
251253
static readonly ChatTitleBarMenu = new MenuId('ChatTitleBarMenu');
252254
static readonly ChatAttachmentsContext = new MenuId('ChatAttachmentsContext');

src/vs/workbench/contrib/chat/browser/actions/chatClearActions.ts

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { ActiveEditorContext } from '../../../../common/contextkeys.js';
1717
import { ChatContextKeys } from '../../common/chatContextKeys.js';
1818
import { IChatEditingSession } from '../../common/chatEditingService.js';
1919
import { ChatModeKind } from '../../common/constants.js';
20-
import { ChatViewId, IChatWidget } from '../chat.js';
20+
import { ChatViewId, IChatWidget, IChatWidgetService } from '../chat.js';
2121
import { EditingSessionAction } from '../chatEditing/chatEditingActions.js';
2222
import { ChatEditorInput } from '../chatEditorInput.js';
2323
import { ACTION_ID_NEW_CHAT, ACTION_ID_NEW_EDIT_SESSION, CHAT_CATEGORY, handleCurrentEditingSession } from './chatActions.js';
@@ -148,7 +148,8 @@ export function registerNewChatActions() {
148148
id: MenuId.ViewTitle,
149149
when: ContextKeyExpr.equals('view', ChatViewId),
150150
group: 'navigation',
151-
order: -3
151+
order: -3,
152+
isHiddenByDefault: true
152153
}]
153154
});
154155
}
@@ -167,17 +168,54 @@ export function registerNewChatActions() {
167168
icon: Codicon.redo,
168169
precondition: ContextKeyExpr.and(ChatContextKeys.chatEditingCanRedo, ChatContextKeys.enabled, ChatContextKeys.editingParticipantRegistered),
169170
f1: true,
171+
menu: [
172+
{
173+
id: MenuId.ViewTitle,
174+
when: ContextKeyExpr.equals('view', ChatViewId),
175+
group: 'navigation',
176+
order: -2,
177+
isHiddenByDefault: true
178+
}
179+
]
180+
});
181+
}
182+
183+
async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession) {
184+
await editingSession.redoInteraction();
185+
}
186+
});
187+
188+
registerAction2(class RedoChatEditInteractionAction2 extends EditingSessionAction {
189+
constructor() {
190+
super({
191+
id: 'workbench.action.chat.redoEdit2',
192+
title: localize2('chat.redoEdit.label2', "Redo Last Request"),
193+
category: CHAT_CATEGORY,
194+
precondition: ContextKeyExpr.and(ChatContextKeys.chatEditingCanRedo, ChatContextKeys.enabled, ChatContextKeys.editingParticipantRegistered),
195+
f1: true,
170196
menu: [{
171-
id: MenuId.ViewTitle,
197+
id: MenuId.ChatMessageRestoreCheckpoint,
172198
when: ContextKeyExpr.equals('view', ChatViewId),
173199
group: 'navigation',
174-
order: -2
200+
order: -1
175201
}]
176202
});
177203
}
178204

179205
async runEditingSessionAction(accessor: ServicesAccessor, editingSession: IChatEditingSession) {
180-
await editingSession.redoInteraction();
206+
const widget = accessor.get(IChatWidgetService);
207+
208+
const redoUntilDone = async () => {
209+
if (!editingSession.canRedo.get()) {
210+
return;
211+
}
212+
213+
await editingSession.redoInteraction();
214+
setTimeout(() => redoUntilDone(), 0);
215+
};
216+
217+
await redoUntilDone();
218+
widget.lastFocusedWidget?.viewModel?.model.setCheckpoint(undefined);
181219
}
182220
});
183221
}

src/vs/workbench/contrib/chat/browser/actions/chatExecuteActions.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,8 @@ abstract class SubmitAction extends Action2 {
142142
const snapshotRequestId = chatRequests[itemIndex].id;
143143
await session.restoreSnapshot(snapshotRequestId, undefined);
144144
}
145+
} else if (widget?.viewModel?.model.checkpoint) {
146+
widget.viewModel.model.setCheckpoint(undefined);
145147
}
146148
widget?.acceptInput(context?.inputValue);
147149
}

src/vs/workbench/contrib/chat/browser/chat.contribution.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -268,6 +268,12 @@ configurationRegistry.registerConfiguration({
268268
description: nls.localize('chat.emptyChatState', "Shows a modified empty chat state with hints in the input placeholder text."),
269269
tags: ['experimental', 'onExp'],
270270
},
271+
'chat.checkpoints.enabled': {
272+
type: 'boolean',
273+
default: true,
274+
description: nls.localize('chat.checkpoints.enabled', "Enables checkpoints in chat. Checkpoints allow you to restore the chat to a previous state."),
275+
tags: ['experimental'],
276+
},
271277
[mcpEnabledSection]: {
272278
type: 'boolean',
273279
description: nls.localize('chat.mcp.enabled', "Enables integration with Model Context Protocol servers to provide additional tools and functionality."),

src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts

Lines changed: 112 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,74 @@ export class ChatEditingShowChangesAction extends EditingSessionAction {
305305
}
306306
registerAction2(ChatEditingShowChangesAction);
307307

308+
async function restoreSnapshotWithConfirmation(accessor: ServicesAccessor, item: ChatTreeItem): Promise<void> {
309+
const configurationService = accessor.get(IConfigurationService);
310+
const dialogService = accessor.get(IDialogService);
311+
const chatService = accessor.get(IChatService);
312+
const chatModel = chatService.getSession(item.sessionId);
313+
if (!chatModel) {
314+
return;
315+
}
316+
317+
const session = chatModel.editingSession;
318+
if (!session) {
319+
return;
320+
}
321+
322+
const requestId = isRequestVM(item) ? item.id :
323+
isResponseVM(item) ? item.requestId : undefined;
324+
325+
if (requestId) {
326+
const chatRequests = chatModel.getRequests();
327+
const itemIndex = chatRequests.findIndex(request => request.id === requestId);
328+
const editsToUndo = chatRequests.length - itemIndex;
329+
330+
const requestsToRemove = chatRequests.slice(itemIndex);
331+
const requestIdsToRemove = new Set(requestsToRemove.map(request => request.id));
332+
const entriesModifiedInRequestsToRemove = session.entries.get().filter((entry) => requestIdsToRemove.has(entry.lastModifyingRequestId)) ?? [];
333+
const shouldPrompt = entriesModifiedInRequestsToRemove.length > 0 && configurationService.getValue('chat.editing.confirmEditRequestRemoval') === true;
334+
335+
let message: string;
336+
if (editsToUndo === 1) {
337+
if (entriesModifiedInRequestsToRemove.length === 1) {
338+
message = localize('chat.removeLast.confirmation.message2', "This will remove your last request and undo the edits made to {0}. Do you want to proceed?", basename(entriesModifiedInRequestsToRemove[0].modifiedURI));
339+
} else {
340+
message = localize('chat.removeLast.confirmation.multipleEdits.message', "This will remove your last request and undo edits made to {0} files in your working set. Do you want to proceed?", entriesModifiedInRequestsToRemove.length);
341+
}
342+
} else {
343+
if (entriesModifiedInRequestsToRemove.length === 1) {
344+
message = localize('chat.remove.confirmation.message2', "This will remove all subsequent requests and undo edits made to {0}. Do you want to proceed?", basename(entriesModifiedInRequestsToRemove[0].modifiedURI));
345+
} else {
346+
message = localize('chat.remove.confirmation.multipleEdits.message', "This will remove all subsequent requests and undo edits made to {0} files in your working set. Do you want to proceed?", entriesModifiedInRequestsToRemove.length);
347+
}
348+
}
349+
350+
const confirmation = shouldPrompt
351+
? await dialogService.confirm({
352+
title: editsToUndo === 1
353+
? localize('chat.removeLast.confirmation.title', "Do you want to undo your last edit?")
354+
: localize('chat.remove.confirmation.title', "Do you want to undo {0} edits?", editsToUndo),
355+
message: message,
356+
primaryButton: localize('chat.remove.confirmation.primaryButton', "Yes"),
357+
checkbox: { label: localize('chat.remove.confirmation.checkbox', "Don't ask again"), checked: false },
358+
type: 'info'
359+
})
360+
: { confirmed: true };
361+
362+
if (!confirmation.confirmed) {
363+
return;
364+
}
365+
366+
if (confirmation.checkboxChecked) {
367+
await configurationService.updateValue('chat.editing.confirmEditRequestRemoval', false);
368+
}
369+
370+
// Restore the snapshot to what it was before the request(s) that we deleted
371+
const snapshotRequestId = chatRequests[itemIndex].id;
372+
await session.restoreSnapshot(snapshotRequestId, undefined);
373+
}
374+
}
375+
308376
registerAction2(class RemoveAction extends Action2 {
309377
constructor() {
310378
super({
@@ -326,7 +394,7 @@ registerAction2(class RemoveAction extends Action2 {
326394
id: MenuId.ChatMessageTitle,
327395
group: 'navigation',
328396
order: 2,
329-
when: ContextKeyExpr.and(ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'input').negate())
397+
when: ContextKeyExpr.and(ContextKeyExpr.equals(`config.${ChatConfiguration.EditRequests}`, 'input').negate(), ContextKeyExpr.equals(`config.${ChatConfiguration.CheckpointsEnabled}`, false)),
330398
}
331399
]
332400
});
@@ -335,6 +403,7 @@ registerAction2(class RemoveAction extends Action2 {
335403
async run(accessor: ServicesAccessor, ...args: any[]) {
336404
let item: ChatTreeItem | undefined = args[0];
337405
const chatWidgetService = accessor.get(IChatWidgetService);
406+
const configurationService = accessor.get(IConfigurationService);
338407
const widget = chatWidgetService.lastFocusedWidget;
339408
if (!isResponseVM(item) && !isRequestVM(item)) {
340409
item = widget?.getFocus();
@@ -344,76 +413,56 @@ registerAction2(class RemoveAction extends Action2 {
344413
return;
345414
}
346415

347-
const configurationService = accessor.get(IConfigurationService);
348-
const dialogService = accessor.get(IDialogService);
349-
const chatService = accessor.get(IChatService);
350-
const chatModel = chatService.getSession(item.sessionId);
351-
if (!chatModel) {
352-
return;
353-
}
416+
await restoreSnapshotWithConfirmation(accessor, item);
354417

355-
const session = chatModel.editingSession;
356-
if (!session) {
357-
return;
418+
if (isRequestVM(item) && configurationService.getValue('chat.undoRequests.restoreInput')) {
419+
widget?.focusInput();
420+
widget?.input.setValue(item.messageText, false);
358421
}
422+
}
423+
});
359424

360-
const requestId = isRequestVM(item) ? item.id :
361-
isResponseVM(item) ? item.requestId : undefined;
362-
363-
if (requestId) {
364-
const chatRequests = chatModel.getRequests();
365-
const itemIndex = chatRequests.findIndex(request => request.id === requestId);
366-
const editsToUndo = chatRequests.length - itemIndex;
367-
368-
const requestsToRemove = chatRequests.slice(itemIndex);
369-
const requestIdsToRemove = new Set(requestsToRemove.map(request => request.id));
370-
const entriesModifiedInRequestsToRemove = session.entries.get().filter((entry) => requestIdsToRemove.has(entry.lastModifyingRequestId)) ?? [];
371-
const shouldPrompt = entriesModifiedInRequestsToRemove.length > 0 && configurationService.getValue('chat.editing.confirmEditRequestRemoval') === true;
372-
373-
let message: string;
374-
if (editsToUndo === 1) {
375-
if (entriesModifiedInRequestsToRemove.length === 1) {
376-
message = localize('chat.removeLast.confirmation.message2', "This will remove your last request and undo the edits made to {0}. Do you want to proceed?", basename(entriesModifiedInRequestsToRemove[0].modifiedURI));
377-
} else {
378-
message = localize('chat.removeLast.confirmation.multipleEdits.message', "This will remove your last request and undo edits made to {0} files in your working set. Do you want to proceed?", entriesModifiedInRequestsToRemove.length);
379-
}
380-
} else {
381-
if (entriesModifiedInRequestsToRemove.length === 1) {
382-
message = localize('chat.remove.confirmation.message2', "This will remove all subsequent requests and undo edits made to {0}. Do you want to proceed?", basename(entriesModifiedInRequestsToRemove[0].modifiedURI));
383-
} else {
384-
message = localize('chat.remove.confirmation.multipleEdits.message', "This will remove all subsequent requests and undo edits made to {0} files in your working set. Do you want to proceed?", entriesModifiedInRequestsToRemove.length);
425+
registerAction2(class RestoreCheckpointAction extends Action2 {
426+
constructor() {
427+
super({
428+
id: 'workbench.action.chat.restoreCheckpoint',
429+
title: localize2('chat.restoreCheckpoint.label', "Restore Checkpoint"),
430+
tooltip: localize2('chat.restoreCheckpoint.tooltip', "Restore Checkpoint"),
431+
f1: false,
432+
category: CHAT_CATEGORY,
433+
keybinding: {
434+
primary: KeyCode.Delete,
435+
mac: {
436+
primary: KeyMod.CtrlCmd | KeyCode.Backspace,
437+
},
438+
when: ContextKeyExpr.and(ChatContextKeys.inChatSession, EditorContextKeys.textInputFocus.negate()),
439+
weight: KeybindingWeight.WorkbenchContrib,
440+
},
441+
menu: [
442+
{
443+
id: MenuId.ChatMessageCheckpoint,
444+
group: 'navigation',
445+
order: 2,
446+
when: ChatContextKeys.isRequest
385447
}
386-
}
387-
388-
const confirmation = shouldPrompt
389-
? await dialogService.confirm({
390-
title: editsToUndo === 1
391-
? localize('chat.removeLast.confirmation.title', "Do you want to undo your last edit?")
392-
: localize('chat.remove.confirmation.title', "Do you want to undo {0} edits?", editsToUndo),
393-
message: message,
394-
primaryButton: localize('chat.remove.confirmation.primaryButton', "Yes"),
395-
checkbox: { label: localize('chat.remove.confirmation.checkbox', "Don't ask again"), checked: false },
396-
type: 'info'
397-
})
398-
: { confirmed: true };
399-
400-
if (!confirmation.confirmed) {
401-
return;
402-
}
403-
404-
if (confirmation.checkboxChecked) {
405-
await configurationService.updateValue('chat.editing.confirmEditRequestRemoval', false);
406-
}
448+
]
449+
});
450+
}
407451

408-
// Restore the snapshot to what it was before the request(s) that we deleted
409-
const snapshotRequestId = chatRequests[itemIndex].id;
410-
await session.restoreSnapshot(snapshotRequestId, undefined);
452+
async run(accessor: ServicesAccessor, ...args: any[]) {
453+
let item: ChatTreeItem | undefined = args[0];
454+
const chatWidgetService = accessor.get(IChatWidgetService);
455+
const widget = chatWidgetService.lastFocusedWidget;
456+
if (!isResponseVM(item) && !isRequestVM(item)) {
457+
item = widget?.getFocus();
411458
}
412459

413-
if (isRequestVM(item) && configurationService.getValue('chat.undoRequests.restoreInput')) {
414-
widget?.focusInput();
415-
widget?.input.setValue(item.messageText, false);
460+
if (!item) {
461+
return;
416462
}
463+
464+
widget?.viewModel?.model.setCheckpoint(item.id);
465+
await restoreSnapshotWithConfirmation(accessor, item);
417466
}
418467
});
419468

0 commit comments

Comments
 (0)