diff --git a/src/vs/workbench/api/common/extHost.api.impl.ts b/src/vs/workbench/api/common/extHost.api.impl.ts index 3b3c6f87b22..04372c16964 100644 --- a/src/vs/workbench/api/common/extHost.api.impl.ts +++ b/src/vs/workbench/api/common/extHost.api.impl.ts @@ -1411,9 +1411,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I checkProposedApiEnabled(extension, 'chatProvider'); return extHostLanguageModels.registerLanguageModel(extension, id, provider, metadata); }, - registerChatVariableResolver(id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: vscode.ChatVariableResolver) { + registerChatVariableResolver(id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: vscode.ChatVariableResolver, fullName?: string, icon?: vscode.ThemeIcon) { checkProposedApiEnabled(extension, 'chatVariableResolver'); - return extHostChatVariables.registerVariableResolver(extension, id, name, userDescription, modelDescription, isSlow, resolver); + return extHostChatVariables.registerVariableResolver(extension, id, name, userDescription, modelDescription, isSlow, resolver, fullName, icon?.id); }, registerMappedEditsProvider(selector: vscode.DocumentSelector, provider: vscode.MappedEditsProvider) { checkProposedApiEnabled(extension, 'mappedEditsProvider'); diff --git a/src/vs/workbench/api/common/extHostChatVariables.ts b/src/vs/workbench/api/common/extHostChatVariables.ts index 43a9dcceb53..dfc37201bd4 100644 --- a/src/vs/workbench/api/common/extHostChatVariables.ts +++ b/src/vs/workbench/api/common/extHostChatVariables.ts @@ -6,6 +6,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { onUnexpectedExternalError } from 'vs/base/common/errors'; import { IDisposable, toDisposable } from 'vs/base/common/lifecycle'; +import { ThemeIcon } from 'vs/base/common/themables'; import { IExtensionDescription } from 'vs/platform/extensions/common/extensions'; import { ExtHostChatVariablesShape, IChatVariableResolverProgressDto, IMainContext, MainContext, MainThreadChatVariablesShape } from 'vs/workbench/api/common/extHost.protocol'; import * as typeConvert from 'vs/workbench/api/common/extHostTypeConverters'; @@ -52,10 +53,11 @@ export class ExtHostChatVariables implements ExtHostChatVariablesShape { return undefined; } - registerVariableResolver(extension: IExtensionDescription, id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: vscode.ChatVariableResolver): IDisposable { + registerVariableResolver(extension: IExtensionDescription, id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: vscode.ChatVariableResolver, fullName?: string, themeIconId?: string): IDisposable { const handle = ExtHostChatVariables._idPool++; - this._resolver.set(handle, { extension, data: { id, name, description: userDescription, modelDescription }, resolver: resolver }); - this._proxy.$registerVariable(handle, { id, name, description: userDescription, modelDescription, isSlow }); + const icon = themeIconId ? ThemeIcon.fromId(themeIconId) : undefined; + this._resolver.set(handle, { extension, data: { id, name, description: userDescription, modelDescription, icon }, resolver: resolver }); + this._proxy.$registerVariable(handle, { id, name, description: userDescription, modelDescription, isSlow, fullName, icon }); return toDisposable(() => { this._resolver.delete(handle); diff --git a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts index f9e94661e10..a6516a796de 100644 --- a/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts +++ b/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts @@ -6,6 +6,7 @@ import { Codicon } from 'vs/base/common/codicons'; import { KeyCode, KeyMod } from 'vs/base/common/keyCodes'; import { Schemas } from 'vs/base/common/network'; +import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { ServicesAccessor } from 'vs/editor/browser/editorExtensions'; import { localize, localize2 } from 'vs/nls'; @@ -37,7 +38,7 @@ class AttachContextAction extends Action2 { category: CHAT_CATEGORY, keybinding: { when: CONTEXT_IN_CHAT_INPUT, - primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyA, + primary: KeyMod.CtrlCmd | KeyCode.Slash, weight: KeybindingWeight.EditorContrib }, menu: [ @@ -60,14 +61,20 @@ class AttachContextAction extends Action2 { const chatVariablesService = accessor.get(IChatVariablesService); const widgetService = accessor.get(IChatWidgetService); - const quickPickItems: QuickPickItem[] = []; + const quickPickItems: (QuickPickItem & { name?: string; icon?: ThemeIcon })[] = []; + for (const variable of chatVariablesService.getVariables()) { + if (variable.fullName) { + quickPickItems.push({ label: `${variable.icon ? `$(${variable.icon.id}) ` : ''}${variable.fullName}`, name: variable.name, id: variable.id, icon: variable.icon }); + } + } + if (chatVariablesService.hasVariable(SelectAndInsertFileAction.Name)) { quickPickItems.push(SelectAndInsertFileAction.Item, { type: 'separator' }); } const picks = await quickInputService.quickAccess.pick('', { enabledProviderPrefixes: [AnythingQuickAccessProvider.PREFIX], - placeholder: localize('chatContext.searchFiles.placeholder', 'Search files by name'), + placeholder: localize('chatContext.attach.placeholder', 'Search attachments'), providerOptions: { additionPicks: quickPickItems, includeSymbols: false, @@ -84,7 +91,14 @@ class AttachContextAction extends Action2 { const context: { widget?: IChatWidget } | undefined = args[0]; const widget = context?.widget ?? widgetService.lastFocusedWidget; - widget?.attachContext(...picks.map((p) => ({ name: p.label, value: 'resource' in p && URI.isUri(p.resource) ? p.resource : undefined, id: 'resource' in p && URI.isUri(p.resource) ? `${SelectAndInsertFileAction.Name}:${p.resource.toString()}` : '' }))); + widget?.attachContext(...picks.map((p) => ({ + fullName: p.label, + icon: 'icon' in p && ThemeIcon.isThemeIcon(p.icon) ? p.icon : undefined, + name: 'name' in p && typeof p.name === 'string' ? p.name : p.label, + value: 'resource' in p && URI.isUri(p.resource) ? p.resource : undefined, + id: 'id' in p && typeof p.id === 'string' ? p.id : + 'resource' in p && URI.isUri(p.resource) ? `${SelectAndInsertFileAction.Name}:${p.resource.toString()}` : '' + }))); } } } diff --git a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts index 5f191e62655..17d6f47866a 100644 --- a/src/vs/workbench/contrib/chat/browser/chatInputPart.ts +++ b/src/vs/workbench/contrib/chat/browser/chatInputPart.ts @@ -441,7 +441,7 @@ export class ChatInputPart extends Disposable implements IHistoryNavigationWidge hidePath: true, }); } else { - label.setLabel(attachment.name); + label.setLabel(attachment.fullName ?? attachment.name); } const clearButton = new Button(widget, { supportIcons: true }); diff --git a/src/vs/workbench/contrib/chat/browser/chatVariables.ts b/src/vs/workbench/contrib/chat/browser/chatVariables.ts index 8474766e0e9..560f2f887e2 100644 --- a/src/vs/workbench/contrib/chat/browser/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/browser/chatVariables.ts @@ -30,7 +30,7 @@ export class ChatVariablesService implements IChatVariablesService { ) { } - async resolveVariables(prompt: IParsedChatRequest, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { + async resolveVariables(prompt: IParsedChatRequest, attachedContextVariables: IChatRequestVariableEntry[] | undefined, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { let resolvedVariables: IChatRequestVariableEntry[] = []; const jobs: Promise[] = []; @@ -58,6 +58,26 @@ export class ChatVariablesService implements IChatVariablesService { } }); + attachedContextVariables + ?.forEach((attachment, i) => { + const data = this._resolver.get(attachment.name.toLowerCase()); + if (data) { + const references: IChatContentReference[] = []; + const variableProgressCallback = (item: IChatVariableResolverProgress) => { + if (item.kind === 'reference') { + references.push(item); + return; + } + progress(item); + }; + jobs.push(data.resolver(prompt.text, '', model, variableProgressCallback, token).then(value => { + if (value) { + resolvedVariables[i] = { id: data.data.id, modelDescription: data.data.modelDescription, name: attachment.name, range: attachment.range, value, references }; + } + }).catch(onUnexpectedExternalError)); + } + }); + await Promise.allSettled(jobs); resolvedVariables = coalesce(resolvedVariables); diff --git a/src/vs/workbench/contrib/chat/browser/media/chat.css b/src/vs/workbench/contrib/chat/browser/media/chat.css index 5c4aef84471..97c1103e53d 100644 --- a/src/vs/workbench/contrib/chat/browser/media/chat.css +++ b/src/vs/workbench/contrib/chat/browser/media/chat.css @@ -489,7 +489,13 @@ align-items: center; } -.interactive-session .chat-attached-context .chat-attached-context-attachment .codicon { +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label-container .monaco-highlighted-label { + display: flex !important; + align-items: center !important; +} + +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-icon-label .monaco-button.codicon.codicon-close, +.interactive-session .chat-attached-context .chat-attached-context-attachment .monaco-button.codicon.codicon-close { color: var(--vscode-descriptionForeground); } diff --git a/src/vs/workbench/contrib/chat/common/chatModel.ts b/src/vs/workbench/contrib/chat/common/chatModel.ts index 6fe68717c3e..895c910ed24 100644 --- a/src/vs/workbench/contrib/chat/common/chatModel.ts +++ b/src/vs/workbench/contrib/chat/common/chatModel.ts @@ -25,6 +25,8 @@ import { IChatRequestVariableValue } from 'vs/workbench/contrib/chat/common/chat export interface IChatRequestVariableEntry { id: string; + fullName?: string; + icon?: ThemeIcon; name: string; modelDescription?: string; range?: IOffsetRange; diff --git a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts index 8acb4f44e8d..a106ee69435 100644 --- a/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts +++ b/src/vs/workbench/contrib/chat/common/chatServiceImpl.ts @@ -561,10 +561,7 @@ export class ChatService extends Disposable implements IChatService { const initVariableData: IChatRequestVariableData = { variables: [] }; request = model.addRequest(parsedRequest, initVariableData, attempt, agent, agentSlashCommandPart?.command); completeResponseCreated(); - const variableData = await this.chatVariablesService.resolveVariables(parsedRequest, model, progressCallback, token); - if (options?.attachedContext) { - variableData.variables.push(...options.attachedContext); - } + const variableData = await this.chatVariablesService.resolveVariables(parsedRequest, options?.attachedContext, model, progressCallback, token); request.variableData = variableData; const promptTextResult = getPromptText(request.message); diff --git a/src/vs/workbench/contrib/chat/common/chatVariables.ts b/src/vs/workbench/contrib/chat/common/chatVariables.ts index 602b899c56c..644cad67a79 100644 --- a/src/vs/workbench/contrib/chat/common/chatVariables.ts +++ b/src/vs/workbench/contrib/chat/common/chatVariables.ts @@ -5,17 +5,20 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable } from 'vs/base/common/lifecycle'; +import { ThemeIcon } from 'vs/base/common/themables'; import { URI } from 'vs/base/common/uri'; import { IRange } from 'vs/editor/common/core/range'; import { Location } from 'vs/editor/common/languages'; import { createDecorator } from 'vs/platform/instantiation/common/instantiation'; -import { IChatModel, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatModel, IChatRequestVariableData, IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatContentReference, IChatProgressMessage } from 'vs/workbench/contrib/chat/common/chatService'; export interface IChatVariableData { id: string; name: string; + icon?: ThemeIcon; + fullName?: string; description: string; modelDescription?: string; isSlow?: boolean; @@ -46,7 +49,7 @@ export interface IChatVariablesService { /** * Resolves all variables that occur in `prompt` */ - resolveVariables(prompt: IParsedChatRequest, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise; + resolveVariables(prompt: IParsedChatRequest, attachedContextVariables: IChatRequestVariableEntry[] | undefined, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise; resolveVariable(variableName: string, promptText: string, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise; } diff --git a/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts b/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts index 530752d4118..40f72512a6a 100644 --- a/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts +++ b/src/vs/workbench/contrib/chat/test/browser/chatVariables.test.ts @@ -48,7 +48,7 @@ suite('ChatVariables', function () { const resolveVariables = async (text: string) => { const result = parser.parseChatRequest('1', text); - return await service.resolveVariables(result, null!, () => { }, CancellationToken.None); + return await service.resolveVariables(result, undefined, null!, () => { }, CancellationToken.None); }; { diff --git a/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts b/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts index 902631c259a..d18f9b473df 100644 --- a/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts +++ b/src/vs/workbench/contrib/chat/test/common/mockChatVariables.ts @@ -5,7 +5,7 @@ import { CancellationToken } from 'vs/base/common/cancellation'; import { IDisposable } from 'vs/base/common/lifecycle'; -import { IChatModel, IChatRequestVariableData } from 'vs/workbench/contrib/chat/common/chatModel'; +import { IChatModel, IChatRequestVariableData, IChatRequestVariableEntry } from 'vs/workbench/contrib/chat/common/chatModel'; import { IParsedChatRequest } from 'vs/workbench/contrib/chat/common/chatParserTypes'; import { IChatRequestVariableValue, IChatVariableData, IChatVariableResolver, IChatVariableResolverProgress, IChatVariablesService, IDynamicVariable } from 'vs/workbench/contrib/chat/common/chatVariables'; @@ -31,7 +31,7 @@ export class MockChatVariablesService implements IChatVariablesService { return []; } - async resolveVariables(prompt: IParsedChatRequest, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { + async resolveVariables(prompt: IParsedChatRequest, attachedContextVariables: IChatRequestVariableEntry[] | undefined, model: IChatModel, progress: (part: IChatVariableResolverProgress) => void, token: CancellationToken): Promise { return { variables: [] }; diff --git a/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts b/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts index 7e43d3ad546..91380192de2 100644 --- a/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts +++ b/src/vscode-dts/vscode.proposed.chatVariableResolver.d.ts @@ -15,8 +15,10 @@ declare module 'vscode' { * @param modelDescription A description of the variable for the model. * @param isSlow Temp, to limit access to '#codebase' which is not a 'reference' and will fit into a tools API later. * @param resolver Will be called to provide the chat variable's value when it is used. + * @param fullName The full name of the variable when selecting context in the picker UI. + * @param icon An icon to display when selecting context in the picker UI. */ - export function registerChatVariableResolver(id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: ChatVariableResolver): Disposable; + export function registerChatVariableResolver(id: string, name: string, userDescription: string, modelDescription: string | undefined, isSlow: boolean | undefined, resolver: ChatVariableResolver, fullName?: string, icon?: ThemeIcon): Disposable; } export interface ChatVariableValue {