diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts index 32cd6e3de7b..a9ef3840595 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingActions.ts @@ -28,6 +28,7 @@ import { KeybindingWeight } from '../../../../../platform/keybinding/common/keyb import { IEditorPane } from '../../../../common/editor.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { IAgentSessionsService } from '../agentSessions/agentSessionsService.js'; +import { IChatRequestVariableEntry, isImplicitVariableEntry, isPromptFileVariableEntry, isPromptTextVariableEntry, isStringVariableEntry, isWorkspaceVariableEntry } from '../../common/attachments/chatVariableEntries.js'; import { isChatViewTitleActionContext } from '../../common/actions/chatActions.js'; import { ChatContextKeys } from '../../common/actions/chatContextKeys.js'; import { applyingChatEditsFailedContextKey, CHAT_EDITING_MULTI_DIFF_SOURCE_RESOLVER_SCHEME, chatEditingResourceContextKey, chatEditingWidgetFileStateContextKey, decidedChatEditingResourceContextKey, hasAppliedChatEditsContextKey, hasUndecidedChatEditingResourceContextKey, IChatEditingService, IChatEditingSession, ModifiedFileEntryState } from '../../common/editing/chatEditingService.js'; @@ -414,7 +415,20 @@ export class ViewAllSessionChangesAction extends Action2 { } registerAction2(ViewAllSessionChangesAction); -async function restoreSnapshotWithConfirmationByRequestId(accessor: ServicesAccessor, sessionResource: URI, requestId: string): Promise { +function filterToUserAttachedContext(attachedContext: readonly IChatRequestVariableEntry[] | undefined): IChatRequestVariableEntry[] { + if (!attachedContext?.length) { + return []; + } + return attachedContext.filter(a => + !isImplicitVariableEntry(a) && + !isWorkspaceVariableEntry(a) && + !isStringVariableEntry(a) && + !(isPromptFileVariableEntry(a) && a.automaticallyAdded) && + !(isPromptTextVariableEntry(a) && a.automaticallyAdded) + ); +} + +async function restoreSnapshotWithConfirmationByRequestId(accessor: ServicesAccessor, sessionResource: URI, requestId: string): Promise { const configurationService = accessor.get(IConfigurationService); const dialogService = accessor.get(IDialogService); const chatWidgetService = accessor.get(IChatWidgetService); @@ -422,18 +436,18 @@ async function restoreSnapshotWithConfirmationByRequestId(accessor: ServicesAcce const chatService = accessor.get(IChatService); const chatModel = chatService.getSession(sessionResource); if (!chatModel) { - return; + return false; } const session = chatModel.editingSession; if (!session) { - return; + return false; } const chatRequests = chatModel.getRequests(); const itemIndex = chatRequests.findIndex(request => request.id === requestId); if (itemIndex === -1) { - return; + return false; } const editsToUndo = chatRequests.length - itemIndex; @@ -472,7 +486,7 @@ async function restoreSnapshotWithConfirmationByRequestId(accessor: ServicesAcce if (!confirmation.confirmed) { widget?.viewModel?.model.setCheckpoint(undefined); - return; + return false; } if (confirmation.checkboxChecked) { @@ -482,17 +496,18 @@ async function restoreSnapshotWithConfirmationByRequestId(accessor: ServicesAcce // Restore the snapshot to what it was before the request(s) that we deleted const snapshotRequestId = chatRequests[itemIndex].id; await session.restoreSnapshot(snapshotRequestId, undefined); + return true; } -async function restoreSnapshotWithConfirmation(accessor: ServicesAccessor, item: ChatTreeItem): Promise { +async function restoreSnapshotWithConfirmation(accessor: ServicesAccessor, item: ChatTreeItem): Promise { const requestId = isRequestVM(item) ? item.id : isResponseVM(item) ? item.requestId : undefined; if (!requestId) { - return; + return false; } - await restoreSnapshotWithConfirmationByRequestId(accessor, item.sessionResource, requestId); + return restoreSnapshotWithConfirmationByRequestId(accessor, item.sessionResource, requestId); } registerAction2(class RemoveAction extends Action2 { @@ -535,11 +550,15 @@ registerAction2(class RemoveAction extends Action2 { return; } - await restoreSnapshotWithConfirmation(accessor, item); + const confirmed = await restoreSnapshotWithConfirmation(accessor, item); - if (isRequestVM(item) && configurationService.getValue('chat.undoRequests.restoreInput')) { + if (confirmed && isRequestVM(item) && configurationService.getValue('chat.undoRequests.restoreInput')) { widget?.focusInput(); widget?.input.setValue(item.messageText, false); + const userAttachments = filterToUserAttachedContext(item.attachedContext); + if (userAttachments.length) { + await widget?.input.restoreAttachments(userAttachments); + } } } }); @@ -583,13 +602,19 @@ registerAction2(class RestoreCheckpointAction extends Action2 { return; } + const userAttachments = isRequestVM(item) ? filterToUserAttachedContext(item.attachedContext) : []; + if (isRequestVM(item)) { widget?.focusInput(); widget?.input.setValue(item.messageText, false); } widget?.viewModel?.model.setCheckpoint(item.id); - await restoreSnapshotWithConfirmation(accessor, item); + const confirmed = await restoreSnapshotWithConfirmation(accessor, item); + + if (confirmed && userAttachments.length) { + await widget?.input.restoreAttachments(userAttachments); + } } }); diff --git a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts index 7ad7f579cae..08a4ed41ac9 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/widget/input/chatInputPart.ts @@ -16,6 +16,7 @@ import { HoverPosition } from '../../../../../../base/browser/ui/hover/hoverWidg import { IAction } from '../../../../../../base/common/actions.js'; import { equals as arraysEqual } from '../../../../../../base/common/arrays.js'; import { DeferredPromise, RunOnceScheduler } from '../../../../../../base/common/async.js'; +import { isDefined } from '../../../../../../base/common/types.js'; import { CancellationToken } from '../../../../../../base/common/cancellation.js'; import { Codicon } from '../../../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../../../base/common/event.js'; @@ -1379,16 +1380,15 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge this.navigateHistory(false); } - private async navigateHistory(previous: boolean): Promise { - const historyEntry = previous ? - this.history.previous() : this.history.next(); + /** + * Restores attachments to the input, re-fetching image binary data as needed. + */ + async restoreAttachments(attachments: readonly IChatRequestVariableEntry[]): Promise { + let restored = [...attachments]; - let historyAttachments = historyEntry?.attachments ?? []; - - // Check for images in history to restore the value. - if (historyAttachments.length > 0) { - historyAttachments = (await Promise.all(historyAttachments.map(async (attachment) => { - if (isImageVariableEntry(attachment) && attachment.references?.length && URI.isUri(attachment.references[0].reference)) { + if (restored.length > 0) { + restored = (await Promise.all(restored.map(async (attachment) => { + if (isImageVariableEntry(attachment) && !attachment.value && attachment.references?.length && URI.isUri(attachment.references[0].reference)) { const currReference = attachment.references[0].reference; try { const imageBinary = currReference.toString(true).startsWith('http') ? await this.sharedWebExtracterService.readImage(currReference, CancellationToken.None) : (await this.fileService.readFile(currReference)).value; @@ -1396,7 +1396,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return undefined; } const newAttachment = { ...attachment }; - newAttachment.value = (isImageVariableEntry(attachment) && attachment.isPasted) ? imageBinary.buffer : await resizeImage(imageBinary.buffer); // if pasted image, we do not need to resize. + newAttachment.value = (isImageVariableEntry(attachment) && attachment.isPasted) ? imageBinary.buffer : await resizeImage(imageBinary.buffer); return newAttachment; } catch (err) { this.logService.error('Failed to fetch and reference.', err); @@ -1404,10 +1404,17 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } } return attachment; - }))).filter(attachment => attachment !== undefined); + }))).filter(isDefined); } - this._attachmentModel.clearAndSetContext(...historyAttachments); + this._attachmentModel.clearAndSetContext(...restored); + } + + private async navigateHistory(previous: boolean): Promise { + const historyEntry = previous ? + this.history.previous() : this.history.next(); + + await this.restoreAttachments(historyEntry?.attachments ?? []); const inputText = historyEntry?.inputText ?? ''; const contribData = historyEntry?.contrib ?? {}; diff --git a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css index dd77d092198..651c9f48168 100644 --- a/src/vs/workbench/contrib/chat/browser/widget/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/widget/media/chat.css @@ -3235,10 +3235,6 @@ have to be updated for changes to the rules above, or to support more deeply nes .request-hover { top: 0px; } - - .checkpoint-container { - display: none; - } } .interactive-list > .monaco-list:focus > .monaco-scrollable-element > .monaco-list-rows > .monaco-list-row.focused.request { diff --git a/src/vs/workbench/contrib/chat/common/model/chatModel.ts b/src/vs/workbench/contrib/chat/common/model/chatModel.ts index 2fb3d3943ff..547db1b42f2 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatModel.ts @@ -1887,7 +1887,7 @@ class InputModel implements IInputModel { return { contrib: value.contrib, - attachments: persistableAttachments, + attachments: persistableAttachments.map(IChatRequestVariableEntry.toExport), mode: value.mode, selectedModel: value.selectedModel ? { identifier: value.selectedModel.identifier, @@ -2179,7 +2179,7 @@ export class ChatModel extends Disposable implements IChatModel { // Initialize input model from serialized data (undefined for new chats) const serializedInputState = initialModelProps.inputState || (isValidFullData && initialData.inputState ? initialData.inputState : undefined); this.inputModel = new InputModel(serializedInputState && { - attachments: serializedInputState.attachments, + attachments: (serializedInputState.attachments ?? []).map(IChatRequestVariableEntry.fromExport), mode: serializedInputState.mode, selectedModel: serializedInputState.selectedModel && { identifier: serializedInputState.selectedModel.identifier, diff --git a/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts b/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts index bc7b42e663c..c1cf5de1c14 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatSessionOperationLog.ts @@ -9,6 +9,7 @@ import { equals as objectsEqual } from '../../../../../base/common/objects.js'; import { isEqual as _urisEqual } from '../../../../../base/common/resources.js'; import { hasKey } from '../../../../../base/common/types.js'; import { URI, UriComponents } from '../../../../../base/common/uri.js'; +import { IChatRequestVariableEntry } from '../attachments/chatVariableEntries.js'; import { IChatMarkdownContent, ResponseModelState } from '../chatService/chatService.js'; import { ModifiedFileEntryState } from '../editing/chatEditingService.js'; import { IParsedChatRequest } from '../requestParser/chatParserTypes.js'; @@ -116,7 +117,7 @@ const agentEditedFileEventSchema = Adapt.object({ - variables: Adapt.t(v => v.variables, Adapt.array(Adapt.value((a, b) => a.name === b.name))), + variables: Adapt.t(v => v.variables.map(IChatRequestVariableEntry.toExport), Adapt.array(Adapt.value((a, b) => a.name === b.name))), }); const requestSchema = Adapt.object({ @@ -155,7 +156,7 @@ const requestSchema = Adapt.object({ - attachments: Adapt.v(i => i.attachments, objectsEqual), + attachments: Adapt.v(i => i.attachments.map(IChatRequestVariableEntry.toExport), objectsEqual), mode: Adapt.v(i => i.mode, (a, b) => a.id === b.id), selectedModel: Adapt.v(i => i.selectedModel, (a, b) => a?.identifier === b?.identifier), inputText: Adapt.v(i => i.inputText), diff --git a/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts index a6fd04dae2b..ca0d6dedb00 100644 --- a/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/model/chatViewModel.ts @@ -94,6 +94,7 @@ export interface IChatRequestViewModel { readonly slashCommand: IChatAgentCommand | undefined; readonly agentOrSlashCommandDetected: boolean; readonly shouldBeBlocked: IObservable; + readonly attachedContext?: readonly IChatRequestVariableEntry[]; readonly modelId?: string; readonly timestamp: number; /** The kind of pending request, or undefined if not pending */ @@ -462,6 +463,10 @@ export class ChatRequestViewModel implements IChatRequestViewModel { currentRenderedHeight: number | undefined; + get attachedContext() { + return this._model.attachedContext; + } + get modelId() { return this._model.modelId; }