diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index 0228bc59274..06f95c8f2f0 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -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 (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 | 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 | 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 { @@ -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 { + 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(); + 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({ useSeparators: true })); + quickPick.canAcceptInBackground = !onBackgroundAccept; + quickPick.placeholder = localize('pickAProblem', 'Pick a problem to attach...'); + quickPick.items = items; + + return new Promise(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 { + + 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; +} diff --git a/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatAttachInstructionsAction.ts b/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatAttachInstructionsAction.ts index 5dac781c59c..1df01e8afcd 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatAttachInstructionsAction.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/promptActions/chatAttachInstructionsAction.ts @@ -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 => { return await commandService.executeCommand( ATTACH_INSTRUCTIONS_ACTION_ID, diff --git a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts index 71a78502cdb..f601d70bedb 100644 --- a/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/contrib/chatDynamicVariables.ts @@ -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 { - 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(); - 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({ useSeparators: true })); - quickPick.canAcceptInBackground = !onBackgroundAccept; - quickPick.placeholder = localize('pickAProblem', 'Pick a problem to attach...'); - quickPick.items = items; - - return new Promise(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()); -} diff --git a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/attachInstructionsCommand.ts b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/attachInstructionsCommand.ts index befb1787f84..d6a8b99f892 100644 --- a/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/attachInstructionsCommand.ts +++ b/src/vs/workbench/contrib/chat/browser/promptSyntax/contributions/attachInstructionsCommand.ts @@ -54,10 +54,10 @@ const command = async ( ): Promise => { const commandService = accessor.get(ICommandService); - await runAttachInstructionsAction({ + await runAttachInstructionsAction(commandService, { resource: getActiveInstructionsFileUri(accessor), widget: getFocusedChatWidget(accessor), - }, commandService); + }); }; /** diff --git a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts index c123f290cb8..f093b0218fe 100644 --- a/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts +++ b/src/vs/workbench/contrib/chat/common/languageModelToolsService.ts @@ -53,8 +53,8 @@ export type ToolDataSource = } | { type: 'mcp'; - label: string; collectionId: - string; + label: string; + collectionId: string; definitionId: string; } | { type: 'internal' };