|
|
|
|
@@ -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;
|
|
|
|
|
}
|
|
|
|
|
|