diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index f6657f85254..de0ad326f51 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -48,7 +48,7 @@ import { ISymbolQuickPickItem, SymbolsQuickAccessProvider } from '../../../searc import { SearchContext } from '../../../search/common/constants.js'; import { ChatAgentLocation, IChatAgentService } from '../../common/chatAgents.js'; import { ChatContextKeys } from '../../common/chatContextKeys.js'; -import { IChatEditingService, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { IChatEditingService } from '../../common/chatEditingService.js'; import { IChatRequestVariableEntry } from '../../common/chatModel.js'; import { ChatRequestAgentPart } from '../../common/chatParserTypes.js'; import { IChatVariablesService } from '../../common/chatVariables.js'; @@ -445,13 +445,13 @@ export class AttachContextAction extends Action2 { category: CHAT_CATEGORY, precondition: ContextKeyExpr.or(AttachContextAction._cdt, ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession))), keybinding: { - when: ChatContextKeys.inChatInput, + when: ContextKeyExpr.and(ChatContextKeys.location.notEqualsTo(ChatAgentLocation.EditingSession), ChatContextKeys.inChatInput), primary: KeyMod.CtrlCmd | KeyCode.Slash, weight: KeybindingWeight.EditorContrib }, menu: [ { - when: ContextKeyExpr.or(ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)), ContextKeyExpr.and(ContextKeyExpr.or(ChatContextKeys.location.isEqualTo(ChatAgentLocation.Panel), ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)), AttachContextAction._cdt)), + when: AttachContextAction._cdt, id: MenuId.ChatInput, group: 'navigation', order: 2 @@ -515,26 +515,21 @@ export class AttachContextAction extends Action2 { }); } } else { - // file attachment - if (chatEditingService) { - getEditingSession(chatEditingService, widget)?.addFileToWorkingSet(pick.resource); - } else { - let isOmitted = false; - try { - const createdModel = await textModelService.createModelReference(pick.resource); - createdModel.dispose(); - } catch { - isOmitted = true; - } - - toAttach.push({ - id: this._getFileContextId({ resource: pick.resource }), - value: pick.resource, - name: pick.label, - isFile: true, - isOmitted - }); + let isOmitted = false; + try { + const createdModel = await textModelService.createModelReference(pick.resource); + createdModel.dispose(); + } catch { + isOmitted = true; } + + toAttach.push({ + id: this._getFileContextId({ resource: pick.resource }), + value: pick.resource, + name: pick.label, + isFile: true, + isOmitted + }); } } else if (isIGotoSymbolQuickPickItem(pick) && pick.uri && pick.range) { toAttach.push({ @@ -548,31 +543,23 @@ export class AttachContextAction extends Action2 { for (const editor of editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput || e instanceof NotebookEditorInput)) { const uri = editor instanceof DiffEditorInput ? editor.modified.resource : editor.resource; if (uri) { - if (chatEditingService) { - getEditingSession(chatEditingService, widget)?.addFileToWorkingSet(uri); - } else { - toAttach.push({ - id: this._getFileContextId({ resource: uri }), - value: uri, - name: labelService.getUriBasenameLabel(uri), - isFile: true, - }); - } + toAttach.push({ + id: this._getFileContextId({ resource: uri }), + value: uri, + name: labelService.getUriBasenameLabel(uri), + isFile: true, + }); } } } else if (isISearchResultsQuickPickItem(pick)) { const searchView = viewsService.getViewWithId(SEARCH_VIEW_ID) as SearchView; for (const result of searchView.model.searchResult.matches()) { - if (chatEditingService) { - getEditingSession(chatEditingService, widget)?.addFileToWorkingSet(result.resource); - } else { - toAttach.push({ - id: this._getFileContextId({ resource: result.resource }), - value: result.resource, - name: labelService.getUriBasenameLabel(result.resource), - isFile: true, - }); - } + toAttach.push({ + id: this._getFileContextId({ resource: result.resource }), + value: result.resource, + name: labelService.getUriBasenameLabel(result.resource), + isFile: true, + }); } } else if (isRelatedFileQuickPickItem(pick)) { // Get all provider results and show them in a second tier picker @@ -580,12 +567,12 @@ export class AttachContextAction extends Action2 { if (!chatSessionId || !chatEditingService) { continue; } - const relatedFiles = await chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), CancellationToken.None); + const relatedFiles = await chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None); if (!relatedFiles) { continue; } const attachments = widget.attachmentModel.getAttachmentIDs(); - const itemsPromise = chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), CancellationToken.None) + const itemsPromise = chatEditingService.getRelatedFiles(chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None) .then((files) => (files ?? []).reduce<(WithUriValue | IQuickPickSeparator)[]>((acc, cur) => { acc.push({ type: 'separator', label: cur.group }); for (const file of cur.files) { @@ -685,103 +672,103 @@ export class AttachContextAction extends Action2 { const chatEditingService = widget.location === ChatAgentLocation.EditingSession ? accessor.get(IChatEditingService) : undefined; const quickPickItems: IAttachmentQuickPickItem[] = []; - if (!context || !context.showFilesOnly) { - if (extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData'))) { - const imageData = await clipboardService.readImage(); - if (isImage(imageData)) { - quickPickItems.push({ - kind: 'image', - id: await imageToHash(imageData), - label: localize('imageFromClipboard', 'Image from Clipboard'), - iconClass: ThemeIcon.asClassName(Codicon.fileMedia), - }); - } - + if (extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData'))) { + const imageData = await clipboardService.readImage(); + if (isImage(imageData)) { quickPickItems.push({ - kind: 'screenshot', - id: ScreenshotVariableId, - icon: ThemeIcon.fromId(Codicon.deviceCamera.id), - iconClass: ThemeIcon.asClassName(Codicon.deviceCamera), - label: (isElectron - ? localize('chatContext.attachScreenshot.labelElectron.Window', 'Screenshot Window') - : localize('chatContext.attachScreenshot.labelWeb', 'Screenshot')), + kind: 'image', + id: await imageToHash(imageData), + label: localize('imageFromClipboard', 'Image from Clipboard'), + iconClass: ThemeIcon.asClassName(Codicon.fileMedia), }); } - if (widget.viewModel?.sessionId) { - const agentPart = widget.parsedInput.parts.find((part): part is ChatRequestAgentPart => part instanceof ChatRequestAgentPart); - if (agentPart) { - const completions = await chatAgentService.getAgentCompletionItems(agentPart.agent.id, '', CancellationToken.None); - for (const variable of completions) { - if (variable.fullName && variable.command) { - quickPickItems.push({ - kind: 'command', - label: variable.fullName, - id: variable.id, - command: variable.command, - icon: variable.icon, - iconClass: variable.icon ? ThemeIcon.asClassName(variable.icon) : undefined, - value: variable.value, - name: variable.name - }); - } else { - // Currently there's nothing that falls into this category - } + quickPickItems.push({ + kind: 'screenshot', + id: ScreenshotVariableId, + icon: ThemeIcon.fromId(Codicon.deviceCamera.id), + iconClass: ThemeIcon.asClassName(Codicon.deviceCamera), + label: (isElectron + ? localize('chatContext.attachScreenshot.labelElectron.Window', 'Screenshot Window') + : localize('chatContext.attachScreenshot.labelWeb', 'Screenshot')), + }); + } + + if (widget.viewModel?.sessionId) { + const agentPart = widget.parsedInput.parts.find((part): part is ChatRequestAgentPart => part instanceof ChatRequestAgentPart); + if (agentPart) { + const completions = await chatAgentService.getAgentCompletionItems(agentPart.agent.id, '', CancellationToken.None); + for (const variable of completions) { + if (variable.fullName && variable.command) { + quickPickItems.push({ + kind: 'command', + label: variable.fullName, + id: variable.id, + command: variable.command, + icon: variable.icon, + iconClass: variable.icon ? ThemeIcon.asClassName(variable.icon) : undefined, + value: variable.value, + name: variable.name + }); + } else { + // Currently there's nothing that falls into this category } } } + } - for (const tool of languageModelToolsService.getTools()) { - if (tool.canBeReferencedInPrompt) { - const item: IToolQuickPickItem = { - kind: 'tool', - label: tool.displayName ?? '', - id: tool.id, - icon: ThemeIcon.isThemeIcon(tool.icon) ? tool.icon : undefined // TODO need to support icon path? - }; - if (ThemeIcon.isThemeIcon(tool.icon)) { - item.iconClass = ThemeIcon.asClassName(tool.icon); - } else if (tool.icon) { - item.iconPath = tool.icon; - } - - quickPickItems.push(item); + for (const tool of languageModelToolsService.getTools()) { + if (tool.canBeReferencedInPrompt) { + const item: IToolQuickPickItem = { + kind: 'tool', + label: tool.displayName ?? '', + id: tool.id, + icon: ThemeIcon.isThemeIcon(tool.icon) ? tool.icon : undefined // TODO need to support icon path? + }; + if (ThemeIcon.isThemeIcon(tool.icon)) { + item.iconClass = ThemeIcon.asClassName(tool.icon); + } else if (tool.icon) { + item.iconPath = tool.icon; } + + quickPickItems.push(item); } + } + quickPickItems.push({ + kind: 'quickaccess', + label: localize('chatContext.symbol', 'Symbol...'), + iconClass: ThemeIcon.asClassName(Codicon.symbolField), + prefix: SymbolsQuickAccessProvider.PREFIX, + id: 'symbol' + }); + + quickPickItems.push({ + kind: 'folder', + label: localize('chatContext.folder', 'Folder...'), + iconClass: ThemeIcon.asClassName(Codicon.folder), + id: 'folder', + }); + + if (widget.location === ChatAgentLocation.Notebook) { quickPickItems.push({ - kind: 'quickaccess', - label: localize('chatContext.symbol', 'Symbol...'), - iconClass: ThemeIcon.asClassName(Codicon.symbolField), - prefix: SymbolsQuickAccessProvider.PREFIX, - id: 'symbol' + kind: 'command', + id: 'chatContext.notebook.kernelVariable', + icon: ThemeIcon.fromId(Codicon.serverEnvironment.id), + iconClass: ThemeIcon.asClassName(Codicon.serverEnvironment), + value: 'kernelVariable', + label: localize('chatContext.notebook.kernelVariable', 'Kernel Variable...'), + command: { + id: 'notebook.chat.selectAndInsertKernelVariable', + title: localize('chatContext.notebook.selectkernelVariable', 'Select and Insert Kernel Variable'), + arguments: [{ widget, range: undefined }] + } }); + } - quickPickItems.push({ - kind: 'folder', - label: localize('chatContext.folder', 'Folder...'), - iconClass: ThemeIcon.asClassName(Codicon.folder), - id: 'folder', - }); - - if (widget.location === ChatAgentLocation.Notebook) { - quickPickItems.push({ - kind: 'command', - id: 'chatContext.notebook.kernelVariable', - icon: ThemeIcon.fromId(Codicon.serverEnvironment.id), - iconClass: ThemeIcon.asClassName(Codicon.serverEnvironment), - value: 'kernelVariable', - label: localize('chatContext.notebook.kernelVariable', 'Kernel Variable...'), - command: { - id: 'notebook.chat.selectAndInsertKernelVariable', - title: localize('chatContext.notebook.selectkernelVariable', 'Select and Insert Kernel Variable'), - arguments: [{ widget, range: undefined }] - } - }); - } - } else if (context.showFilesOnly) { + if (context?.showFilesOnly) { if (chatEditingService?.hasRelatedFilesProviders() && (widget.getInput() || (getEditingSession(chatEditingService, widget)?.workingSet.size))) { - quickPickItems.push({ + quickPickItems.unshift({ kind: 'related-files', id: 'related-files', label: localize('chatContext.relatedFiles', 'Related Files'), @@ -789,7 +776,7 @@ export class AttachContextAction extends Action2 { }); } if (editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput).length > 0) { - quickPickItems.push({ + quickPickItems.unshift({ kind: 'open-editors', id: 'open-editors', label: localize('chatContext.editors', 'Open Editors'), @@ -797,7 +784,7 @@ export class AttachContextAction extends Action2 { }); } if (SearchContext.HasSearchResults.getValue(contextKeyService)) { - quickPickItems.push({ + quickPickItems.unshift({ kind: 'search-results', id: 'search-results', label: localize('chatContext.searchResults', 'Search Results'), @@ -827,6 +814,9 @@ export class AttachContextAction extends Action2 { this._show(quickInputService, commandService, widget, quickChatService, quickPickItems.sort(function (a, b) { + if (a.kind === 'open-editors') { return -1; } + if (b.kind === 'open-editors') { return 1; } + const first = extractTextFromIconLabel(a.label).toUpperCase(); const second = extractTextFromIconLabel(b.label).toUpperCase(); @@ -862,13 +852,6 @@ export class AttachContextAction extends Action2 { filter: (item: IChatContextQuickPickItem | IQuickPickSeparator) => { // Avoid attaching the same context twice const attachedContext = widget.attachmentModel.getAttachmentIDs(); - if (chatEditingService) { - for (const [file, state] of getEditingSession(chatEditingService, widget)?.workingSet.entries() ?? []) { - if (state.state !== WorkingSetEntryState.Suggested) { - attachedContext.add(this._getFileContextId({ resource: file })); - } - } - } if (isIOpenEditorsQuickPickItem(item)) { for (const editor of editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput)) { @@ -935,16 +918,21 @@ registerAction2(class AttachFilesAction extends AttachContextAction { constructor() { super({ id: 'workbench.action.chat.editing.attachFiles', - title: localize2('workbench.action.chat.editing.attachFiles.label', "Add Files to Working Set"), + title: localize2('workbench.action.chat.editing.attachFiles.label', "Add Files to Copilot Edits"), f1: false, category: CHAT_CATEGORY, - precondition: ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession) + precondition: ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession), + keybinding: { + when: ContextKeyExpr.and(ChatContextKeys.inChatInput, ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditingSession)), + primary: KeyMod.CtrlCmd | KeyCode.Slash, + weight: KeybindingWeight.EditorContrib + } }); } override async run(accessor: ServicesAccessor, ...args: any[]): Promise { const context = args[0]; - const attachFilesContext = { ...context, showFilesOnly: true, placeholder: localize('chatAttachFiles', 'Search for files to add to your working set') }; + const attachFilesContext = { ...context, showFilesOnly: true }; return super.run(accessor, attachFilesContext); } }); diff --git a/src/vs/workbench/contrib/chat/browser/chat.ts b/src/vs/workbench/contrib/chat/browser/chat.ts index b127232b593..982143743ce 100644 --- a/src/vs/workbench/contrib/chat/browser/chat.ts +++ b/src/vs/workbench/contrib/chat/browser/chat.ts @@ -190,7 +190,6 @@ export interface IChatWidgetViewOptions { }; defaultElementHeight?: number; editorOverflowWidgetsDomNode?: HTMLElement; - enableImplicitContext?: boolean; enableWorkingSet?: 'explicit' | 'implicit'; } diff --git a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts index b5f9c250ac2..9278fe4aed0 100644 --- a/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts +++ b/src/vs/workbench/contrib/chat/browser/chatAttachmentModel.ts @@ -42,6 +42,15 @@ export class ChatAttachmentModel extends Disposable { return this._attachments.size; } + get fileAttachments(): URI[] { + return this.attachments.reduce((acc, file) => { + if (file.isFile && URI.isUri(file.value)) { + acc.push(file.value); + } + return acc; + }, []); + } + getAttachmentIDs() { return new Set(this._attachments.keys()); } @@ -92,48 +101,3 @@ export class ChatAttachmentModel extends Disposable { this.addContext(...attachments); } } - -export class EditsAttachmentModel extends ChatAttachmentModel { - - private _onFileLimitExceeded = this._register(new Emitter()); - readonly onFileLimitExceeded = this._onFileLimitExceeded.event; - - get fileAttachments() { - return this.attachments.filter(attachment => attachment.isFile); - } - - private readonly _excludedFileAttachments: IChatRequestVariableEntry[] = []; - get excludedFileAttachments(): IChatRequestVariableEntry[] { - return this._excludedFileAttachments; - } - - constructor( - @IInstantiationService _initService: IInstantiationService, - ) { - super(_initService); - } - - override addContext(...attachments: IChatRequestVariableEntry[]) { - const currentAttachmentIds = this.getAttachmentIDs(); - const fileAttachments = attachments.filter(attachment => attachment.isFile); - const otherAttachments = attachments.filter(attachment => !attachment.isFile); - - // deduplicate file attachments - const newFileAttachments = []; - const newFileAttachmentIds = new Set(); - for (const attachment of fileAttachments) { - if (newFileAttachmentIds.has(attachment.id) || currentAttachmentIds.has(attachment.id)) { - continue; - } - newFileAttachmentIds.add(attachment.id); - newFileAttachments.push(attachment); - } - - super.addContext(...otherAttachments, ...newFileAttachments); - } - - override clear(): void { - this._excludedFileAttachments.splice(0, this._excludedFileAttachments.length); - super.clear(); - } -} diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts index 0b89c05c70b..b62621ea5bb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatAttachmentsContentPart.ts @@ -56,7 +56,6 @@ export class ChatAttachmentsContentPart extends Disposable { constructor( private readonly variables: IChatRequestVariableEntry[], private readonly contentReferences: ReadonlyArray = [], - private readonly workingSet: ReadonlyArray = [], public readonly domNode: HTMLElement | undefined = dom.$('.chat-attached-context'), @IContextKeyService private readonly contextKeyService: IContextKeyService, @IInstantiationService private readonly instantiationService: IInstantiationService, @@ -84,10 +83,6 @@ export class ChatAttachmentsContentPart extends Disposable { this.variables.forEach(async (attachment) => { let resource = URI.isUri(attachment.value) ? attachment.value : attachment.value && typeof attachment.value === 'object' && 'uri' in attachment.value && URI.isUri(attachment.value.uri) ? attachment.value.uri : undefined; let range = attachment.value && typeof attachment.value === 'object' && 'range' in attachment.value && Range.isIRange(attachment.value.range) ? attachment.value.range : undefined; - if (resource && attachment.isFile && this.workingSet.find(entry => entry.toString() === resource?.toString())) { - // Don't render attachment if it's in the working set - return; - } const widget = dom.append(container, dom.$('.chat-attached-context-attachment.show-file-icons')); const label = this._contextResourceLabels.create(widget, { supportIcons: true, hoverDelegate, hoverTargetOverride: widget }); diff --git a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts index c73c3883381..427e7b2fca6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatContentParts/chatReferencesContentPart.ts @@ -435,25 +435,15 @@ class CollapsibleListRenderer implements IListRenderer (attachment.value as URI).toString()); + const fileAttachments = chatWidget.attachmentModel ? chatWidget.attachmentModel.fileAttachments : []; + const attachmentIdsToRemove = fileAttachments.map(attachment => attachment.toString()); chatWidget.attachmentModel.delete(...attachmentIdsToRemove); } } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts index 24af03c47e8..6f31c908335 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingServiceImpl.ts @@ -369,24 +369,11 @@ export class ChatEditingService extends Disposable implements IChatEditingServic }); } - async getRelatedFiles(chatSessionId: string, prompt: string, token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined> { - const currentSession = this.getEditingSession(chatSessionId); - if (!currentSession) { - return undefined; - } - const userAddedWorkingSetEntries: URI[] = []; - for (const [uri, metadata] of currentSession.workingSet) { - // Don't incorporate suggested files into the related files request - // but do consider transient entries like open editors - if (metadata.state !== WorkingSetEntryState.Suggested) { - userAddedWorkingSetEntries.push(uri); - } - } - + async getRelatedFiles(chatSessionId: string, prompt: string, files: URI[], token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined> { const providers = Array.from(this._chatRelatedFilesProviders.values()); const result = await Promise.all(providers.map(async provider => { try { - const relatedFiles = await provider.provideRelatedFiles({ prompt, files: userAddedWorkingSetEntries }, token); + const relatedFiles = await provider.provideRelatedFiles({ prompt, files }, token); if (relatedFiles?.length) { return { group: provider.description, files: relatedFiles }; } diff --git a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts index 45dbea5f3d4..a8f60cd6404 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditing/chatEditingSession.ts @@ -17,7 +17,6 @@ import { asyncTransaction, autorun, derived, derivedOpts, derivedWithStore, IObs import { autorunDelta, autorunIterableDelta } from '../../../../../base/common/observableInternal/autorun.js'; import { isEqual, joinPath } from '../../../../../base/common/resources.js'; import { URI } from '../../../../../base/common/uri.js'; -import { isCodeEditor, isDiffEditor } from '../../../../../editor/browser/editorBrowser.js'; import { IBulkEditService } from '../../../../../editor/browser/services/bulkEditService.js'; import { IOffsetEdit, ISingleOffsetEdit, OffsetEdit } from '../../../../../editor/common/core/offsetEdit.js'; import { TextEdit } from '../../../../../editor/common/languages.js'; @@ -35,16 +34,15 @@ import { IInstantiationService } from '../../../../../platform/instantiation/com import { ILogService } from '../../../../../platform/log/common/log.js'; import { observableConfigValue } from '../../../../../platform/observable/common/platformObservableUtils.js'; import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js'; -import { IEditorCloseEvent, SaveReason } from '../../../../common/editor.js'; +import { SaveReason } from '../../../../common/editor.js'; import { DiffEditorInput } from '../../../../common/editor/diffEditorInput.js'; import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js'; import { IEditorService } from '../../../../services/editor/common/editorService.js'; import { ITextFileService } from '../../../../services/textfile/common/textfiles.js'; import { MultiDiffEditor } from '../../../multiDiffEditor/browser/multiDiffEditor.js'; import { MultiDiffEditorInput } from '../../../multiDiffEditor/browser/multiDiffEditorInput.js'; -import { isNotebookEditorInput } from '../../../notebook/common/notebookEditorInput.js'; import { INotebookService } from '../../../notebook/common/notebookService.js'; -import { ChatEditingSessionChangeType, ChatEditingSessionState, chatEditingSnapshotScheme, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IEditSessionEntryDiff, IModifiedFileEntry, IStreamingEdits, WorkingSetDisplayMetadata, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { ChatEditingSessionChangeType, ChatEditingSessionState, ChatEditKind, getMultiDiffSourceUri, IChatEditingSession, IEditSessionEntryDiff, IModifiedFileEntry, IStreamingEdits, WorkingSetDisplayMetadata, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatRequestDisablement, IChatResponseModel } from '../../common/chatModel.js'; import { IChatService } from '../../common/chatService.js'; import { AbstractChatEditingModifiedFileEntry, IModifiedEntryTelemetryInfo, ISnapshotEntry } from './chatEditingModifiedFileEntry.js'; @@ -56,10 +54,6 @@ const STORAGE_CONTENTS_FOLDER = 'contents'; const STORAGE_STATE_FILE = 'state.json'; const POST_EDIT_STOP_ID = 'd19944f6-f46c-4e17-911b-79a8e843c7c0'; // randomly generated -const untrackedSchemes: readonly string[] = [ - chatEditingSnapshotScheme, -]; - class ThrottledSequencer extends Sequencer { private _size = 0; @@ -234,12 +228,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio }); } - // Add the currently active editors to the working set - this._trackCurrentEditorsInWorkingSet(); this._triggerSaveParticipantsOnAccept(); - this._register(this._editorService.onDidVisibleEditorsChange(() => { - this._trackCurrentEditorsInWorkingSet(); - })); this._register(autorun(reader => { const entries = this.entries.read(reader); entries.forEach(entry => { @@ -306,61 +295,6 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio )); } - private _trackCurrentEditorsInWorkingSet(e?: IEditorCloseEvent) { - const existingTransientEntries = new ResourceSet(); - for (const file of this._workingSet.keys()) { - if (this._workingSet.get(file)?.state === WorkingSetEntryState.Transient) { - existingTransientEntries.add(file); - } - } - - const activeEditors = new ResourceSet(); - this._editorGroupsService.groups.forEach((group) => { - if (!group.activeEditorPane) { - return; - } - let uri; - if (isNotebookEditorInput(group.activeEditorPane.input)) { - uri = group.activeEditorPane.input.resource; - } else { - let activeEditorControl = group.activeEditorPane.getControl(); - if (isDiffEditor(activeEditorControl)) { - activeEditorControl = activeEditorControl.getOriginalEditor().hasTextFocus() ? activeEditorControl.getOriginalEditor() : activeEditorControl.getModifiedEditor(); - } - if ((isCodeEditor(activeEditorControl)) && activeEditorControl.hasModel()) { - uri = activeEditorControl.getModel().uri; - } - } - if (!uri) { - return; - } - if (untrackedSchemes.includes(uri.scheme)) { - return; - } - if (existingTransientEntries.has(uri)) { - existingTransientEntries.delete(uri); - } else if ((!this._workingSet.has(uri) || this._workingSet.get(uri)?.state === WorkingSetEntryState.Suggested) && !this._removedTransientEntries.has(uri)) { - // Don't add as a transient entry if it's already a confirmed part of the working set - // or if the user has intentionally removed it from the working set - activeEditors.add(uri); - } - }); - - let didChange = false; - for (const entry of existingTransientEntries) { - didChange = this._workingSet.delete(entry) || didChange; - } - - for (const entry of activeEditors) { - this._workingSet.set(entry, { state: WorkingSetEntryState.Transient, description: localize('chatEditing.transient', "Open Editor") }); - didChange = true; - } - - if (didChange) { - this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); - } - } - private _findSnapshot(requestId: string): IChatEditingSessionSnapshot | undefined { return this._linearHistory.get().find(s => s.requestId === requestId); } @@ -610,7 +544,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio const state = this._workingSet.get(uri); if (state !== undefined) { didRemoveUris = this._workingSet.delete(uri) || didRemoveUris; - if (reason === WorkingSetEntryRemovalReason.User && (state.state === WorkingSetEntryState.Transient || state.state === WorkingSetEntryState.Suggested)) { + if (reason === WorkingSetEntryRemovalReason.User && state.state === WorkingSetEntryState.Suggested) { this._removedTransientEntries.add(uri); } } @@ -626,7 +560,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio markIsReadonly(resource: URI, isReadonly?: boolean): void { const entry = this._workingSet.get(resource); if (entry) { - if (entry.state === WorkingSetEntryState.Transient || entry.state === WorkingSetEntryState.Suggested) { + if (entry.state === WorkingSetEntryState.Suggested) { entry.state = WorkingSetEntryState.Attached; } entry.isMarkedReadonly = isReadonly ?? !entry.isMarkedReadonly; @@ -850,7 +784,7 @@ export class ChatEditingSession extends Disposable implements IChatEditingSessio this._workingSet.set(resource, { description, state: WorkingSetEntryState.Suggested }); this._trackUntitledWorkingSetEntry(resource); this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); - } else if (state === undefined || state.state === WorkingSetEntryState.Transient || state.state === WorkingSetEntryState.Suggested) { + } else if (state === undefined || state.state === WorkingSetEntryState.Suggested) { this._workingSet.set(resource, { description, state: WorkingSetEntryState.Attached }); this._trackUntitledWorkingSetEntry(resource); this._onDidChange.fire(ChatEditingSessionChangeType.WorkingSet); diff --git a/src/vs/workbench/contrib/chat/browser/chatEditor.ts b/src/vs/workbench/contrib/chat/browser/chatEditor.ts index 46c63ff9344..7b89c3d65a6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatEditor.ts +++ b/src/vs/workbench/contrib/chat/browser/chatEditor.ts @@ -68,7 +68,6 @@ export class ChatEditor extends EditorPane { undefined, { supportsFileReferences: true, - enableImplicitContext: true }, { listForeground: editorForeground, diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index d30dcdf08b5..e8885e46d43 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -15,9 +15,7 @@ import { IManagedHoverTooltipMarkdownString } from '../../../../base/browser/ui/ import { IHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegate.js'; import { createInstantHoverDelegate, getDefaultHoverDelegate } from '../../../../base/browser/ui/hover/hoverDelegateFactory.js'; import { renderLabelWithIcons } from '../../../../base/browser/ui/iconLabel/iconLabels.js'; -import { ProgressBar } from '../../../../base/browser/ui/progressbar/progressbar.js'; import { IAction, Separator, toAction } from '../../../../base/common/actions.js'; -import { coalesce } from '../../../../base/common/arrays.js'; import { Promises } from '../../../../base/common/async.js'; import { Codicon } from '../../../../base/common/codicons.js'; import { Emitter, Event } from '../../../../base/common/event.js'; @@ -35,7 +33,6 @@ import { EditorOptions } from '../../../../editor/common/config/editorOptions.js import { IDimension } from '../../../../editor/common/core/dimension.js'; import { IPosition } from '../../../../editor/common/core/position.js'; import { IRange, Range } from '../../../../editor/common/core/range.js'; -import { isLocation } from '../../../../editor/common/languages.js'; import { ILanguageService } from '../../../../editor/common/languages/language.js'; import { ITextModel } from '../../../../editor/common/model.js'; import { getIconClasses } from '../../../../editor/common/services/getIconClasses.js'; @@ -81,9 +78,8 @@ import { getSimpleCodeEditorWidgetOptions, getSimpleEditorOptions, setupSimpleEd import { revealInSideBarCommand } from '../../files/browser/fileActions.contribution.js'; import { ChatAgentLocation, IChatAgentService } from '../common/chatAgents.js'; import { ChatContextKeys } from '../common/chatContextKeys.js'; -import { ChatEditingSessionState, IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../common/chatEditingService.js'; +import { IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../common/chatEditingService.js'; import { IChatRequestVariableEntry, isPasteVariableEntry } from '../common/chatModel.js'; -import { ChatRequestDynamicVariablePart } from '../common/chatParserTypes.js'; import { IChatFollowup } from '../common/chatService.js'; import { IChatVariablesService } from '../common/chatVariables.js'; import { IChatResponseViewModel } from '../common/chatViewModel.js'; @@ -93,7 +89,7 @@ import { CancelAction, ChatModelPickerActionId, ChatSubmitAction, ChatSubmitSeco import { ImplicitContextAttachmentWidget } from './attachments/implicitContextAttachment.js'; import { InstructionAttachmentsWidget } from './attachments/instructionsAttachment/instructionAttachments.js'; import { IChatWidget } from './chat.js'; -import { ChatAttachmentModel, EditsAttachmentModel } from './chatAttachmentModel.js'; +import { ChatAttachmentModel } from './chatAttachmentModel.js'; import { toChatVariable } from './chatAttachmentModel/chatInstructionAttachmentsModel.js'; import { hookUpResourceAttachmentDragAndContextMenu, hookUpSymbolAttachmentDragAndContextMenu } from './chatContentParts/chatAttachmentsContentPart.js'; import { IDisposableReference } from './chatContentParts/chatCollections.js'; @@ -124,7 +120,6 @@ interface IChatInputPartOptions { telemetrySource?: string; }; editorOverflowWidgetsDomNode?: HTMLElement; - enableImplicitContext?: boolean; renderWorkingSet?: boolean; } @@ -243,6 +238,8 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private attachedContextContainer!: HTMLElement; private readonly attachedContextDisposables = this._register(new MutableDisposable()); + private relatedFilesContainer!: HTMLElement; + private chatEditingSessionWidgetContainer!: HTMLElement; private _inputPartHeight: number = 0; @@ -299,7 +296,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge private readonly _chatEditsActionsDisposables = this._register(new DisposableStore()); private readonly _chatEditsDisposables = this._register(new DisposableStore()); - private _chatEditsProgress: ProgressBar | undefined; private _chatEditsListPool: CollapsibleListPool; private _chatEditList: IDisposableReference> | undefined; get selectedElements(): URI[] { @@ -322,10 +318,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge public get attemptedWorkingSetEntriesCount() { return this._attemptedWorkingSetEntriesCount; } - private _combinedChatEditWorkingSetEntries: IWorkingSetEntry[] = []; - public get chatEditWorkingSetFiles() { - return this._combinedChatEditWorkingSetEntries; - } private readonly getInputState: () => IChatInputState; @@ -365,11 +357,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge ) { super(); + this._attachmentModel = this._register(this.instantiationService.createInstance(ChatAttachmentModel)); if (this.location === ChatAgentLocation.EditingSession) { - this._attachmentModel = this._register(this.instantiationService.createInstance(EditsAttachmentModel)); this.dnd = this._register(this.instantiationService.createInstance(EditsDragAndDrop, this.attachmentModel, styles)); } else { - this._attachmentModel = this._register(this.instantiationService.createInstance(ChatAttachmentModel)); this.dnd = this._register(this.instantiationService.createInstance(ChatDragAndDrop, this.attachmentModel, styles)); } @@ -684,6 +675,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge ]), ]), dom.h('.chat-attached-context@attachedContextContainer'), + dom.h('.chat-related-files@relatedFilesContainer'), dom.h('.interactive-input-followups@followupsContainer'), ]) ]); @@ -693,8 +685,9 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge dom.h('.chat-editing-session@chatEditingSessionWidgetContainer'), dom.h('.interactive-input-and-side-toolbar@inputAndSideToolbar', [ dom.h('.chat-input-container@inputContainer', [ - dom.h('.chat-editor-container@editorContainer'), dom.h('.chat-attached-context@attachedContextContainer'), + dom.h('.chat-related-files@relatedFilesContainer'), + dom.h('.chat-editor-container@editorContainer'), dom.h('.chat-input-toolbars@inputToolbars'), ]), ]), @@ -708,13 +701,12 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge const inputContainer = elements.inputContainer; // The chat editor, attachments, and toolbars const editorContainer = elements.editorContainer; this.attachedContextContainer = elements.attachedContextContainer; + this.relatedFilesContainer = elements.relatedFilesContainer; const toolbarsContainer = elements.inputToolbars; this.chatEditingSessionWidgetContainer = elements.chatEditingSessionWidgetContainer; - this.renderAttachedContext(); - if (this.options.enableImplicitContext) { - this._implicitContext = this._register(new ChatImplicitContext()); - this._register(this._implicitContext.onDidChangeValue(() => this._handleAttachedContextChange())); - } + this.renderAttachedContext(widget); + this._implicitContext = this._register(new ChatImplicitContext()); + this._register(this._implicitContext.onDidChangeValue(() => this._handleAttachedContextChange())); this._register(this._attachmentModel.onDidChangeContext(() => this._handleAttachedContextChange())); this.renderChatEditingSessionState(null, widget); @@ -830,7 +822,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge onDidChangeModel: this._onDidChangeCurrentLanguageModel.event, setModel: (model: ILanguageModelChatMetadataAndIdentifier) => { this.setCurrentLanguageModelByUser(model); - this.renderAttachedContext(); + this.renderAttachedContext(widget); }, getModels: () => this.getModels() }; @@ -907,11 +899,29 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge onDidChangeCursorPosition(); this._register(this.themeService.onDidFileIconThemeChange(() => { - this.renderAttachedContext(); + this.renderAttachedContext(widget); })); } - private async renderAttachedContext() { + private renderAddFilesHint(container: HTMLElement, hoverDelegate: IHoverDelegate, store: DisposableStore, chatWidget: IChatWidget) { + const button = store.add(new Button(container, { + supportIcons: true, + secondary: true, + hoverDelegate + })); + button.element.classList.add('chat-attached-context-attachment', 'chat-add-files'); + button.label = localize('chatAddFiles', '{0} Add Files...', '$(attach)'); + const commandId = 'workbench.action.chat.editing.attachFiles'; + const kb = this.keybindingService.lookupKeybinding(commandId)?.getLabel(); + const title = localize('attachFiles.label', 'Attach files to your request{0}', kb ? ` (${kb})` : ''); + button.setTitle(title); + store.add(button.onDidClick(() => { + this.commandService.executeCommand(commandId, { widget: chatWidget, placeholder: localize('chatAttachFiles', 'Search for files and context to add to your request') }); + })); + dom.append(container, button.element); + } + + private async renderAttachedContext(widget?: IChatWidget) { const container = this.attachedContextContainer; const oldHeight = container.offsetHeight; const store = new DisposableStore(); @@ -919,11 +929,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge dom.clearNode(container); const hoverDelegate = store.add(createInstantHoverDelegate()); - const attachments = this.location === ChatAgentLocation.EditingSession - // Render as attachments anything that isn't a file, but still render specific ranges in a file - ? [...this.attachmentModel.attachments.entries()].filter(([_, attachment]) => !attachment.isFile || attachment.isFile && typeof attachment.value === 'object' && !!attachment.value && 'range' in attachment.value) - : [...this.attachmentModel.attachments.entries()]; - dom.setVisibility(Boolean(attachments.length) || Boolean(this.implicitContext?.value) || !this.instructionAttachmentsPart.empty, this.attachedContextContainer); + if (widget && this.options.renderWorkingSet) { + this.renderAddFilesHint(container, hoverDelegate, store, widget); + } + const attachments = [...this.attachmentModel.attachments.entries()]; + dom.setVisibility(Boolean(widget && this.options.renderWorkingSet) || Boolean(attachments.length) || Boolean(this.implicitContext?.value) || !this.instructionAttachmentsPart.empty, this.attachedContextContainer); if (!attachments.length) { this._indexOfLastAttachedContextDeletedWithKeyboard = -1; } @@ -1193,22 +1203,11 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge async renderChatEditingSessionState(chatEditingSession: IChatEditingSession | null, chatWidget?: IChatWidget) { dom.setVisibility(Boolean(chatEditingSession), this.chatEditingSessionWidgetContainer); - if (!chatEditingSession || !this.options.renderWorkingSet) { - dom.clearNode(this.chatEditingSessionWidgetContainer); - this._chatEditsDisposables.clear(); - this._chatEditList = undefined; - this._combinedChatEditWorkingSetEntries = []; - this._chatEditsProgress?.dispose(); - return; + await this.renderAttachedContext(chatWidget); + if (chatEditingSession) { + this.renderChatRelatedFiles(chatEditingSession, this.relatedFilesContainer); } - const currentChatEditingState = chatEditingSession.state.get(); - if (this._chatEditList && !chatWidget?.viewModel?.requestInProgress && (currentChatEditingState === ChatEditingSessionState.Idle || currentChatEditingState === ChatEditingSessionState.Initial)) { - this._chatEditsProgress?.stop(); - } - - // Summary of number of files changed - const innerContainer = this.chatEditingSessionWidgetContainer.querySelector('.chat-editing-session-container.show-file-icons') as HTMLElement ?? dom.append(this.chatEditingSessionWidgetContainer, $('.chat-editing-session-container.show-file-icons')); const seenEntries = new ResourceSet(); const entries: IChatCollapsibleListItem[] = chatEditingSession?.entries.get().map((entry) => { seenEntries.add(entry.modifiedURI); @@ -1218,16 +1217,16 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge kind: 'reference', }; }) ?? []; - for (const attachment of (this.attachmentModel as EditsAttachmentModel).fileAttachments) { - if (URI.isUri(attachment.value) && !seenEntries.has(attachment.value)) { - entries.unshift({ - reference: attachment.value, - state: WorkingSetEntryState.Attached, - kind: 'reference', - }); - seenEntries.add(attachment.value); - } + + if (!chatEditingSession || !this.options.renderWorkingSet || !entries.length) { + dom.clearNode(this.chatEditingSessionWidgetContainer); + this._chatEditsDisposables.clear(); + this._chatEditList = undefined; + return; } + + // Summary of number of files changed + const innerContainer = this.chatEditingSessionWidgetContainer.querySelector('.chat-editing-session-container.show-file-icons') as HTMLElement ?? dom.append(this.chatEditingSessionWidgetContainer, $('.chat-editing-session-container.show-file-icons')); for (const [file, metadata] of chatEditingSession.workingSet.entries()) { if (!seenEntries.has(file) && metadata.state !== WorkingSetEntryState.Suggested) { entries.unshift({ @@ -1240,16 +1239,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge seenEntries.add(file); } } - // Factor file variables that are part of the user query into the working set - for (const part of chatWidget?.parsedInput.parts ?? []) { - if (part instanceof ChatRequestDynamicVariablePart && part.isFile && (URI.isUri(part.data) && !seenEntries.has(part.data) || isLocation(part.data) && !seenEntries.has(part.data.uri))) { - entries.unshift({ - reference: part.data, - state: WorkingSetEntryState.Attached, - kind: 'reference', - }); - } - } entries.sort((a, b) => { if (a.kind === 'reference' && b.kind === 'reference') { @@ -1260,18 +1249,15 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge } return 0; }); + const overviewRegion = innerContainer.querySelector('.chat-editing-session-overview') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-overview')); const overviewTitle = overviewRegion.querySelector('.working-set-title') as HTMLElement ?? dom.append(overviewRegion, $('.working-set-title')); - const overviewWorkingSet = overviewTitle.querySelector('span') ?? dom.append(overviewTitle, $('span')); const overviewFileCount = overviewTitle.querySelector('span.working-set-count') ?? dom.append(overviewTitle, $('span.working-set-count')); - overviewWorkingSet.textContent = localize('chatEditingSession.workingSet', 'Working Set'); - - let suggestedFilesInWorkingSetCount = 0; overviewFileCount.textContent = ''; if (entries.length === 1) { - overviewFileCount.textContent = ' ' + localize('chatEditingSession.oneFile', '(1 file)'); + overviewFileCount.textContent = localize('chatEditingSession.oneFile.1', '1 file changed'); suggestedFilesInWorkingSetCount = entries[0].kind === 'reference' && entries[0].state === WorkingSetEntryState.Suggested ? 1 : 0; } else { suggestedFilesInWorkingSetCount = entries.filter(e => e.kind === 'reference' && e.state === WorkingSetEntryState.Suggested).length; @@ -1279,10 +1265,10 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge if (entries.length > 1) { const fileCount = entries.length - suggestedFilesInWorkingSetCount; - overviewFileCount.textContent = ' ' + (fileCount === 1 ? localize('chatEditingSession.oneFile', '(1 file)') : localize('chatEditingSession.manyFiles', '({0} files)', fileCount)); + overviewFileCount.textContent = (fileCount === 1 ? localize('chatEditingSession.oneFile.1', '1 file changed') : localize('chatEditingSession.manyFiles.1', '{0} files changed', fileCount)); } - overviewTitle.ariaLabel = overviewWorkingSet.textContent + overviewFileCount.textContent; + overviewTitle.ariaLabel = overviewFileCount.textContent; overviewTitle.tabIndex = 0; // Clear out the previous actions (if any) @@ -1308,11 +1294,6 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge return; } - if (currentChatEditingState === ChatEditingSessionState.StreamingEdits || chatWidget?.viewModel?.requestInProgress) { - // this._chatEditsProgress ??= new ProgressBar(innerContainer); - this._chatEditsProgress?.infinite().show(500); - } - // Working set const workingSetContainer = innerContainer.querySelector('.chat-editing-session-list') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-list')); if (!this._chatEditList) { @@ -1354,53 +1335,42 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge list.layout(height); list.getHTMLElement().style.height = `${height}px`; list.splice(0, list.length, entries); - this._combinedChatEditWorkingSetEntries = coalesce(entries.map((e) => e.kind === 'reference' && URI.isUri(e.reference) ? ({ uri: e.reference, isMarkedReadonly: e.isMarkedReadonly }) : undefined)); - - const addFilesElement = innerContainer.querySelector('.chat-editing-session-toolbar-actions') as HTMLElement ?? dom.append(innerContainer, $('.chat-editing-session-toolbar-actions')); + this._onDidChangeHeight.fire(); + } + async renderChatRelatedFiles(chatEditingSession: IChatEditingSession, anchor: HTMLElement) { + dom.clearNode(anchor); const hoverDelegate = getDefaultHoverDelegate('element'); - - const button = this._chatEditsActionsDisposables.add(new Button(addFilesElement, { - supportIcons: true, - secondary: true, - hoverDelegate - })); - button.label = localize('chatAddFiles', '{0} Add Files...', '$(add)'); - button.setTitle(button.enabled ? localize('addFiles.label', 'Add files to your working set') : localize('chatEditingSession.fileLimitReached', 'You have reached the maximum number of files that can be added to the working set.')); - this._chatEditsActionsDisposables.add(button.onDidClick(() => { - this.commandService.executeCommand('workbench.action.chat.editing.attachFiles', { widget: chatWidget }); - })); - dom.append(addFilesElement, button.element); - - // RELATED files (after Add Files...) for (const [uri, metadata] of chatEditingSession.workingSet) { if (metadata.state !== WorkingSetEntryState.Suggested) { continue; } - const addBtn = this._chatEditsActionsDisposables.add(new Button(addFilesElement, { + const uriLabel = this._chatEditsActionsDisposables.add(new Button(anchor, { supportIcons: true, secondary: true, hoverDelegate })); - addBtn.label = this.labelService.getUriBasenameLabel(uri); - addBtn.element.classList.add('monaco-icon-label', ...getIconClasses(this.modelService, this.languageService, uri, FileKind.FILE)); - addBtn.setTitle(localize('suggeste.title', "{0} - {1}", this.labelService.getUriLabel(uri, { relative: true }), metadata.description ?? '')); + uriLabel.label = this.labelService.getUriBasenameLabel(uri); + uriLabel.element.classList.add('monaco-icon-label', ...getIconClasses(this.modelService, this.languageService, uri, FileKind.FILE)); + uriLabel.element.title = localize('suggeste.title', "{0} - {1}", this.labelService.getUriLabel(uri, { relative: true }), metadata.description ?? ''); - this._chatEditsActionsDisposables.add(addBtn.onDidClick(() => { + this._chatEditsActionsDisposables.add(uriLabel.onDidClick(() => { group.remove(); // REMOVE asap - chatEditingSession.addFileToWorkingSet(uri); + this._attachmentModel.addFile(uri); + chatEditingSession.remove(WorkingSetEntryRemovalReason.User, uri); })); - const rmBtn = this._chatEditsActionsDisposables.add(new Button(addFilesElement, { + const addButton = this._chatEditsActionsDisposables.add(new Button(anchor, { supportIcons: false, secondary: true, hoverDelegate, - ariaLabel: localize('chatEditingSession.removeSuggestion', 'Remove suggestion {0}', this.labelService.getUriLabel(uri, { relative: true })), + ariaLabel: localize('chatEditingSession.addSuggestion', 'Add suggestion {0}', this.labelService.getUriLabel(uri, { relative: true })), })); - rmBtn.icon = Codicon.close; - rmBtn.setTitle(localize('chatEditingSession.removeSuggested', 'Remove suggestion')); - this._chatEditsActionsDisposables.add(rmBtn.onDidClick(() => { + addButton.icon = Codicon.add; + addButton.setTitle(localize('chatEditingSession.addSuggested', 'Add suggestion')); + this._chatEditsActionsDisposables.add(addButton.onDidClick(() => { group.remove(); // REMOVE asap + this._attachmentModel.addFile(uri); chatEditingSession.remove(WorkingSetEntryRemovalReason.User, uri); })); @@ -1408,17 +1378,17 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge sep.classList.add('separator'); const group = document.createElement('span'); - group.classList.add('monaco-button-dropdown', 'sidebyside-button'); - group.appendChild(addBtn.element); + group.classList.add('monaco-button-dropdown', 'sidebyside-button', 'show-file-icons'); + group.appendChild(uriLabel.element); group.appendChild(sep); - group.appendChild(rmBtn.element); - dom.append(addFilesElement, group); + group.appendChild(addButton.element); + dom.append(anchor, group); this._chatEditsActionsDisposables.add(toDisposable(() => { group.remove(); })); } - + this._onDidChangeHeight.fire(); } async renderFollowups(items: IChatFollowup[] | undefined, response: IChatResponseViewModel | undefined): Promise { @@ -1484,7 +1454,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge inputPartEditorHeight: Math.min(this._inputEditor.getContentHeight(), this.inputEditorMaxHeight), inputPartHorizontalPadding: this.options.renderStyle === 'compact' ? 16 : 32, inputPartVerticalPadding: this.options.renderStyle === 'compact' ? 12 : 28, - attachmentsHeight: this.attachedContextContainer.offsetHeight, + attachmentsHeight: this.attachedContextContainer.offsetHeight + this.relatedFilesContainer.offsetHeight, editorBorder: 2, inputPartHorizontalPaddingInside: 12, toolbarsWidth: this.options.renderStyle === 'compact' ? executeToolbarWidth + executeToolbarPadding + inputToolbarWidth + inputToolbarPadding : 0, diff --git a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts index e79f6690f34..8f5d0743d04 100644 --- a/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts +++ b/src/vs/workbench/contrib/chat/browser/chatListRenderer.ts @@ -578,7 +578,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer | undefined, workingSet: ReadonlyArray | undefined, templateData: IChatListItemTemplate) { - return this.instantiationService.createInstance(ChatAttachmentsContentPart, variables, contentReferences, workingSet, undefined); + private renderAttachments(variables: IChatRequestVariableEntry[], contentReferences: ReadonlyArray | undefined, templateData: IChatListItemTemplate) { + return this.instantiationService.createInstance(ChatAttachmentsContentPart, variables, contentReferences, undefined); } private renderTextEdit(context: IChatContentPartRenderContext, chatTextEdit: IChatTextEditGroup, templateData: IChatListItemTemplate): IChatContentPart { diff --git a/src/vs/workbench/contrib/chat/browser/chatQuick.ts b/src/vs/workbench/contrib/chat/browser/chatQuick.ts index fe13bc97df6..412295c39eb 100644 --- a/src/vs/workbench/contrib/chat/browser/chatQuick.ts +++ b/src/vs/workbench/contrib/chat/browser/chatQuick.ts @@ -221,7 +221,7 @@ class QuickChat extends Disposable { ChatWidget, ChatAgentLocation.Panel, { isQuickChat: true }, - { autoScroll: true, renderInputOnTop: true, renderStyle: 'compact', menus: { inputSideToolbar: MenuId.ChatInputSide }, enableImplicitContext: true }, + { autoScroll: true, renderInputOnTop: true, renderStyle: 'compact', menus: { inputSideToolbar: MenuId.ChatInputSide } }, { listForeground: quickInputForeground, listBackground: quickInputBackground, diff --git a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts index bb826d44c31..a7ad1507eb6 100644 --- a/src/vs/workbench/contrib/chat/browser/chatViewPane.ts +++ b/src/vs/workbench/contrib/chat/browser/chatViewPane.ts @@ -185,7 +185,6 @@ export class ChatViewPane extends ViewPane implements IViewWelcomeDelegate { referencesExpandedWhenEmptyResponse: this.chatOptions.location !== ChatAgentLocation.EditingSession, progressMessageAtBottomOfResponse: this.chatOptions.location === ChatAgentLocation.EditingSession, }, - enableImplicitContext: this.chatOptions.location === ChatAgentLocation.Panel, editorOverflowWidgetsDomNode: editorOverflowNode, enableWorkingSet: this.chatOptions.location === ChatAgentLocation.EditingSession ? 'explicit' : undefined }, diff --git a/src/vs/workbench/contrib/chat/browser/chatWidget.ts b/src/vs/workbench/contrib/chat/browser/chatWidget.ts index 0d2cf3f81dc..f2474822d75 100644 --- a/src/vs/workbench/contrib/chat/browser/chatWidget.ts +++ b/src/vs/workbench/contrib/chat/browser/chatWidget.ts @@ -830,7 +830,6 @@ export class ChatWidget extends Disposable implements IChatWidget { renderStyle: options?.renderStyle === 'minimal' ? 'compact' : options?.renderStyle, menus: { executeToolbar: MenuId.ChatExecute, ...this.viewOptions.menus }, editorOverflowWidgetsDomNode: this.viewOptions.editorOverflowWidgetsDomNode, - enableImplicitContext: this.viewOptions.enableImplicitContext, renderWorkingSet: this.viewOptions.enableWorkingSet === 'explicit' }, this.styles, @@ -1100,22 +1099,20 @@ export class ChatWidget extends Disposable implements IChatWidget { } let attachedContext = this.inputPart.getAttachedAndImplicitContext(this.viewModel.sessionId); - let workingSet: URI[] | undefined; if (this.viewOptions.enableWorkingSet !== undefined) { const currentEditingSession = this._editingSession; const unconfirmedSuggestions = new ResourceSet(); const uniqueWorkingSetEntries = new ResourceSet(); // NOTE: this is used for bookkeeping so the UI can avoid rendering references in the UI that are already shown in the working set - const editingSessionAttachedContext: IChatRequestVariableEntry[] = []; + const editingSessionAttachedContext: IChatRequestVariableEntry[] = attachedContext; // Pick up everything that the user sees is part of the working set. - // This should never exceed the maximum file entries limit above. - for (const { uri, isMarkedReadonly } of this.inputPart.chatEditWorkingSetFiles) { + for (const [uri, state] of currentEditingSession.get()?.workingSet || []) { // Skip over any suggested files that haven't been confirmed yet in the working set - if (currentEditingSession.get()?.workingSet.get(uri)?.state === WorkingSetEntryState.Suggested) { + if (state.state === WorkingSetEntryState.Suggested) { unconfirmedSuggestions.add(uri); } else { uniqueWorkingSetEntries.add(uri); - editingSessionAttachedContext.push(this.attachmentModel.asVariableEntry(uri, undefined, isMarkedReadonly)); + editingSessionAttachedContext.unshift(this.attachmentModel.asVariableEntry(uri, undefined, state.isMarkedReadonly)); } } @@ -1153,7 +1150,6 @@ export class ChatWidget extends Disposable implements IChatWidget { } } } - workingSet = [...uniqueWorkingSetEntries.values()]; attachedContext = editingSessionAttachedContext; type ChatEditingWorkingSetClassification = { @@ -1166,7 +1162,7 @@ export class ChatWidget extends Disposable implements IChatWidget { originalSize: number; actualSize: number; }; - this.telemetryService.publicLog2('chatEditing/workingSetSize', { originalSize: this.inputPart.attemptedWorkingSetEntriesCount, actualSize: uniqueWorkingSetEntries.size }); + this.telemetryService.publicLog2('chatEditing/workingSetSize', { originalSize: uniqueWorkingSetEntries.size, actualSize: uniqueWorkingSetEntries.size }); currentEditingSession.get()?.remove(WorkingSetEntryRemovalReason.User, ...unconfirmedSuggestions); } @@ -1178,7 +1174,6 @@ export class ChatWidget extends Disposable implements IChatWidget { locationData: this._location.resolveData?.(), parserContext: { selectedAgent: this._lastSelectedAgent }, attachedContext, - workingSet, noCommandDetection: options?.noCommandDetection, hasInstructionAttachments: this.inputPart.hasInstructionAttachments, }); diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts index 8e7bf6a8e53..c5c3fedf9a6 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputCompletions.ts @@ -666,7 +666,7 @@ class BuiltinDynamicCompletions extends Disposable { // RELATED FILES if (widget.location === ChatAgentLocation.EditingSession && widget.viewModel && this._chatEditingService.getEditingSession(widget.viewModel.sessionId)) { - const relatedFiles = (await raceTimeout(this._chatEditingService.getRelatedFiles(widget.viewModel.sessionId, widget.getInput(), token), 200)) ?? []; + const relatedFiles = (await raceTimeout(this._chatEditingService.getRelatedFiles(widget.viewModel.sessionId, widget.getInput(), widget.attachmentModel.fileAttachments, token), 200)) ?? []; for (const relatedFileGroup of relatedFiles) { for (const relatedFile of relatedFileGroup.files) { if (seen.has(relatedFile.uri)) { diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts index 46270b02a6b..3860b5d5efa 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatInputRelatedFilesContrib.ts @@ -6,12 +6,12 @@ import { CancellationToken } from '../../../../../base/common/cancellation.js'; import { Event } from '../../../../../base/common/event.js'; import { Disposable, DisposableStore } from '../../../../../base/common/lifecycle.js'; -import { ResourceMap } from '../../../../../base/common/map.js'; +import { ResourceMap, ResourceSet } from '../../../../../base/common/map.js'; +import { autorun } from '../../../../../base/common/observable.js'; import { URI } from '../../../../../base/common/uri.js'; import { localize } from '../../../../../nls.js'; import { IWorkbenchContribution } from '../../../../common/contributions.js'; -import { ChatAgentLocation } from '../../common/chatAgents.js'; -import { ChatEditingSessionChangeType, IChatEditingService, IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; +import { IChatEditingService, IChatEditingSession, WorkingSetEntryRemovalReason, WorkingSetEntryState } from '../../common/chatEditingService.js'; import { IChatWidget, IChatWidgetService } from '../chat.js'; export class ChatRelatedFilesContribution extends Disposable implements IWorkbenchContribution { @@ -26,16 +26,15 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben ) { super(); - this._register( - this.chatWidgetService.onDidAddWidget(widget => { - if (widget.location === ChatAgentLocation.EditingSession && widget.viewModel?.sessionId) { - const editingSession = this.chatEditingService.getEditingSession(widget.viewModel.sessionId); - if (editingSession) { - this._handleNewEditingSession(editingSession, widget); - } + this._register(autorun((reader) => { + const sessions = this.chatEditingService.editingSessionsObs.read(reader); + sessions.forEach(session => { + const widget = this.chatWidgetService.getWidgetBySessionId(session.chatSessionId); + if (widget && !this.chatEditingSessionDisposables.has(session.chatSessionId)) { + this._handleNewEditingSession(session, widget); } - }), - ); + }); + })); } private _updateRelatedFileSuggestions(currentEditingSession: IChatEditingSession, widget: IChatWidget) { @@ -44,12 +43,12 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben } const workingSetEntries = currentEditingSession.entries.get(); - if (workingSetEntries.length > 0) { + if (workingSetEntries.length > 0 || widget.attachmentModel.fileAttachments.length === 0) { // Do this only for the initial working set state return; } - this._currentRelatedFilesRetrievalOperation = this.chatEditingService.getRelatedFiles(currentEditingSession.chatSessionId, widget.getInput(), CancellationToken.None) + this._currentRelatedFilesRetrievalOperation = this.chatEditingService.getRelatedFiles(currentEditingSession.chatSessionId, widget.getInput(), widget.attachmentModel.fileAttachments, CancellationToken.None) .then((files) => { if (!files?.length || !widget.viewModel?.sessionId) { return; @@ -60,6 +59,11 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben return; // Might have disposed while we were calculating } + const existingFiles = new ResourceSet(widget.attachmentModel.fileAttachments); + if (!existingFiles.size) { + return; + } + // Pick up to 2 related files const newSuggestions = new ResourceMap<{ description: string; group: string }>(); for (const group of files) { @@ -67,7 +71,11 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben if (newSuggestions.size >= 2) { break; } + if (existingFiles.has(file.uri)) { + continue; + } newSuggestions.set(file.uri, { group: group.group, description: file.description }); + existingFiles.add(file.uri); } } @@ -101,10 +109,8 @@ export class ChatRelatedFilesContribution extends Disposable implements IWorkben disposableStore.add(onDebouncedType(() => { this._updateRelatedFileSuggestions(currentEditingSession, widget); })); - disposableStore.add(currentEditingSession.onDidChange((e) => { - if (e === ChatEditingSessionChangeType.WorkingSet) { - this._updateRelatedFileSuggestions(currentEditingSession, widget); - } + disposableStore.add(widget.attachmentModel.onDidChangeContext(() => { + this._updateRelatedFileSuggestions(currentEditingSession, widget); })); disposableStore.add(currentEditingSession.onDidDispose(() => { disposableStore.dispose(); diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 89cefce6b6c..e9dd2540fca 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -685,25 +685,68 @@ have to be updated for changes to the rules above, or to support more deeply nes justify-content: start; } -.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button.secondary:first-child { +.chat-related-files { + padding: 4px 0 0 0; + display: flex; + flex-wrap: wrap; + align-items: center; +} + +.chat-related-files .monaco-button-dropdown .monaco-text-button { + font-size: 11px; + justify-content: left; + width: fit-content; + padding: 0px; + border: none; +} + +.chat-related-files .monaco-button-dropdown .monaco-text-button span { + font-style: italic; + height: 18px; + opacity: 0.7; +} + +.chat-related-files .monaco-button-dropdown { + border-radius: 4px; + height: 18px; + border: 1px solid var(--vscode-input-border); + align-items: center; + overflow: hidden; + padding: 0 0 0 2px; +} + +.chat-related-files .monaco-button.codicon.codicon-add { + display: flex; + color: var(--vscode-descriptionForeground); + padding-right: 4px; + padding-left: 4px; + padding-top: 3px; + height: 100%; +} + +.interactive-session .chat-related-files .monaco-icon-label::before { + padding: 2px 2px 0 0; +} + +.interactive-session .chat-editing-session .chat-related-files .monaco-button.secondary:first-child { margin: 3px 0px 3px 3px; flex-shrink: 0; } -.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button.secondary.monaco-icon-label::before { +.interactive-session .chat-editing-session .chat-related-files .monaco-button.secondary.monaco-icon-label::before { display: inline-flex; align-items: center; } -.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button.secondary:only-child { +.interactive-session .chat-editing-session .chat-related-files .monaco-button.secondary:only-child { width: 100%; } -.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button.secondary.disabled { +.interactive-session .chat-editing-session .chat-related-files .monaco-button.secondary.disabled { cursor: initial; } -.interactive-session .chat-editing-session .chat-editing-session-toolbar-actions .monaco-button.secondary .codicon { +.interactive-session .chat-editing-session .chat-related-files .monaco-button.secondary .codicon { font-size: 12px; margin-left: 4px; } @@ -1078,6 +1121,13 @@ have to be updated for changes to the rules above, or to support more deeply nes border-radius: 4px; height: 18px; max-width: 100%; + width: fit-content; +} + +.interactive-session .chat-attached-context .chat-attached-context-attachment.chat-add-files { + height: 20px; + gap: 0px; + color: var(--vscode-descriptionForeground); } .interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-button { @@ -1091,6 +1141,8 @@ have to be updated for changes to the rules above, or to support more deeply nes outline-offset: -4px; } +.chat-related-files .monaco-button.codicon.codicon-add:hover, +.interactive-session .chat-attached-context .chat-attached-context-attachment.chat-add-files:hover, .interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-button:hover { cursor: pointer; background: var(--vscode-toolbar-hoverBackground); @@ -1122,9 +1174,8 @@ have to be updated for changes to the rules above, or to support more deeply nes } .interactive-session .chat-attached-context { - padding: 0 0 4px 0; + padding: 5px 0 0 0; display: flex; - gap: 4px; flex-wrap: wrap; cursor: default; } diff --git a/src/vs/workbench/contrib/chat/common/chatEditingService.ts b/src/vs/workbench/contrib/chat/common/chatEditingService.ts index 0fd3869b4aa..0f4d8ff0dc9 100644 --- a/src/vs/workbench/contrib/chat/common/chatEditingService.ts +++ b/src/vs/workbench/contrib/chat/common/chatEditingService.ts @@ -41,7 +41,7 @@ export interface IChatEditingService { hasRelatedFilesProviders(): boolean; registerRelatedFilesProvider(handle: number, provider: IChatRelatedFilesProvider): IDisposable; - getRelatedFiles(chatSessionId: string, prompt: string, token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined>; + getRelatedFiles(chatSessionId: string, prompt: string, files: URI[], token: CancellationToken): Promise<{ group: string; files: IChatRelatedFile[] }[] | undefined>; //#endregion } @@ -88,7 +88,7 @@ export interface IChatEditingSession extends IDisposable { readonly state: IObservable; readonly entries: IObservable; readonly workingSet: ResourceMap; - addFileToWorkingSet(uri: URI, description?: string, kind?: WorkingSetEntryState.Transient | WorkingSetEntryState.Suggested): void; + addFileToWorkingSet(uri: URI, description?: string, kind?: WorkingSetEntryState.Suggested): void; show(): Promise; remove(reason: WorkingSetEntryRemovalReason, ...uris: URI[]): void; markIsReadonly(uri: URI, isReadonly?: boolean): void; @@ -155,7 +155,7 @@ export const enum WorkingSetEntryState { Modified, Accepted, Rejected, - Transient, + Transient, // TODO@joyceerhl remove this Attached, Sent, // TODO@joyceerhl remove this Suggested, diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 5a8a97a0d16..dec4623cd07 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -125,7 +125,6 @@ export interface IChatRequestModel { readonly confirmation?: string; readonly locationData?: IChatLocationData; readonly attachedContext?: IChatRequestVariableEntry[]; - readonly workingSet?: URI[]; readonly isCompleteAddedRequest: boolean; readonly response?: IChatResponseModel; shouldBeRemovedOnSend: IChatRequestDisablement | undefined; @@ -290,10 +289,6 @@ export class ChatRequestModel implements IChatRequestModel { return this._attachedContext; } - public get workingSet(): URI[] | undefined { - return this._workingSet; - } - constructor( private _session: ChatModel, public readonly message: IParsedChatRequest, @@ -303,7 +298,6 @@ export class ChatRequestModel implements IChatRequestModel { private _confirmation?: string, private _locationData?: IChatLocationData, private _attachedContext?: IChatRequestVariableEntry[], - private _workingSet?: URI[], public readonly isCompleteAddedRequest = false, restoredId?: string, ) { @@ -1254,7 +1248,7 @@ export class ChatModel extends Disposable implements IChatModel { // Old messages don't have variableData, or have it in the wrong (non-array) shape const variableData: IChatRequestVariableData = this.reviveVariableData(raw.variableData); - const request = new ChatRequestModel(this, parsedRequest, variableData, raw.timestamp ?? -1, undefined, undefined, undefined, undefined, raw.workingSet?.map((uri) => URI.revive(uri)), undefined, raw.requestId); + const request = new ChatRequestModel(this, parsedRequest, variableData, raw.timestamp ?? -1, undefined, undefined, undefined, undefined, undefined, raw.requestId); request.shouldBeRemovedOnSend = raw.isHidden ? { requestId: raw.requestId } : raw.shouldBeRemovedOnSend; if (raw.response || raw.result || (raw as any).responseErrorDetails) { const agent = (raw.agent && 'metadata' in raw.agent) ? // Check for the new format, ignore entries in the old format @@ -1388,7 +1382,7 @@ export class ChatModel extends Disposable implements IChatModel { } addRequest(message: IParsedChatRequest, variableData: IChatRequestVariableData, attempt: number, chatAgent?: IChatAgentData, slashCommand?: IChatAgentCommand, confirmation?: string, locationData?: IChatLocationData, attachments?: IChatRequestVariableEntry[], workingSet?: URI[], isCompleteAddedRequest?: boolean): ChatRequestModel { - const request = new ChatRequestModel(this, message, variableData, Date.now(), attempt, confirmation, locationData, attachments, workingSet, isCompleteAddedRequest); + const request = new ChatRequestModel(this, message, variableData, Date.now(), attempt, confirmation, locationData, attachments, isCompleteAddedRequest); request.response = new ChatResponseModel([], this, chatAgent, slashCommand, request.id, undefined, undefined, undefined, undefined, undefined, undefined, isCompleteAddedRequest); this._requests.push(request); @@ -1548,7 +1542,6 @@ export class ChatModel extends Disposable implements IChatModel { vote: r.response?.vote, voteDownReason: r.response?.voteDownReason, agent: agentJson, - workingSet: r.workingSet, slashCommand: r.response?.slashCommand, usedContext: r.response?.usedContext, contentReferences: r.response?.contentReferences, diff --git a/src/vs/workbench/contrib/chat/common/chatService.ts b/src/vs/workbench/contrib/chat/common/chatService.ts index 642855f9f83..b2246a10052 100644 --- a/src/vs/workbench/contrib/chat/common/chatService.ts +++ b/src/vs/workbench/contrib/chat/common/chatService.ts @@ -438,7 +438,6 @@ export interface IChatSendRequestOptions { acceptedConfirmationData?: any[]; rejectedConfirmationData?: any[]; attachedContext?: IChatRequestVariableEntry[]; - workingSet?: URI[]; /** The target agent ID can be specified with this property instead of using @ in 'message' */ agentId?: string; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 9622f30adba..6e5e83b27a0 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -476,7 +476,6 @@ export class ChatService extends Disposable implements IChatService { ...options, locationData: request.locationData, attachedContext: request.attachedContext, - workingSet: request.workingSet, hasInstructionAttachments: options?.hasInstructionAttachments ?? false, }; await this._sendRequestAsync(model, model.sessionId, request.message, attempt, enableCommandDetection, defaultAgent, location, resendOptions).responseCompletePromise; @@ -635,7 +634,7 @@ export class ChatService extends Disposable implements IChatService { if (agentPart || (defaultAgent && !commandPart)) { const prepareChatAgentRequest = async (agent: IChatAgentData, command?: IChatAgentCommand, enableCommandDetection?: boolean, chatRequest?: ChatRequestModel, isParticipantDetected?: boolean): Promise => { const initVariableData: IChatRequestVariableData = { variables: [] }; - request = chatRequest ?? model.addRequest(parsedRequest, initVariableData, attempt, agent, command, options?.confirmation, options?.locationData, options?.attachedContext, options?.workingSet); + request = chatRequest ?? model.addRequest(parsedRequest, initVariableData, attempt, agent, command, options?.confirmation, options?.locationData, options?.attachedContext); let variableData: IChatRequestVariableData; let message: string; @@ -644,11 +643,6 @@ export class ChatService extends Disposable implements IChatService { message = getPromptText(request.message).message; } else { variableData = this.chatVariablesService.resolveVariables(parsedRequest, request.attachedContext); - for (const variable of variableData.variables) { - if (request.workingSet && variable.isFile && URI.isUri(variable.value)) { - request.workingSet.push(variable.value); - } - } model.updateRequest(request, variableData); const promptTextResult = getPromptText(request.message); diff --git a/src/vs/workbench/contrib/chat/common/chatViewModel.ts b/src/vs/workbench/contrib/chat/common/chatViewModel.ts index 0dadc461d0c..88d973ecc41 100644 --- a/src/vs/workbench/contrib/chat/common/chatViewModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatViewModel.ts @@ -74,7 +74,6 @@ export interface IChatRequestViewModel { readonly variables: IChatRequestVariableEntry[]; currentRenderedHeight: number | undefined; readonly contentReferences?: ReadonlyArray; - readonly workingSet?: ReadonlyArray; readonly confirmation?: string; readonly shouldBeRemovedOnSend: IChatRequestDisablement | undefined; readonly isComplete: boolean; @@ -379,10 +378,6 @@ export class ChatRequestViewModel implements IChatRequestViewModel { return this._model.response?.contentReferences; } - get workingSet() { - return this._model.workingSet; - } - get confirmation() { return this._model.confirmation; } diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap index 127a458fd8a..2de45db43d1 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize.0.snap @@ -78,7 +78,6 @@ slashCommands: [ ], disambiguation: [ ] }, - workingSet: undefined, slashCommand: undefined, usedContext: { documents: [ diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap index dd966544c50..0a9e4d67229 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_deserialize_with_response.0.snap @@ -78,7 +78,6 @@ slashCommands: [ ], disambiguation: [ ] }, - workingSet: undefined, slashCommand: undefined, usedContext: undefined, contentReferences: [ ], diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap index 637d3801e22..36fd2784d41 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_can_serialize.1.snap @@ -85,7 +85,6 @@ slashCommands: [ ], disambiguation: [ ] }, - workingSet: undefined, slashCommand: undefined, usedContext: { documents: [ @@ -154,7 +153,6 @@ disambiguation: [ ], isDefault: true }, - workingSet: undefined, slashCommand: undefined, usedContext: undefined, contentReferences: [ ], diff --git a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap index 9ebead9b671..8e5fd37cea2 100644 --- a/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap +++ b/src/vs/workbench/contrib/chat/test/common/__snapshots__/ChatService_sendRequest_fails.0.snap @@ -78,7 +78,6 @@ slashCommands: [ ], disambiguation: [ ] }, - workingSet: undefined, slashCommand: undefined, usedContext: undefined, contentReferences: [ ],