Move "Tools" into dedicated bucket of the "Add Context" picker (#246812)

This commit is contained in:
Johannes Rieken
2025-04-17 13:52:39 +02:00
committed by GitHub
parent 5edaa56c0e
commit 7e8f505cd9
5 changed files with 342 additions and 327 deletions

View File

@@ -3,14 +3,15 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { groupBy } from '../../../../../base/common/arrays.js';
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { Codicon } from '../../../../../base/common/codicons.js';
import { ResolvedKeybinding } from '../../../../../base/common/keybindings.js';
import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';
import { DisposableStore } from '../../../../../base/common/lifecycle.js';
import { Schemas } from '../../../../../base/common/network.js';
import { isElectron } from '../../../../../base/common/platform.js';
import { basename, dirname } from '../../../../../base/common/resources.js';
import { compare } from '../../../../../base/common/strings.js';
import { basename, dirname, extUri } from '../../../../../base/common/resources.js';
import { ThemeIcon } from '../../../../../base/common/themables.js';
import { WithUriValue } from '../../../../../base/common/types.js';
import { URI } from '../../../../../base/common/uri.js';
@@ -30,6 +31,7 @@ import { IKeybindingService } from '../../../../../platform/keybinding/common/ke
import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';
import { ILabelService } from '../../../../../platform/label/common/label.js';
import { ILogService } from '../../../../../platform/log/common/log.js';
import { IMarkerService, MarkerSeverity } from '../../../../../platform/markers/common/markers.js';
import { AnythingQuickAccessProviderRunOptions } from '../../../../../platform/quickinput/common/quickAccess.js';
import { IQuickInputService, IQuickPickItem, IQuickPickItemWithResource, IQuickPickSeparator, QuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js';
import { ActiveEditorContext, TextCompareEditorActiveContext } from '../../../../common/contextkeys.js';
@@ -55,15 +57,16 @@ import { IChatEditingService } from '../../common/chatEditingService.js';
import { IChatRequestVariableEntry, IDiagnosticVariableEntryFilterData, OmittedState } from '../../common/chatModel.js';
import { ChatRequestAgentPart } from '../../common/chatParserTypes.js';
import { ChatAgentLocation } from '../../common/constants.js';
import { IToolData } from '../../common/languageModelToolsService.js';
import { IChatWidget, IChatWidgetService, IQuickChatService, showChatView } from '../chat.js';
import { imageToHash, isImage } from '../chatPasteProviders.js';
import { isQuickChat } from '../chatWidget.js';
import { createFolderQuickPick, createMarkersQuickPick } from '../contrib/chatDynamicVariables.js';
import { createFolderQuickPick } from '../contrib/chatDynamicVariables.js';
import { convertBufferToScreenshotVariable, ScreenshotVariableId } from '../contrib/screenshot.js';
import { resizeImage } from '../imageUtils.js';
import { INSTRUCTIONS_COMMAND_ID } from '../promptSyntax/contributions/attachInstructionsCommand.js';
import { CHAT_CATEGORY } from './chatActions.js';
import { runAttachInstructionsAction, registerPromptActions } from './promptActions/index.js';
import { runAttachInstructionsAction } from './promptActions/index.js';
export function registerChatContextActions() {
registerAction2(AttachContextAction);
@@ -76,9 +79,33 @@ export function registerChatContextActions() {
/**
* We fill the quickpick with these types, and enable some quick access providers
*/
type IAttachmentQuickPickItem = ICommandVariableQuickPickItem | IQuickAccessQuickPickItem | IToolQuickPickItem |
IImageQuickPickItem | IOpenEditorsQuickPickItem | ISearchResultsQuickPickItem |
IScreenShotQuickPickItem | IRelatedFilesQuickPickItem | IInstructionsQuickPickItem | IFolderQuickPickItem | IDiagnosticsQuickPickItem;
type IAttachmentQuickPickItem = ICommandVariableQuickPickItem | IQuickAccessQuickPickItem
| IToolsQuickPickItem | IToolQuickPickItem
| IImageQuickPickItem | IOpenEditorsQuickPickItem | ISearchResultsQuickPickItem
| IScreenShotQuickPickItem | IRelatedFilesQuickPickItem | IInstructionsQuickPickItem
| IFolderQuickPickItem | IFolderResultQuickPickItem
| IDiagnosticsQuickPickItem | IDiagnosticsQuickPickItemWithFilter;
function isIAttachmentQuickPickItem(obj: unknown): obj is IAttachmentQuickPickItem {
return (
typeof obj === 'object'
&& obj !== null
&& typeof (<IAttachmentQuickPickItem>obj).kind === 'string'
);
}
const attachmentsOrdinals: (IAttachmentQuickPickItem['kind'])[] = [
// bottom-most
'tools',
'screenshot',
'image',
'quickaccess',
'diagnostic',
'instructions',
'folder',
'open-editors',
// top-most
];
/**
* These are the types that we can get out of the quick pick
@@ -100,18 +127,6 @@ function isISymbolQuickPickItem(obj: unknown): obj is ISymbolQuickPickItem {
&& !!(obj as ISymbolQuickPickItem).symbol);
}
function isIFolderSearchResultQuickPickItem(obj: unknown): obj is IFolderResultQuickPickItem {
return (
typeof obj === 'object'
&& (obj as IFolderResultQuickPickItem).kind === 'folder-search-result');
}
function isIDiagnosticsQuickPickItemWithFilter(obj: unknown): obj is IDiagnosticsQuickPickItemWithFilter {
return (
typeof obj === 'object'
&& (obj as IDiagnosticsQuickPickItemWithFilter).kind === 'diagnostic-filter');
}
function isIQuickPickItemWithResource(obj: unknown): obj is IQuickPickItemWithResource {
return (
typeof obj === 'object'
@@ -119,40 +134,11 @@ function isIQuickPickItemWithResource(obj: unknown): obj is IQuickPickItemWithRe
&& URI.isUri((obj as IQuickPickItemWithResource).resource));
}
function isIOpenEditorsQuickPickItem(obj: unknown): obj is IOpenEditorsQuickPickItem {
return (
typeof obj === 'object'
&& (obj as IOpenEditorsQuickPickItem).id === 'open-editors');
}
function isISearchResultsQuickPickItem(obj: unknown): obj is ISearchResultsQuickPickItem {
return (
typeof obj === 'object'
&& (obj as ISearchResultsQuickPickItem).kind === 'search-results');
}
function isScreenshotQuickPickItem(obj: unknown): obj is IScreenShotQuickPickItem {
return (
typeof obj === 'object'
&& (obj as IScreenShotQuickPickItem).kind === 'screenshot');
}
function isRelatedFileQuickPickItem(obj: unknown): obj is IRelatedFilesQuickPickItem {
return (
typeof obj === 'object'
&& (obj as IRelatedFilesQuickPickItem).kind === 'related-files'
);
}
/**
* Checks is a provided object is a prompt instructions quick pick item.
*/
function isPromptInstructionsQuickPickItem(obj: unknown): obj is IInstructionsQuickPickItem {
if (!obj || typeof obj !== 'object') {
return false;
}
return ('kind' in obj && obj.kind === INSTRUCTION_PICK_ID);
interface IToolsQuickPickItem extends IQuickPickItem {
kind: 'tools';
id: string;
label: string;
}
interface IRelatedFilesQuickPickItem extends IQuickPickItem {
@@ -184,7 +170,6 @@ interface ICommandVariableQuickPickItem extends IQuickPickItem {
command: Command;
name?: string;
value: unknown;
icon?: ThemeIcon;
}
@@ -193,6 +178,7 @@ interface IToolQuickPickItem extends IQuickPickItem {
id: string;
name?: string;
icon?: ThemeIcon;
tool: IToolData;
}
interface IQuickAccessQuickPickItem extends IQuickPickItem {
@@ -444,7 +430,7 @@ export class AttachSearchResultAction extends Action2 {
}
let insertText = `#${AttachSearchResultAction.Name}`;
const varRange = new Range(originalRange.startLineNumber, originalRange.startColumn, originalRange.endLineNumber, originalRange.startColumn + insertText.length);
const varRange = new Range(originalRange.startLineNumber, originalRange.startColumn, originalRange.endLineNumber, originalRange.startLineNumber + insertText.length);
// check character before the start of the range. If it's not a space, add a space
const model = editor.getModel();
if (model && model.getValueInRange(new Range(originalRange.startLineNumber, originalRange.startColumn - 1, originalRange.startLineNumber, originalRange.startColumn)) !== ' ') {
@@ -504,7 +490,122 @@ export class AttachContextAction extends Action2 {
const toAttach: IChatRequestVariableEntry[] = [];
for (const pick of picks) {
if (isISymbolQuickPickItem(pick) && pick.symbol) {
if (isIAttachmentQuickPickItem(pick)) {
if (pick.kind === 'folder-search-result') {
toAttach.push({
kind: 'directory',
id: pick.id,
value: pick.resource,
name: basename(pick.resource),
});
} else if (pick.kind === 'diagnostic-filter') {
toAttach.push({
id: pick.id,
name: pick.label,
value: pick.filter,
kind: 'diagnostic',
icon: pick.icon,
...pick.filter,
});
} else if (pick.kind === 'open-editors') {
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) {
toAttach.push({
kind: 'file',
id: this._getFileContextId({ resource: uri }),
value: uri,
name: labelService.getUriBasenameLabel(uri),
});
}
}
} else if (pick.kind === 'search-results') {
const searchView = viewsService.getViewWithId(SEARCH_VIEW_ID) as SearchView;
for (const result of searchView.model.searchResult.matches()) {
toAttach.push({
kind: 'file',
id: this._getFileContextId({ resource: result.resource }),
value: result.resource,
name: labelService.getUriBasenameLabel(result.resource),
});
}
} else if (pick.kind === 'related-files') {
// Get all provider results and show them in a second tier picker
const chatSessionId = widget.viewModel?.sessionId;
if (!chatSessionId || !chatEditingService) {
continue;
}
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(), 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) {
acc.push({
type: 'item',
label: labelService.getUriBasenameLabel(file.uri),
description: labelService.getUriLabel(dirname(file.uri), { relative: true }),
value: file.uri,
disabled: attachments.has(this._getFileContextId({ resource: file.uri })),
picked: true
});
}
return acc;
}, []));
const selectedFiles = await quickInputService.pick(itemsPromise, { placeHolder: localize('relatedFiles', 'Add related files to your working set'), canPickMany: true });
for (const file of selectedFiles ?? []) {
toAttach.push({
kind: 'file',
id: this._getFileContextId({ resource: file.value }),
value: file.value,
name: file.label,
omittedState: OmittedState.NotOmitted
});
}
} else if (pick.kind === 'screenshot') {
const blob = await hostService.getScreenshot();
if (blob) {
toAttach.push(convertBufferToScreenshotVariable(blob));
}
} else if (pick.kind === 'command') {
// Dynamic variable with a followup command
const selection = await commandService.executeCommand(pick.command.id, ...(pick.command.arguments ?? []));
if (!selection) {
// User made no selection, skip this variable
continue;
}
toAttach.push({
...pick,
value: pick.value,
name: `${typeof pick.value === 'string' && pick.value.startsWith('#') ? pick.value.slice(1) : ''}${selection}`,
// Apply the original icon with the new name
fullName: selection
});
} else if (pick.kind === 'tool') {
toAttach.push({
id: pick.id,
name: pick.tool.displayName,
fullName: pick.tool.displayName,
value: undefined,
icon: pick.icon,
kind: 'tool'
});
} else if (pick.kind === 'image') {
const fileBuffer = await clipboardService.readImage();
toAttach.push({
id: await imageToHash(fileBuffer),
name: localize('pastedImage', 'Pasted Image'),
fullName: localize('pastedImage', 'Pasted Image'),
value: fileBuffer,
kind: 'image',
});
}
} else if (isISymbolQuickPickItem(pick) && pick.symbol) {
// Workspace symbol
toAttach.push({
kind: 'symbol',
@@ -515,24 +616,6 @@ export class AttachContextAction extends Action2 {
fullName: pick.label,
name: pick.symbol.name,
});
} else if (isIFolderSearchResultQuickPickItem(pick)) {
const folder = pick.resource;
toAttach.push({
kind: 'directory',
id: pick.id,
value: folder,
name: basename(folder),
});
} else if (isIDiagnosticsQuickPickItemWithFilter(pick)) {
toAttach.push({
id: pick.id,
name: pick.label,
value: pick.filter,
kind: 'diagnostic',
icon: pick.icon,
...pick.filter,
});
} else if (isIQuickPickItemWithResource(pick) && pick.resource) {
if (/\.(png|jpg|jpeg|bmp|gif|tiff)$/i.test(pick.resource.path)) {
// checks if the file is an image
@@ -573,107 +656,6 @@ export class AttachContextAction extends Action2 {
fullName: pick.label,
name: pick.symbolName!,
});
} else if (isIOpenEditorsQuickPickItem(pick)) {
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) {
toAttach.push({
kind: 'file',
id: this._getFileContextId({ resource: uri }),
value: uri,
name: labelService.getUriBasenameLabel(uri),
});
}
}
} else if (isISearchResultsQuickPickItem(pick)) {
const searchView = viewsService.getViewWithId(SEARCH_VIEW_ID) as SearchView;
for (const result of searchView.model.searchResult.matches()) {
toAttach.push({
kind: 'file',
id: this._getFileContextId({ resource: result.resource }),
value: result.resource,
name: labelService.getUriBasenameLabel(result.resource),
});
}
} else if (isRelatedFileQuickPickItem(pick)) {
// Get all provider results and show them in a second tier picker
const chatSessionId = widget.viewModel?.sessionId;
if (!chatSessionId || !chatEditingService) {
continue;
}
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(), 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) {
acc.push({
type: 'item',
label: labelService.getUriBasenameLabel(file.uri),
description: labelService.getUriLabel(dirname(file.uri), { relative: true }),
value: file.uri,
disabled: attachments.has(this._getFileContextId({ resource: file.uri })),
picked: true
});
}
return acc;
}, []));
const selectedFiles = await quickInputService.pick(itemsPromise, { placeHolder: localize('relatedFiles', 'Add related files to your working set'), canPickMany: true });
for (const file of selectedFiles ?? []) {
toAttach.push({
kind: 'file',
id: this._getFileContextId({ resource: file.value }),
value: file.value,
name: file.label,
omittedState: OmittedState.NotOmitted
});
}
} else if (isScreenshotQuickPickItem(pick)) {
const blob = await hostService.getScreenshot();
if (blob) {
toAttach.push(convertBufferToScreenshotVariable(blob));
}
} else if (isPromptInstructionsQuickPickItem(pick)) {
await runAttachInstructionsAction({ widget }, commandService);
} else {
// Anything else is an attachment
const attachmentPick = pick as IAttachmentQuickPickItem;
if (attachmentPick.kind === 'command') {
// Dynamic variable with a followup command
const selection = await commandService.executeCommand(attachmentPick.command.id, ...(attachmentPick.command.arguments ?? []));
if (!selection) {
// User made no selection, skip this variable
continue;
}
toAttach.push({
...attachmentPick,
value: attachmentPick.value,
name: `${typeof attachmentPick.value === 'string' && attachmentPick.value.startsWith('#') ? attachmentPick.value.slice(1) : ''}${selection}`,
// Apply the original icon with the new name
fullName: selection
});
} else if (attachmentPick.kind === 'tool') {
toAttach.push({
id: attachmentPick.id,
name: attachmentPick.label,
fullName: attachmentPick.label,
value: undefined,
icon: attachmentPick.icon,
kind: 'tool'
});
} else if (attachmentPick.kind === 'image') {
const fileBuffer = await clipboardService.readImage();
toAttach.push({
id: await imageToHash(fileBuffer),
name: localize('pastedImage', 'Pasted Image'),
fullName: localize('pastedImage', 'Pasted Image'),
value: fileBuffer,
kind: 'image',
});
}
}
}
@@ -694,13 +676,13 @@ export class AttachContextAction extends Action2 {
const extensionService = accessor.get(IExtensionService);
const instantiationService = accessor.get(IInstantiationService);
const keybindingService = accessor.get(IKeybindingService);
const chatEditingService = accessor.get(IChatEditingService);
const context: { widget?: IChatWidget; showFilesOnly?: boolean; placeholder?: string } | undefined = args[0];
const widget = context?.widget ?? widgetService.lastFocusedWidget;
if (!widget) {
return;
}
const chatEditingService = accessor.get(IChatEditingService);
const quickPickItems: IAttachmentQuickPickItem[] = [];
if (extensionService.extensions.some(ext => isProposedApiEnabled(ext, 'chatReferenceBinaryData'))) {
@@ -748,27 +730,16 @@ export class AttachContextAction extends Action2 {
}
}
for (const tool of widget.input.selectedToolsModel.tools.get()) {
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: 'tools',
label: localize('chatContext.tools', 'Tools...'),
iconClass: ThemeIcon.asClassName(Codicon.tools),
id: 'tools',
});
quickPickItems.push({
kind: 'quickaccess',
label: localize('chatContext.symbol', 'Symbol...'),
label: localize('chatContext.symbol', 'Symbols...'),
iconClass: ThemeIcon.asClassName(Codicon.symbolField),
prefix: SymbolsQuickAccessProvider.PREFIX,
id: 'symbol'
@@ -776,14 +747,14 @@ export class AttachContextAction extends Action2 {
quickPickItems.push({
kind: 'folder',
label: localize('chatContext.folder', 'Folder...'),
label: localize('chatContext.folder', 'Folders...'),
iconClass: ThemeIcon.asClassName(Codicon.folder),
id: 'folder',
});
quickPickItems.push({
kind: 'diagnostic',
label: localize('chatContext.diagnstic', 'Problem...'),
label: localize('chatContext.diagnstic', 'Problems...'),
iconClass: ThemeIcon.asClassName(Codicon.error),
id: 'diagnostic'
});
@@ -845,24 +816,15 @@ export class AttachContextAction extends Action2 {
});
}
function extractTextFromIconLabel(label: string | undefined): string {
if (!label) {
return '';
quickPickItems.sort((a, b) => {
let result = attachmentsOrdinals.indexOf(b.kind) - attachmentsOrdinals.indexOf(a.kind);
if (result === 0) {
result = a.label.localeCompare(b.label);
}
const match = label.match(/\$\([^\)]+\)\s*(.+)/);
return match ? match[1] : label;
}
return result;
});
instantiationService.invokeFunction(this._show.bind(this), widget, 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();
return compare(first, second);
}), '', context?.placeholder);
instantiationService.invokeFunction(this._show.bind(this), widget, quickPickItems, '', context?.placeholder);
}
private async _showDiagnosticsPick(instantiationService: IInstantiationService, onBackgroundAccept: (item: IChatContextQuickPickItem[]) => void): Promise<IDiagnosticsQuickPickItemWithFilter | undefined> {
@@ -874,16 +836,15 @@ export class AttachContextAction extends Action2 {
filter: item,
});
const filter = await instantiationService.invokeFunction(accessor =>
createMarkersQuickPick(accessor, 'problem', items => onBackgroundAccept(items.map(convert))));
const filter = await instantiationService.invokeFunction(createMarkersQuickPick, items => onBackgroundAccept(items.map(convert)));
return filter && convert(filter);
}
private _show(accessor: ServicesAccessor, widget: IChatWidget, quickPickItems: (IChatContextQuickPickItem | QuickPickItem)[] | undefined, query: string = '', placeholder?: string) {
const quickInputService = accessor.get(IQuickInputService);
const quickChatService = accessor.get(IQuickChatService);
const clipboardService = accessor.get(IClipboardService);
const editorService = accessor.get(IEditorService);
const commandService = accessor.get(ICommandService);
const instantiationService = accessor.get(IInstantiationService);
const attach = (isBackgroundAccept: boolean, ...items: IChatContextQuickPickItem[]) => {
@@ -891,37 +852,45 @@ export class AttachContextAction extends Action2 {
};
const providerOptions: AnythingQuickAccessProviderRunOptions = {
additionPicks: quickPickItems,
handleAccept: async (inputItem: IChatContextQuickPickItem, isBackgroundAccept: boolean) => {
let item: IChatContextQuickPickItem | undefined = inputItem;
if ('kind' in item && item.kind === 'folder') {
item = await this._showFolders(instantiationService);
} else if ('kind' in item && item.kind === 'diagnostic') {
item = await this._showDiagnosticsPick(instantiationService, i => attach(true, ...i));
}
if (!item) {
instantiationService.invokeFunction(this._show.bind(this), widget, quickPickItems, '', placeholder);
return;
}
if (isIAttachmentQuickPickItem(item)) {
if ('prefix' in item) {
instantiationService.invokeFunction(this._show.bind(this), widget, quickPickItems, item.prefix, placeholder);
} else {
if (!clipboardService) {
if (item.kind === 'quickaccess') {
instantiationService.invokeFunction(this._show.bind(this), widget, quickPickItems, item.prefix, placeholder);
return;
} else if (item.kind === 'instructions') {
runAttachInstructionsAction(commandService, { widget });
return;
}
attach(isBackgroundAccept, item);
if (isQuickChat(widget)) {
quickChatService.open();
if (item.kind === 'folder') {
item = await this._showFolders(instantiationService);
} else if (item.kind === 'diagnostic') {
item = await this._showDiagnosticsPick(instantiationService, i => attach(true, ...i));
} else if (item.kind === 'tools') {
item = await instantiationService.invokeFunction(showToolsPick, widget);
}
if (!item) {
// restart picker when sub-picker didn't return anything
instantiationService.invokeFunction(this._show.bind(this), widget, quickPickItems, '', placeholder);
return;
}
}
attach(isBackgroundAccept, item);
if (isQuickChat(widget)) {
quickChatService.open();
}
},
additionPicks: quickPickItems,
filter: (item: IChatContextQuickPickItem | IQuickPickSeparator) => {
// Avoid attaching the same context twice
const attachedContext = widget.attachmentModel.getAttachmentIDs();
if (isIOpenEditorsQuickPickItem(item)) {
if (isIAttachmentQuickPickItem(item) && item.kind === 'open-editors') {
for (const editor of editorService.editors.filter(e => e instanceof FileEditorInput || e instanceof DiffEditorInput || e instanceof UntitledTextEditorInput)) {
// There is an open editor that hasn't yet been attached to the chat
if (editor.resource && !attachedContext.has(this._getFileContextId({ resource: editor.resource }))) {
@@ -982,7 +951,137 @@ export class AttachContextAction extends Action2 {
}
}
/**
* Register all actions related to reusable prompt files.
*/
registerPromptActions();
async function createMarkersQuickPick(accessor: ServicesAccessor, onBackgroundAccept?: (item: IDiagnosticVariableEntryFilterData[]) => void): Promise<IDiagnosticVariableEntryFilterData | undefined> {
const quickInputService = accessor.get(IQuickInputService);
const markerService = accessor.get(IMarkerService);
const labelService = accessor.get(ILabelService);
const markers = markerService.read({ severities: MarkerSeverity.Error | MarkerSeverity.Warning | MarkerSeverity.Info });
const grouped = groupBy(markers, (a, b) => extUri.compare(a.resource, b.resource));
const severities = new Set<MarkerSeverity>();
type MarkerPickItem = IQuickPickItem & { resource?: URI; entry: IDiagnosticVariableEntryFilterData };
const items: (MarkerPickItem | IQuickPickSeparator)[] = [];
let pickCount = 0;
for (const group of grouped) {
const resource = group[0].resource;
items.push({ type: 'separator', label: labelService.getUriLabel(resource, { relative: true }) });
for (const marker of group) {
pickCount++;
severities.add(marker.severity);
items.push({
type: 'item',
resource: marker.resource,
label: marker.message,
description: localize('markers.panel.at.ln.col.number', "[Ln {0}, Col {1}]", '' + marker.startLineNumber, '' + marker.startColumn),
entry: IDiagnosticVariableEntryFilterData.fromMarker(marker),
});
}
}
items.unshift({ type: 'item', label: localize('markers.panel.allErrors', 'All Problems'), entry: { filterSeverity: MarkerSeverity.Info } });
const store = new DisposableStore();
const quickPick = store.add(quickInputService.createQuickPick<MarkerPickItem>({ useSeparators: true }));
quickPick.canAcceptInBackground = !onBackgroundAccept;
quickPick.placeholder = localize('pickAProblem', 'Pick a problem to attach...');
quickPick.items = items;
return new Promise<IDiagnosticVariableEntryFilterData | undefined>(resolve => {
store.add(quickPick.onDidHide(() => resolve(undefined)));
store.add(quickPick.onDidAccept(ev => {
if (ev.inBackground) {
onBackgroundAccept?.(quickPick.selectedItems.map(i => i.entry));
} else {
resolve(quickPick.selectedItems[0]?.entry);
quickPick.dispose();
}
}));
quickPick.show();
}).finally(() => store.dispose());
}
async function showToolsPick(accessor: ServicesAccessor, widget: IChatWidget): Promise<IToolQuickPickItem | undefined> {
const quickPickService = accessor.get(IQuickInputService);
function classify(tool: IToolData) {
if (tool.source.type === 'extension') {
if (!tool.source.isExternalTool) {
return { ordinal: 1, groupLabel: tool.source.label };
} else {
return { ordinal: 3, groupLabel: localize('chatContext.tools.extension', 'Extensions') };
}
} else if (tool.source.type === 'mcp') {
return { ordinal: 2, groupLabel: localize('chatContext.tools.mcp', 'MCP Servers') };
} else {
return { ordinal: 4, groupLabel: '' };
}
}
type Pick = IToolQuickPickItem & { ordinal: number; groupLabel: string };
const items: Pick[] = [];
for (const tool of widget.input.selectedToolsModel.tools.get()) {
if (!tool.canBeReferencedInPrompt) {
continue;
}
const item: Pick = {
tool,
...classify(tool),
kind: 'tool',
label: tool.toolReferenceName ?? tool.id,
description: (tool.toolReferenceName ?? tool.id) !== tool.displayName ? tool.displayName : undefined,
id: tool.id,
icon: ThemeIcon.isThemeIcon(tool.icon) ? tool.icon : undefined
};
if (ThemeIcon.isThemeIcon(tool.icon)) {
item.iconClass = ThemeIcon.asClassName(tool.icon);
} else if (tool.icon) {
item.iconPath = tool.icon;
}
items.push(item);
}
items.sort((a, b) => {
let res = a.ordinal - b.ordinal;
if (res !== 0) {
return res;
}
let strA = a.tool.source.type !== 'internal' ? a.tool.source.label : '';
let strB = b.tool.source.type !== 'internal' ? b.tool.source.label : '';
res = strA.localeCompare(strB);
if (res !== 0) {
return res;
}
strA = a.label;
strB = b.label;
return strA.localeCompare(strB);
});
let lastGroupLabel: string | undefined;
const picks: (IQuickPickSeparator | Pick)[] = [];
for (const item of items) {
if (lastGroupLabel !== item.groupLabel) {
picks.push({ type: 'separator', label: item.groupLabel });
lastGroupLabel = item.groupLabel;
}
picks.push(item);
}
const result = await quickPickService.pick(picks, {
placeHolder: localize('chatContext.tools.placeholder', 'Select a Tool'),
canPickMany: false
});
return result;
}

View File

@@ -127,8 +127,8 @@ class AttachInstructionsAction extends Action2 {
* encapsulate/enforce the correct options to be passed to the action.
*/
export const runAttachInstructionsAction = async (
options: IAttachInstructionsActionOptions,
commandService: ICommandService,
options: IAttachInstructionsActionOptions,
): Promise<void> => {
return await commandService.executeCommand(
ATTACH_INSTRUCTIONS_ACTION_ID,

View File

@@ -3,8 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { coalesce, groupBy } from '../../../../../base/common/arrays.js';
import { assertNever } from '../../../../../base/common/assert.js';
import { coalesce } from '../../../../../base/common/arrays.js';
import { CancellationToken } from '../../../../../base/common/cancellation.js';
import { Codicon } from '../../../../../base/common/codicons.js';
import { isCancellationError } from '../../../../../base/common/errors.js';
@@ -18,20 +17,16 @@ import { URI } from '../../../../../base/common/uri.js';
import { IRange, Range } from '../../../../../editor/common/core/range.js';
import { IDecorationOptions } from '../../../../../editor/common/editorCommon.js';
import { Command, isLocation } from '../../../../../editor/common/languages.js';
import { localize } from '../../../../../nls.js';
import { Action2, registerAction2 } from '../../../../../platform/actions/common/actions.js';
import { ICommandService } from '../../../../../platform/commands/common/commands.js';
import { IConfigurationService } from '../../../../../platform/configuration/common/configuration.js';
import { FileType, IFileService } from '../../../../../platform/files/common/files.js';
import { IInstantiationService, ServicesAccessor } from '../../../../../platform/instantiation/common/instantiation.js';
import { ILabelService } from '../../../../../platform/label/common/label.js';
import { IMarkerService, MarkerSeverity } from '../../../../../platform/markers/common/markers.js';
import { PromptsConfig } from '../../../../../platform/prompts/common/config.js';
import { IQuickInputService, IQuickPickItem, IQuickPickSeparator } from '../../../../../platform/quickinput/common/quickInput.js';
import { IUriIdentityService } from '../../../../../platform/uriIdentity/common/uriIdentity.js';
import { IQuickInputService, IQuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js';
import { IWorkspaceContextService } from '../../../../../platform/workspace/common/workspace.js';
import { getExcludes, IFileQuery, ISearchComplete, ISearchConfiguration, ISearchService, QueryType } from '../../../../services/search/common/search.js';
import { IDiagnosticVariableEntryFilterData } from '../../common/chatModel.js';
import { IChatRequestVariableValue, IDynamicVariable } from '../../common/chatVariables.js';
import { IChatWidget } from '../chat.js';
import { ChatWidget, IChatWidgetContrib } from '../chatWidget.js';
@@ -500,82 +495,3 @@ export class AddDynamicVariableAction extends Action2 {
}
}
registerAction2(AddDynamicVariableAction);
export async function createMarkersQuickPick(accessor: ServicesAccessor, level: 'problem' | 'file', onBackgroundAccept?: (item: IDiagnosticVariableEntryFilterData[]) => void): Promise<IDiagnosticVariableEntryFilterData | undefined> {
const markers = accessor.get(IMarkerService).read({ severities: MarkerSeverity.Error | MarkerSeverity.Warning | MarkerSeverity.Info });
if (!markers.length) {
return;
}
const uriIdentityService = accessor.get(IUriIdentityService);
const labelService = accessor.get(ILabelService);
const grouped = groupBy(markers, (a, b) => uriIdentityService.extUri.compare(a.resource, b.resource));
const severities = new Set<MarkerSeverity>();
type MarkerPickItem = IQuickPickItem & { resource?: URI; entry: IDiagnosticVariableEntryFilterData };
const items: (MarkerPickItem | IQuickPickSeparator)[] = [];
let pickCount = 0;
for (const group of grouped) {
const resource = group[0].resource;
if (level === 'problem') {
items.push({ type: 'separator', label: labelService.getUriLabel(resource, { relative: true }) });
for (const marker of group) {
pickCount++;
severities.add(marker.severity);
items.push({
type: 'item',
resource: marker.resource,
label: marker.message,
description: localize('markers.panel.at.ln.col.number', "[Ln {0}, Col {1}]", '' + marker.startLineNumber, '' + marker.startColumn),
entry: IDiagnosticVariableEntryFilterData.fromMarker(marker),
});
}
} else if (level === 'file') {
const entry = { filterUri: resource };
pickCount++;
items.push({
type: 'item',
resource,
label: IDiagnosticVariableEntryFilterData.label(entry),
description: group[0].message + (group.length > 1 ? localize('problemsMore', '+ {0} more', group.length - 1) : ''),
entry,
});
for (const marker of group) {
severities.add(marker.severity);
}
} else {
assertNever(level);
}
}
if (pickCount < 2) { // single error in a URI
return items.find((i): i is MarkerPickItem => i.type === 'item')?.entry;
}
if (level === 'file') {
items.unshift({ type: 'separator', label: localize('markers.panel.files', 'Files') });
}
items.unshift({ type: 'item', label: localize('markers.panel.allErrors', 'All Problems'), entry: { filterSeverity: MarkerSeverity.Info } });
const quickInputService = accessor.get(IQuickInputService);
const store = new DisposableStore();
const quickPick = store.add(quickInputService.createQuickPick<MarkerPickItem>({ useSeparators: true }));
quickPick.canAcceptInBackground = !onBackgroundAccept;
quickPick.placeholder = localize('pickAProblem', 'Pick a problem to attach...');
quickPick.items = items;
return new Promise<IDiagnosticVariableEntryFilterData | undefined>(resolve => {
store.add(quickPick.onDidHide(() => resolve(undefined)));
store.add(quickPick.onDidAccept(ev => {
if (ev.inBackground) {
onBackgroundAccept?.(quickPick.selectedItems.map(i => i.entry));
} else {
resolve(quickPick.selectedItems[0]?.entry);
quickPick.dispose();
}
}));
quickPick.show();
}).finally(() => store.dispose());
}

View File

@@ -54,10 +54,10 @@ const command = async (
): Promise<void> => {
const commandService = accessor.get(ICommandService);
await runAttachInstructionsAction({
await runAttachInstructionsAction(commandService, {
resource: getActiveInstructionsFileUri(accessor),
widget: getFocusedChatWidget(accessor),
}, commandService);
});
};
/**

View File

@@ -53,8 +53,8 @@ export type ToolDataSource =
}
| {
type: 'mcp';
label: string; collectionId:
string;
label: string;
collectionId: string;
definitionId: string;
}
| { type: 'internal' };