diff --git a/src/vs/editor/common/languages.ts b/src/vs/editor/common/languages.ts index a3156dea8abdd..52e4196f81da0 100644 --- a/src/vs/editor/common/languages.ts +++ b/src/vs/editor/common/languages.ts @@ -1753,7 +1753,14 @@ export interface IWorkspaceTextEdit { } export interface WorkspaceEdit { - edits: Array; + edits: Array; +} + +export interface ICustomEdit { + readonly resource: URI; + readonly metadata?: WorkspaceEditMetadata; + undo(): Promise | void; + redo(): Promise | void; } export interface Rejection { diff --git a/src/vs/monaco.d.ts b/src/vs/monaco.d.ts index 5e8409195fa08..ffa85db53aea2 100644 --- a/src/vs/monaco.d.ts +++ b/src/vs/monaco.d.ts @@ -7954,7 +7954,14 @@ declare namespace monaco.languages { } export interface WorkspaceEdit { - edits: Array; + edits: Array; + } + + export interface ICustomEdit { + readonly resource: Uri; + readonly metadata?: WorkspaceEditMetadata; + undo(): Promise | void; + redo(): Promise | void; } export interface Rejection { diff --git a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts index 60e1812f4336a..b20903fbab38d 100644 --- a/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts +++ b/src/vs/workbench/contrib/bulkEdit/browser/bulkEditService.ts @@ -28,6 +28,7 @@ import { BulkTextEdits } from './bulkTextEdits.js'; import { IEditorService } from '../../../services/editor/common/editorService.js'; import { ILifecycleService, ShutdownReason } from '../../../services/lifecycle/common/lifecycle.js'; import { IWorkingCopyService } from '../../../services/workingCopy/common/workingCopyService.js'; +import { OpaqueEdits, ResourceAttachmentEdit } from './opaqueEdits.js'; function liftEdits(edits: ResourceEdit[]): ResourceEdit[] { return edits.map(edit => { @@ -40,6 +41,11 @@ function liftEdits(edits: ResourceEdit[]): ResourceEdit[] { if (ResourceNotebookCellEdit.is(edit)) { return ResourceNotebookCellEdit.lift(edit); } + + if (ResourceAttachmentEdit.is(edit)) { + return ResourceAttachmentEdit.lift(edit); + } + throw new Error('Unsupported edit'); }); } @@ -122,6 +128,8 @@ class BulkEdit { resources.push(await this._performTextEdits(group, this._undoRedoGroup, this._undoRedoSource, progress)); } else if (group[0] instanceof ResourceNotebookCellEdit) { resources.push(await this._performCellEdits(group, this._undoRedoGroup, this._undoRedoSource, progress)); + } else if (group[0] instanceof ResourceAttachmentEdit) { + resources.push(await this._performOpaqueEdits(group, this._undoRedoGroup, this._undoRedoSource, progress)); } else { console.log('UNKNOWN EDIT'); } @@ -148,6 +156,12 @@ class BulkEdit { const model = this._instaService.createInstance(BulkCellEdits, undoRedoGroup, undoRedoSource, progress, this._token, edits); return await model.apply(); } + + private async _performOpaqueEdits(edits: ResourceAttachmentEdit[], undoRedoGroup: UndoRedoGroup, undoRedoSource: UndoRedoSource | undefined, progress: IProgress): Promise { + this._logService.debug('_performOpaqueEdits', JSON.stringify(edits)); + const model = this._instaService.createInstance(OpaqueEdits, undoRedoGroup, undoRedoSource, progress, this._token, edits); + return await model.apply(); + } } export class BulkEditService implements IBulkEditService { diff --git a/src/vs/workbench/contrib/bulkEdit/browser/opaqueEdits.ts b/src/vs/workbench/contrib/bulkEdit/browser/opaqueEdits.ts new file mode 100644 index 0000000000000..a8615ee859ef7 --- /dev/null +++ b/src/vs/workbench/contrib/bulkEdit/browser/opaqueEdits.ts @@ -0,0 +1,79 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { CancellationToken } from '../../../../base/common/cancellation.js'; +import { isObject } from '../../../../base/common/types.js'; +import { URI } from '../../../../base/common/uri.js'; +import { ResourceEdit } from '../../../../editor/browser/services/bulkEditService.js'; +import { ICustomEdit, WorkspaceEditMetadata } from '../../../../editor/common/languages.js'; +import { IProgress } from '../../../../platform/progress/common/progress.js'; +import { IUndoRedoService, UndoRedoElementType, UndoRedoGroup, UndoRedoSource } from '../../../../platform/undoRedo/common/undoRedo.js'; + +export class ResourceAttachmentEdit extends ResourceEdit implements ICustomEdit { + + static is(candidate: any): candidate is ICustomEdit { + if (candidate instanceof ResourceAttachmentEdit) { + return true; + } else { + return isObject(candidate) + && (Boolean((candidate).undo && (candidate).redo)); + } + } + + static lift(edit: ICustomEdit): ResourceAttachmentEdit { + if (edit instanceof ResourceAttachmentEdit) { + return edit; + } else { + return new ResourceAttachmentEdit(edit.resource, edit.undo, edit.redo, edit.metadata); + } + } + + constructor( + readonly resource: URI, + readonly undo: () => Promise | void, + readonly redo: () => Promise | void, + metadata?: WorkspaceEditMetadata + ) { + super(metadata); + } +} + +export class OpaqueEdits { + + constructor( + private readonly _undoRedoGroup: UndoRedoGroup, + private readonly _undoRedoSource: UndoRedoSource | undefined, + private readonly _progress: IProgress, + private readonly _token: CancellationToken, + private readonly _edits: ResourceAttachmentEdit[], + @IUndoRedoService private readonly _undoRedoService: IUndoRedoService, + ) { } + + async apply(): Promise { + const resources: URI[] = []; + + for (const edit of this._edits) { + if (this._token.isCancellationRequested) { + break; + } + + await edit.redo(); + + this._undoRedoService.pushElement({ + type: UndoRedoElementType.Resource, + resource: edit.resource, + label: edit.metadata?.label || 'Custom Edit', + code: 'paste', + undo: edit.undo, + redo: edit.redo, + }, this._undoRedoGroup, this._undoRedoSource); + + this._progress.report(undefined); + resources.push(edit.resource); + } + + return resources; + } +} diff --git a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts index b65aa16fb600e..e02c37231d1c1 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.contribution.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.contribution.ts @@ -78,6 +78,12 @@ import { ILanguageModelIgnoredFilesService, LanguageModelIgnoredFilesService } f import { ChatGettingStartedContribution } from './actions/chatGettingStarted.js'; import { Extensions, IConfigurationMigrationRegistry } from '../../../common/configuration.js'; import { ChatEditorOverlayController } from './chatEditorOverlay.js'; +import { ITextModelContentProvider, ITextModelService } from '../../../../editor/common/services/resolverService.js'; +import { URI } from '../../../../base/common/uri.js'; +import { ILanguageService } from '../../../../editor/common/languages/language.js'; +import { ITextModel } from '../../../../editor/common/model.js'; +import { IModelService } from '../../../../editor/common/services/model.js'; +import { ChatInputPart } from './chatInputPart.js'; // Register configuration const configurationRegistry = Registry.as(ConfigurationExtensions.Configuration); @@ -182,6 +188,9 @@ class ChatResolverContribution extends Disposable { constructor( @IEditorResolverService editorResolverService: IEditorResolverService, @IInstantiationService instantiationService: IInstantiationService, + @ITextModelService private readonly textModelService: ITextModelService, + @IModelService private readonly modelService: IModelService, + @ILanguageService private readonly languageService: ILanguageService ) { super(); @@ -202,12 +211,32 @@ class ChatResolverContribution extends Disposable { } } )); + + this._register(new ChatInputBoxContentProvider(this.textModelService, this.modelService, this.languageService)); } } AccessibleViewRegistry.register(new ChatResponseAccessibleView()); AccessibleViewRegistry.register(new PanelChatAccessibilityHelp()); AccessibleViewRegistry.register(new QuickChatAccessibilityHelp()); +class ChatInputBoxContentProvider extends Disposable implements ITextModelContentProvider { + constructor( + textModelService: ITextModelService, + private readonly modelService: IModelService, + private readonly languageService: ILanguageService, + ) { + super(); + this._register(textModelService.registerTextModelContentProvider(ChatInputPart.INPUT_SCHEME, this)); + } + + async provideTextContent(resource: URI): Promise { + const existing = this.modelService.getModel(resource); + if (existing) { + return existing; + } + return this.modelService.createModel('', this.languageService.createById('chatinput'), resource); + } +} class ChatSlashStaticSlashCommandsContribution extends Disposable { diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts index 9e1d1b3cd118c..ac56dc179a2ca 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import * as dom from '../../../../../base/browser/dom.js'; +import { IManagedHoverTooltipMarkdownString } from '../../../../../base/browser/ui/hover/hover.js'; import { createInstantHoverDelegate } from '../../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { Emitter } from '../../../../../base/common/event.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; @@ -19,8 +20,8 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { IOpenerService, OpenInternalOptions } from '../../../../../platform/opener/common/opener.js'; import { FolderThemeIcon, IThemeService } from '../../../../../platform/theme/common/themeService.js'; import { ResourceLabels } from '../../../../browser/labels.js'; +import { IChatRequestVariableEntry, isPasteVariableEntry } from '../../common/chatModel.js'; import { revealInsideBarCommand } from '../../../files/browser/fileActions.contribution.js'; -import { IChatRequestVariableEntry } from '../../common/chatModel.js'; import { ChatResponseReferencePartStatusKind, IChatContentReference } from '../../common/chatService.js'; export class ChatAttachmentsContentPart extends Disposable { @@ -126,6 +127,25 @@ export class ChatAttachmentsContentPart extends Disposable { if (!this.attachedContextDisposables.isDisposed) { this.attachedContextDisposables.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverElement)); } + } else if (isPasteVariableEntry(attachment)) { + ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name); + + const hoverContent: IManagedHoverTooltipMarkdownString = { + markdown: { + value: `\`\`\`${attachment.language}\n${attachment.code}\n\`\`\``, + }, + markdownNotSupportedFallback: attachment.code, + }; + + const classNames = ['file-icon', `${attachment.language}-lang-file-icon`]; + label.setLabel(attachment.fileName, undefined, { extraClasses: classNames }); + widget.appendChild(dom.$('span.attachment-additional-info', {}, `Pasted ${attachment.pastedLines}`)); + + widget.style.position = 'relative'; + + if (!this.attachedContextDisposables.isDisposed) { + this.attachedContextDisposables.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverContent, { trapFocus: true })); + } } else { const attachmentLabel = attachment.fullName ?? attachment.name; const withIcon = attachment.icon?.id ? `$(${attachment.icon.id}) ${attachmentLabel}` : attachmentLabel; diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index b9c7505cb4139..244e14b9c855e 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -11,6 +11,7 @@ import { StandardKeyboardEvent } from '../../../../base/browser/keyboardEvent.js import { StandardMouseEvent } from '../../../../base/browser/mouseEvent.js'; import * as aria from '../../../../base/browser/ui/aria/aria.js'; import { Button } from '../../../../base/browser/ui/button/button.js'; +import { IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/hover/hover.js'; import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { createInstantHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; @@ -75,7 +76,7 @@ import { revealInsideBarCommand } from '../../files/browser/fileActions.contribu import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; import { ChatEditingSessionState, IChatEditingService, IChatEditingSession, WorkingSetEntryState } from '../common/chatEditingService.js'; -import { IChatRequestVariableEntry } from '../common/chatModel.js'; +import { IChatRequestVariableEntry, isPasteVariableEntry } from '../common/chatModel.js'; import { ChatRequestDynamicVariablePart } from '../common/chatParserTypes.js'; import { IChatFollowup } from '../common/chatService.js'; import { IChatResponseViewModel } from '../common/chatViewModel.js'; @@ -829,6 +830,23 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge store.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverElement, { trapFocus: false })); resolve(); })); + } else if (isPasteVariableEntry(attachment)) { + ariaLabel = localize('chat.attachment', "Attached context, {0}", attachment.name); + + const hoverContent: IManagedHoverTooltipMarkdownString = { + markdown: { + value: `\`\`\`${attachment.language}\n${attachment.code}\n\`\`\``, + }, + markdownNotSupportedFallback: attachment.code, + }; + + const classNames = ['file-icon', `${attachment.language}-lang-file-icon`]; + label.setLabel(attachment.fileName, undefined, { extraClasses: classNames }); + widget.appendChild(dom.$('span.attachment-additional-info', {}, `Pasted ${attachment.pastedLines}`)); + + widget.style.position = 'relative'; + store.add(this.hoverService.setupManagedHover(hoverDelegate, widget, hoverContent, { trapFocus: true })); + this.attachButtonAndDisposables(widget, index, attachment, hoverDelegate); } else { const attachmentLabel = attachment.fullName ?? attachment.name; const withIcon = attachment.icon?.id ? `$(${attachment.icon.id}) ${attachmentLabel}` : attachmentLabel; diff --git a/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts b/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts index 15befebd0e027..f680f958711b7 100644 --- a/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts +++ b/src/vs/workbench/contrib/chat/browser/chatPasteProviders.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { CancellationToken } from '../../../../base/common/cancellation.js'; -import { IDataTransferItem, IReadonlyVSDataTransfer } from '../../../../base/common/dataTransfer.js'; +import { createStringDataTransferItem, IDataTransferItem, IReadonlyVSDataTransfer, VSDataTransfer } from '../../../../base/common/dataTransfer.js'; import { HierarchicalKind } from '../../../../base/common/hierarchicalKind.js'; import { IRange } from '../../../../editor/common/core/range.js'; import { DocumentPasteContext, DocumentPasteEditProvider, DocumentPasteEditsSession } from '../../../../editor/common/languages.js'; @@ -17,18 +17,22 @@ import { Codicon } from '../../../../base/common/codicons.js'; import { localize } from '../../../../nls.js'; import { IChatRequestVariableEntry } from '../common/chatModel.js'; import { IExtensionService, isProposedApiEnabled } from '../../../services/extensions/common/extensions.js'; +import { Mimes } from '../../../../base/common/mime.js'; +import { URI } from '../../../../base/common/uri.js'; -export class PasteImageProvider implements DocumentPasteEditProvider { +const COPY_MIME_TYPES = 'application/vnd.code.additional-editor-data'; - public readonly kind = new HierarchicalKind('image'); +export class PasteImageProvider implements DocumentPasteEditProvider { + public readonly kind = new HierarchicalKind('chat.attach.image'); public readonly pasteMimeTypes = ['image/*']; + constructor( private readonly chatWidgetService: IChatWidgetService, - private readonly extensionService: IExtensionService + private readonly extensionService: IExtensionService, ) { } - async provideDocumentPasteEdits(_model: ITextModel, _ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise { + async provideDocumentPasteEdits(model: ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise { if (!this.extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData'))) { return; } @@ -62,7 +66,7 @@ export class PasteImageProvider implements DocumentPasteEditProvider { return; } - const widget = this.chatWidgetService.getWidgetByInputUri(_model.uri); + let widget = this.chatWidgetService.getWidgetByInputUri(model.uri); if (!widget) { return; } @@ -87,9 +91,35 @@ export class PasteImageProvider implements DocumentPasteEditProvider { return; } - widget.attachmentModel.addContext(imageContext); + const customEdit = { + resource: model.uri, + variable: imageContext, + undo: () => { + widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget) { + throw new Error('No widget found for undo'); + } + widget.attachmentModel.delete(imageContext.id); + }, + redo: () => { + widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget) { + throw new Error('No widget found for redo'); + } + widget.attachmentModel.addContext(imageContext); + }, + metadata: { needsConfirmation: false, label: imageContext.name } + }; - return; + return { + edits: [{ + insertText: '', title: 'Empty Edit', kind: new HierarchicalKind('chat.attach.image'), handledMimeType: mimeType, + additionalEdit: { + edits: [customEdit], + } + }], + dispose() { }, + }; } } @@ -106,7 +136,6 @@ async function getImageAttachContext(data: Uint8Array, mimeType: string, token: isImage: true, icon: Codicon.fileMedia, isDynamic: true, - isFile: false, mimeType }; } @@ -136,6 +165,119 @@ export function isImage(array: Uint8Array): boolean { ); } +export class CopyTextProvider implements DocumentPasteEditProvider { + + public readonly kind = new HierarchicalKind('chat.attach.text'); + public readonly copyMimeTypes = [COPY_MIME_TYPES]; + + async prepareDocumentPaste(model: ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, token: CancellationToken): Promise { + if (model.uri.scheme === ChatInputPart.INPUT_SCHEME) { + return; + } + const customDataTransfer = new VSDataTransfer(); + const rangesString = JSON.stringify({ ranges: ranges[0], uri: model.uri.toString() }); + customDataTransfer.append(COPY_MIME_TYPES, createStringDataTransferItem(rangesString)); + return customDataTransfer; + } +} + +export class PasteTextProvider implements DocumentPasteEditProvider { + + public readonly kind = new HierarchicalKind('chat.attach.text'); + public readonly pasteMimeTypes = [COPY_MIME_TYPES]; + + constructor( + private readonly chatWidgetService: IChatWidgetService + ) { } + + async provideDocumentPasteEdits(model: ITextModel, ranges: readonly IRange[], dataTransfer: IReadonlyVSDataTransfer, context: DocumentPasteContext, token: CancellationToken): Promise { + if (model.uri.scheme !== ChatInputPart.INPUT_SCHEME) { + return; + } + const text = dataTransfer.get(Mimes.text); + const editorData = dataTransfer.get('vscode-editor-data'); + const additionalEditorData = dataTransfer.get(COPY_MIME_TYPES); + + if (!editorData || !text || !additionalEditorData) { + return; + } + + const textdata = await text.asString(); + const metadata = JSON.parse(await editorData.asString()); + const additionalData = JSON.parse(await additionalEditorData.asString()); + + let widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget) { + return; + } + + const copiedContext = await getCopiedContext(textdata, additionalData.uri, metadata.mode, additionalData.ranges); + + if (token.isCancellationRequested || !copiedContext) { + return; + } + + const currentContextIds = widget.attachmentModel.getAttachmentIDs(); + if (currentContextIds.has(copiedContext.id)) { + return; + } + + const customEdit = { + resource: model.uri, + variable: copiedContext, + undo: () => { + widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget) { + throw new Error('No widget found for undo'); + } + widget.attachmentModel.delete(copiedContext.id); + }, + redo: () => { + widget = this.chatWidgetService.getWidgetByInputUri(model.uri); + if (!widget) { + throw new Error('No widget found for redo'); + } + widget.attachmentModel.addContext(copiedContext); + }, + metadata: { needsConfirmation: false, label: copiedContext.name } + }; + + return { + edits: [{ + insertText: '', title: 'Empty Edit', kind: new HierarchicalKind('chat.attach.text'), handledMimeType: Mimes.text, + additionalEdit: { + edits: [customEdit], + } + }], + dispose() { }, + }; + } +} + +async function getCopiedContext(code: string, file: string, language: string, ranges: IRange): Promise { + const fileName = file.split('/').pop() || 'unknown file'; + const start = ranges.startLineNumber; + const end = ranges.endLineNumber; + const resultText = `Copied Selection of Code: \n\n\n From the file: ${fileName} From lines ${start} to ${end} \n \`\`\`${code}\`\`\``; + const pastedLines = start === end ? localize('pastedAttachment.oneLine', '1 line') : localize('pastedAttachment.multipleLines', '{0} lines', end + 1 - start); + return { + kind: 'paste', + value: resultText, + id: `${fileName}${start}${end}${ranges.startColumn}${ranges.endColumn}`, + name: `${fileName} ${pastedLines}`, + icon: Codicon.code, + isDynamic: true, + pastedLines, + language, + fileName, + code, + references: [{ + reference: URI.parse(file), + kind: 'reference' + }] + }; +} + export class ChatPasteProvidersFeature extends Disposable { constructor( @ILanguageFeaturesService languageFeaturesService: ILanguageFeaturesService, @@ -144,5 +286,7 @@ export class ChatPasteProvidersFeature extends Disposable { ) { super(); this._register(languageFeaturesService.documentPasteEditProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, pattern: '*', hasAccessToAllModels: true }, new PasteImageProvider(chatWidgetService, extensionService))); + this._register(languageFeaturesService.documentPasteEditProvider.register({ scheme: ChatInputPart.INPUT_SCHEME, pattern: '*', hasAccessToAllModels: true }, new PasteTextProvider(chatWidgetService))); + this._register(languageFeaturesService.documentPasteEditProvider.register('*', new CopyTextProvider())); } } diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 120c42a6e81cf..af851b641ea21 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -1292,6 +1292,11 @@ have to be updated for changes to the rules above, or to support more deeply nes border: none; } +.chat-attached-context-attachment .attachment-additional-info { + opacity: 0.7; + font-size: .9em; +} + .chat-attached-context-attachment .chat-attached-context-pill-image { width: 14px; height: 14px; diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index e87ec0d7fba94..9bf14149fe938 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -54,12 +54,24 @@ export interface IChatRequestImplicitVariableEntry extends Omit { + readonly kind: 'paste'; + code: string; + language: string; + fileName: string; + pastedLines: string; +} + +export type IChatRequestVariableEntry = IChatRequestImplicitVariableEntry | IChatRequestPasteVariableEntry | IBaseChatRequestVariableEntry; export function isImplicitVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestImplicitVariableEntry { return obj.kind === 'implicit'; } +export function isPasteVariableEntry(obj: IChatRequestVariableEntry): obj is IChatRequestPasteVariableEntry { + return obj.kind === 'paste'; +} + export function isChatRequestVariableEntry(obj: unknown): obj is IChatRequestVariableEntry { const entry = obj as IChatRequestVariableEntry; return typeof entry === 'object' &&