fix: consolidate working set and chat attachments (#241116)

* Render attachments above chat input part

* Remove Add Files... button

* Remove edits input part progress

* Use same attachment model for chat and edits

* Remove gap for chat attached context element

* Don't render changed files list if edits has not made changes

* Roll active editor tracking into chat implicit context widget

* Fix rendering chat related files

* Render related files below chat attachments and above input editor

* Use chat attachments as source of truth for building chat variables in edits requests

* Render a `+` for suggested related files

* Don't suggest same file twice

* Read working set entries from editing session

* Make working set list title more succinct

* Render working set files in chat requests

* Move Attach button to top of chat input part

* Render keybinding hint in Add Files button hover

* Don't compute/apply related file suggestions if there are no attachments
This commit is contained in:
Joyce Er
2025-02-19 14:45:30 -08:00
committed by GitHub
parent c243707ffa
commit a3d87bbb04
26 changed files with 329 additions and 477 deletions
@@ -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<IQuickPickItem> | 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<void> {
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);
}
});
@@ -190,7 +190,6 @@ export interface IChatWidgetViewOptions {
};
defaultElementHeight?: number;
editorOverflowWidgetsDomNode?: HTMLElement;
enableImplicitContext?: boolean;
enableWorkingSet?: 'explicit' | 'implicit';
}
@@ -42,6 +42,15 @@ export class ChatAttachmentModel extends Disposable {
return this._attachments.size;
}
get fileAttachments(): URI[] {
return this.attachments.reduce<URI[]>((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<void>());
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<string>();
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();
}
}
@@ -56,7 +56,6 @@ export class ChatAttachmentsContentPart extends Disposable {
constructor(
private readonly variables: IChatRequestVariableEntry[],
private readonly contentReferences: ReadonlyArray<IChatContentReference> = [],
private readonly workingSet: ReadonlyArray<URI> = [],
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 });
@@ -435,25 +435,15 @@ class CollapsibleListRenderer implements IListRenderer<IChatCollapsibleListItem,
} else if (matchesSomeScheme(uri, Schemas.mailto, Schemas.http, Schemas.https)) {
templateData.label.setResource({ resource: uri, name: uri.toString() }, { icon: icon ?? Codicon.globe, title: data.options?.status?.description ?? data.title ?? uri.toString(), strikethrough: data.excluded, extraClasses });
} else {
if (data.state === WorkingSetEntryState.Transient || data.state === WorkingSetEntryState.Suggested) {
templateData.label.setResource(
{
resource: uri,
name: basenameOrAuthority(uri),
description: data.description ?? localize('chat.openEditor', 'Open Editor'),
range: 'range' in reference ? reference.range : undefined,
}, { icon, title: data.options?.status?.description ?? data.title, italic: data.state === WorkingSetEntryState.Suggested, strikethrough: data.excluded, extraClasses });
} else {
templateData.label.setFile(uri, {
fileKind: FileKind.FILE,
// Should not have this live-updating data on a historical reference
fileDecorations: undefined,
range: 'range' in reference ? reference.range : undefined,
title: data.options?.status?.description ?? data.title,
strikethrough: data.excluded,
extraClasses
});
}
templateData.label.setFile(uri, {
fileKind: FileKind.FILE,
// Should not have this live-updating data on a historical reference
fileDecorations: undefined,
range: 'range' in reference ? reference.range : undefined,
title: data.options?.status?.description ?? data.title,
strikethrough: data.excluded,
extraClasses
});
}
}
@@ -33,7 +33,6 @@ import { IChatService } from '../../common/chatService.js';
import { isRequestVM, isResponseVM } from '../../common/chatViewModel.js';
import { CHAT_CATEGORY } from '../actions/chatActions.js';
import { ChatTreeItem, IChatWidget, IChatWidgetService } from '../chat.js';
import { EditsAttachmentModel } from '../chatAttachmentModel.js';
export interface IEditingSessionActionContext {
widget?: IChatWidget;
@@ -129,7 +128,7 @@ registerAction2(class AddFileToWorkingSet extends WorkingSetAction {
icon: Codicon.plus,
menu: [{
id: MenuId.ChatEditingWidgetModifiedFilesToolbar,
when: ContextKeyExpr.or(ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, WorkingSetEntryState.Transient), ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, WorkingSetEntryState.Suggested)),
when: ContextKeyExpr.equals(chatEditingWidgetFileStateContextKey.key, WorkingSetEntryState.Suggested),
order: 0,
group: 'navigation'
}],
@@ -391,8 +390,8 @@ export class ChatEditingRemoveAllFilesAction extends EditingSessionAction {
editingSession.remove(WorkingSetEntryRemovalReason.User, ...uris);
// Remove all file attachments
const fileAttachments = chatWidget.attachmentModel ? [...(chatWidget.attachmentModel as EditsAttachmentModel).excludedFileAttachments, ...(chatWidget.attachmentModel as EditsAttachmentModel).fileAttachments] : [];
const attachmentIdsToRemove = fileAttachments.map(attachment => (attachment.value as URI).toString());
const fileAttachments = chatWidget.attachmentModel ? chatWidget.attachmentModel.fileAttachments : [];
const attachmentIdsToRemove = fileAttachments.map(attachment => attachment.toString());
chatWidget.attachmentModel.delete(...attachmentIdsToRemove);
}
}
@@ -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 };
}
@@ -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);
@@ -68,7 +68,6 @@ export class ChatEditor extends EditorPane {
undefined,
{
supportsFileReferences: true,
enableImplicitContext: true
},
{
listForeground: editorForeground,
@@ -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<DisposableStore>());
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<WorkbenchList<IChatCollapsibleListItem>> | 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<void> {
@@ -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,
@@ -578,7 +578,7 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
if (!isFiltered) {
if (isRequestVM(element) && element.variables.length) {
const newPart = this.renderAttachments(element.variables, element.contentReferences, element.workingSet, templateData);
const newPart = this.renderAttachments(element.variables, element.contentReferences, templateData);
if (newPart) {
if (newPart.domNode) {
// p has a :last-child rule for margin
@@ -951,8 +951,8 @@ export class ChatListItemRenderer extends Disposable implements ITreeRenderer<Ch
return part;
}
private renderAttachments(variables: IChatRequestVariableEntry[], contentReferences: ReadonlyArray<IChatContentReference> | undefined, workingSet: ReadonlyArray<URI> | undefined, templateData: IChatListItemTemplate) {
return this.instantiationService.createInstance(ChatAttachmentsContentPart, variables, contentReferences, workingSet, undefined);
private renderAttachments(variables: IChatRequestVariableEntry[], contentReferences: ReadonlyArray<IChatContentReference> | undefined, templateData: IChatListItemTemplate) {
return this.instantiationService.createInstance(ChatAttachmentsContentPart, variables, contentReferences, undefined);
}
private renderTextEdit(context: IChatContentPartRenderContext, chatTextEdit: IChatTextEditGroup, templateData: IChatListItemTemplate): IChatContentPart {
@@ -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,
@@ -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
},
@@ -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<ChatEditingWorkingSetEvent, ChatEditingWorkingSetClassification>('chatEditing/workingSetSize', { originalSize: this.inputPart.attemptedWorkingSetEntriesCount, actualSize: uniqueWorkingSetEntries.size });
this.telemetryService.publicLog2<ChatEditingWorkingSetEvent, ChatEditingWorkingSetClassification>('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,
});
@@ -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)) {
@@ -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();
@@ -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;
}
@@ -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<ChatEditingSessionState>;
readonly entries: IObservable<readonly IModifiedFileEntry[]>;
readonly workingSet: ResourceMap<WorkingSetDisplayMetadata>;
addFileToWorkingSet(uri: URI, description?: string, kind?: WorkingSetEntryState.Transient | WorkingSetEntryState.Suggested): void;
addFileToWorkingSet(uri: URI, description?: string, kind?: WorkingSetEntryState.Suggested): void;
show(): Promise<void>;
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,
@@ -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,
@@ -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;
@@ -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<IChatAgentRequest> => {
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);
@@ -74,7 +74,6 @@ export interface IChatRequestViewModel {
readonly variables: IChatRequestVariableEntry[];
currentRenderedHeight: number | undefined;
readonly contentReferences?: ReadonlyArray<IChatContentReference>;
readonly workingSet?: ReadonlyArray<URI>;
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;
}
@@ -78,7 +78,6 @@
slashCommands: [ ],
disambiguation: [ ]
},
workingSet: undefined,
slashCommand: undefined,
usedContext: {
documents: [
@@ -78,7 +78,6 @@
slashCommands: [ ],
disambiguation: [ ]
},
workingSet: undefined,
slashCommand: undefined,
usedContext: undefined,
contentReferences: [ ],
@@ -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: [ ],
@@ -78,7 +78,6 @@
slashCommands: [ ],
disambiguation: [ ]
},
workingSet: undefined,
slashCommand: undefined,
usedContext: undefined,
contentReferences: [ ],