mirror of
https://github.com/microsoft/vscode.git
synced 2026-07-11 17:18:13 +01:00
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:
@@ -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 });
|
||||
|
||||
+9
-19
@@ -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;
|
||||
}
|
||||
|
||||
-1
@@ -78,7 +78,6 @@
|
||||
slashCommands: [ ],
|
||||
disambiguation: [ ]
|
||||
},
|
||||
workingSet: undefined,
|
||||
slashCommand: undefined,
|
||||
usedContext: {
|
||||
documents: [
|
||||
|
||||
-1
@@ -78,7 +78,6 @@
|
||||
slashCommands: [ ],
|
||||
disambiguation: [ ]
|
||||
},
|
||||
workingSet: undefined,
|
||||
slashCommand: undefined,
|
||||
usedContext: undefined,
|
||||
contentReferences: [ ],
|
||||
|
||||
-2
@@ -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: [ ],
|
||||
|
||||
-1
@@ -78,7 +78,6 @@
|
||||
slashCommands: [ ],
|
||||
disambiguation: [ ]
|
||||
},
|
||||
workingSet: undefined,
|
||||
slashCommand: undefined,
|
||||
usedContext: undefined,
|
||||
contentReferences: [ ],
|
||||
|
||||
Reference in New Issue
Block a user