Path: blob/main/src/vs/workbench/contrib/chat/browser/actions/chatContextActions.ts
5284 views
/*---------------------------------------------------------------------------------------------1* Copyright (c) Microsoft Corporation. All rights reserved.2* Licensed under the MIT License. See License.txt in the project root for license information.3*--------------------------------------------------------------------------------------------*/45import { asArray } from '../../../../../base/common/arrays.js';6import { DeferredPromise, isThenable } from '../../../../../base/common/async.js';7import { CancellationTokenSource } from '../../../../../base/common/cancellation.js';8import { Codicon } from '../../../../../base/common/codicons.js';9import { KeyCode, KeyMod } from '../../../../../base/common/keyCodes.js';10import { DisposableStore, toDisposable } from '../../../../../base/common/lifecycle.js';11import { Schemas } from '../../../../../base/common/network.js';12import { autorun, observableValue } from '../../../../../base/common/observable.js';13import { ThemeIcon } from '../../../../../base/common/themables.js';14import { isObject } from '../../../../../base/common/types.js';15import { URI } from '../../../../../base/common/uri.js';16import { ServicesAccessor } from '../../../../../editor/browser/editorExtensions.js';17import { Range } from '../../../../../editor/common/core/range.js';18import { EditorContextKeys } from '../../../../../editor/common/editorContextKeys.js';19import { ITextModelService } from '../../../../../editor/common/services/resolverService.js';20import { AbstractGotoSymbolQuickAccessProvider, IGotoSymbolQuickPickItem } from '../../../../../editor/contrib/quickAccess/browser/gotoSymbolQuickAccess.js';21import { localize, localize2 } from '../../../../../nls.js';22import { Action2, MenuId, registerAction2 } from '../../../../../platform/actions/common/actions.js';23import { ICommandService } from '../../../../../platform/commands/common/commands.js';24import { ContextKeyExpr, IContextKeyService } from '../../../../../platform/contextkey/common/contextkey.js';25import { IFileService } from '../../../../../platform/files/common/files.js';26import { IInstantiationService } from '../../../../../platform/instantiation/common/instantiation.js';27import { IKeybindingService } from '../../../../../platform/keybinding/common/keybinding.js';28import { KeybindingWeight } from '../../../../../platform/keybinding/common/keybindingsRegistry.js';29import { IListService } from '../../../../../platform/list/browser/listService.js';30import { ILogService } from '../../../../../platform/log/common/log.js';31import { AnythingQuickAccessProviderRunOptions } from '../../../../../platform/quickinput/common/quickAccess.js';32import { IQuickInputService, IQuickPickItem, IQuickPickItemWithResource, QuickPickItem } from '../../../../../platform/quickinput/common/quickInput.js';33import { resolveCommandsContext } from '../../../../browser/parts/editor/editorCommandsContext.js';34import { ResourceContextKey } from '../../../../common/contextkeys.js';35import { EditorResourceAccessor, isEditorCommandsContext, SideBySideEditor } from '../../../../common/editor.js';36import { IEditorGroupsService } from '../../../../services/editor/common/editorGroupsService.js';37import { IEditorService } from '../../../../services/editor/common/editorService.js';38import { ExplorerFolderContext } from '../../../files/common/files.js';39import { CTX_INLINE_CHAT_V2_ENABLED } from '../../../inlineChat/common/inlineChat.js';40import { AnythingQuickAccessProvider } from '../../../search/browser/anythingQuickAccess.js';41import { isSearchTreeFileMatch, isSearchTreeMatch } from '../../../search/browser/searchTreeModel/searchTreeCommon.js';42import { ISymbolQuickPickItem, SymbolsQuickAccessProvider } from '../../../search/browser/symbolsQuickAccess.js';43import { SearchContext } from '../../../search/common/constants.js';44import { ChatContextKeys } from '../../common/actions/chatContextKeys.js';45import { IChatRequestVariableEntry, OmittedState } from '../../common/attachments/chatVariableEntries.js';46import { ChatAgentLocation, isSupportedChatFileScheme } from '../../common/constants.js';47import { IChatWidget, IChatWidgetService, IQuickChatService } from '../chat.js';48import { IChatContextPickerItem, IChatContextPickService, IChatContextValueItem, isChatContextPickerPickItem } from '../attachments/chatContextPickService.js';49import { isQuickChat } from '../widget/chatWidget.js';50import { resizeImage } from '../chatImageUtils.js';51import { registerPromptActions } from '../promptSyntax/promptFileActions.js';52import { CHAT_CATEGORY } from './chatActions.js';5354export function registerChatContextActions() {55registerAction2(AttachContextAction);56registerAction2(AttachFileToChatAction);57registerAction2(AttachFolderToChatAction);58registerAction2(AttachSelectionToChatAction);59registerAction2(AttachSearchResultAction);60registerAction2(AttachPinnedEditorsToChatAction);61registerPromptActions();62}6364async function withChatView(accessor: ServicesAccessor): Promise<IChatWidget | undefined> {65const chatWidgetService = accessor.get(IChatWidgetService);6667const lastFocusedWidget = chatWidgetService.lastFocusedWidget;68if (!lastFocusedWidget || lastFocusedWidget.location === ChatAgentLocation.Chat) {69return chatWidgetService.revealWidget(); // only show chat view if we either have no chat view or its located in view container70}71return lastFocusedWidget;72}7374abstract class AttachResourceAction extends Action2 {7576override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise<void> {77const instaService = accessor.get(IInstantiationService);78const widget = await instaService.invokeFunction(withChatView);79if (!widget) {80return;81}82return instaService.invokeFunction(this.runWithWidget.bind(this), widget, ...args);83}8485abstract runWithWidget(accessor: ServicesAccessor, widget: IChatWidget, ...args: unknown[]): Promise<void>;8687protected _getResources(accessor: ServicesAccessor, ...args: unknown[]): URI[] {88const editorService = accessor.get(IEditorService);8990const contexts = isEditorCommandsContext(args[1]) ? this._getEditorResources(accessor, args) : Array.isArray(args[1]) ? args[1] : [args[0]];91const files = [];92for (const context of contexts) {93let uri;94if (URI.isUri(context)) {95uri = context;96} else if (isSearchTreeFileMatch(context)) {97uri = context.resource;98} else if (isSearchTreeMatch(context)) {99uri = context.parent().resource;100} else if (!context && editorService.activeTextEditorControl) {101uri = EditorResourceAccessor.getCanonicalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });102}103104if (uri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(uri.scheme)) {105files.push(uri);106}107}108109return files;110}111112private _getEditorResources(accessor: ServicesAccessor, ...args: unknown[]): URI[] {113const resolvedContext = resolveCommandsContext(args, accessor.get(IEditorService), accessor.get(IEditorGroupsService), accessor.get(IListService));114115return resolvedContext.groupedEditors116.flatMap(groupedEditor => groupedEditor.editors)117.map(editor => EditorResourceAccessor.getCanonicalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY }))118.filter(uri => uri !== undefined);119}120}121122class AttachFileToChatAction extends AttachResourceAction {123124static readonly ID = 'workbench.action.chat.attachFile';125126constructor() {127super({128id: AttachFileToChatAction.ID,129title: localize2('workbench.action.chat.attachFile.label', "Add File to Chat"),130category: CHAT_CATEGORY,131precondition: ChatContextKeys.enabled,132f1: true,133menu: [{134id: MenuId.SearchContext,135group: 'z_chat',136order: 1,137when: ContextKeyExpr.and(ChatContextKeys.enabled, SearchContext.FileMatchOrMatchFocusKey, SearchContext.SearchResultHeaderFocused.negate()),138}, {139id: MenuId.ExplorerContext,140group: '5_chat',141order: 1,142when: ContextKeyExpr.and(143ChatContextKeys.enabled,144ExplorerFolderContext.negate(),145ContextKeyExpr.or(146ResourceContextKey.Scheme.isEqualTo(Schemas.file),147ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeRemote)148)149),150}, {151id: MenuId.EditorTitleContext,152group: '2_chat',153order: 1,154when: ContextKeyExpr.and(155ChatContextKeys.enabled,156ContextKeyExpr.or(157ResourceContextKey.Scheme.isEqualTo(Schemas.file),158ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeRemote)159)160),161}, {162id: MenuId.EditorContext,163group: '1_chat',164order: 2,165when: ContextKeyExpr.and(166ChatContextKeys.enabled,167ContextKeyExpr.or(168ResourceContextKey.Scheme.isEqualTo(Schemas.file),169ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeRemote),170ResourceContextKey.Scheme.isEqualTo(Schemas.untitled),171ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeUserData)172)173)174}, {175id: MenuId.ChatEditorInlineGutter,176group: '2_chat',177order: 2,178when: ContextKeyExpr.and(ChatContextKeys.enabled, EditorContextKeys.hasNonEmptySelection.negate())179}]180});181}182183override async runWithWidget(accessor: ServicesAccessor, widget: IChatWidget, ...args: unknown[]): Promise<void> {184const files = this._getResources(accessor, ...args);185if (!files.length) {186return;187}188if (widget) {189widget.focusInput();190for (const file of files) {191widget.attachmentModel.addFile(file);192}193}194}195}196197class AttachFolderToChatAction extends AttachResourceAction {198199static readonly ID = 'workbench.action.chat.attachFolder';200201constructor() {202super({203id: AttachFolderToChatAction.ID,204title: localize2('workbench.action.chat.attachFolder.label', "Add Folder to Chat"),205category: CHAT_CATEGORY,206f1: false,207menu: {208id: MenuId.ExplorerContext,209group: '5_chat',210order: 1,211when: ContextKeyExpr.and(212ChatContextKeys.enabled,213ExplorerFolderContext,214ContextKeyExpr.or(215ResourceContextKey.Scheme.isEqualTo(Schemas.file),216ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeRemote)217)218)219}220});221}222223override async runWithWidget(accessor: ServicesAccessor, widget: IChatWidget, ...args: unknown[]): Promise<void> {224const folders = this._getResources(accessor, ...args);225if (!folders.length) {226return;227}228if (widget) {229widget.focusInput();230for (const folder of folders) {231widget.attachmentModel.addFolder(folder);232}233}234}235}236237class AttachPinnedEditorsToChatAction extends Action2 {238239static readonly ID = 'workbench.action.chat.attachPinnedEditors';240241constructor() {242super({243id: AttachPinnedEditorsToChatAction.ID,244title: localize2('workbench.action.chat.attachPinnedEditors.label', "Add Pinned Editors to Chat"),245category: CHAT_CATEGORY,246precondition: ChatContextKeys.enabled,247f1: true,248});249}250251override async run(accessor: ServicesAccessor): Promise<void> {252const editorGroupsService = accessor.get(IEditorGroupsService);253const instaService = accessor.get(IInstantiationService);254255const widget = await instaService.invokeFunction(withChatView);256if (!widget) {257return;258}259260const files: URI[] = [];261for (const group of editorGroupsService.groups) {262for (const editor of group.editors) {263if (group.isPinned(editor)) {264const uri = EditorResourceAccessor.getCanonicalUri(editor, { supportSideBySide: SideBySideEditor.PRIMARY });265if (uri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(uri.scheme)) {266files.push(uri);267}268}269}270}271272if (!files.length) {273return;274}275276widget.focusInput();277for (const file of files) {278widget.attachmentModel.addFile(file);279}280}281}282283class AttachSelectionToChatAction extends Action2 {284285static readonly ID = 'workbench.action.chat.attachSelection';286287constructor() {288super({289id: AttachSelectionToChatAction.ID,290title: localize2('workbench.action.chat.attachSelection.label', "Add Selection to Chat"),291category: CHAT_CATEGORY,292f1: true,293precondition: ChatContextKeys.enabled,294menu: [{295id: MenuId.EditorContext,296group: '1_chat',297order: 1,298when: ContextKeyExpr.and(299ChatContextKeys.enabled,300EditorContextKeys.hasNonEmptySelection,301ContextKeyExpr.or(302ResourceContextKey.Scheme.isEqualTo(Schemas.file),303ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeRemote),304ResourceContextKey.Scheme.isEqualTo(Schemas.untitled),305ResourceContextKey.Scheme.isEqualTo(Schemas.vscodeUserData)306)307)308}, {309id: MenuId.ChatEditorInlineGutter,310group: '2_chat',311order: 1,312when: ContextKeyExpr.and(ChatContextKeys.enabled, EditorContextKeys.hasNonEmptySelection)313}]314});315}316317// eslint-disable-next-line @typescript-eslint/no-explicit-any318override async run(accessor: ServicesAccessor, ...args: any[]): Promise<void> {319const editorService = accessor.get(IEditorService);320321const widget = await accessor.get(IInstantiationService).invokeFunction(withChatView);322if (!widget) {323return;324}325326const [_, matches] = args;327// If we have search matches, it means this is coming from the search widget328if (matches && matches.length > 0) {329const uris = new Map<URI, Range | undefined>();330for (const match of matches) {331if (isSearchTreeFileMatch(match)) {332uris.set(match.resource, undefined);333} else {334const context = { uri: match._parent.resource, range: match._range };335const range = uris.get(context.uri);336if (!range ||337range.startLineNumber !== context.range.startLineNumber && range.endLineNumber !== context.range.endLineNumber) {338uris.set(context.uri, context.range);339widget.attachmentModel.addFile(context.uri, context.range);340}341}342}343// Add the root files for all of the ones that didn't have a match344for (const uri of uris) {345const [resource, range] = uri;346if (!range) {347widget.attachmentModel.addFile(resource);348}349}350} else {351const activeEditor = editorService.activeTextEditorControl;352const activeUri = EditorResourceAccessor.getCanonicalUri(editorService.activeEditor, { supportSideBySide: SideBySideEditor.PRIMARY });353if (activeEditor && activeUri && [Schemas.file, Schemas.vscodeRemote, Schemas.untitled].includes(activeUri.scheme)) {354const selection = activeEditor.getSelection();355if (selection) {356widget.focusInput();357const range = selection.isEmpty() ? new Range(selection.startLineNumber, 1, selection.startLineNumber + 1, 1) : selection;358widget.attachmentModel.addFile(activeUri, range);359}360}361}362}363}364365export class AttachSearchResultAction extends Action2 {366367private static readonly Name = 'searchResults';368369constructor() {370super({371id: 'workbench.action.chat.insertSearchResults',372title: localize2('chat.insertSearchResults', 'Add Search Results to Chat'),373category: CHAT_CATEGORY,374f1: false,375menu: [{376id: MenuId.SearchContext,377group: 'z_chat',378order: 3,379when: ContextKeyExpr.and(380ChatContextKeys.enabled,381SearchContext.SearchResultHeaderFocused),382}]383});384}385async run(accessor: ServicesAccessor) {386const logService = accessor.get(ILogService);387const widget = await accessor.get(IInstantiationService).invokeFunction(withChatView);388389if (!widget) {390logService.trace('InsertSearchResultAction: no chat view available');391return;392}393394const editor = widget.inputEditor;395const originalRange = editor.getSelection() ?? editor.getModel()?.getFullModelRange().collapseToEnd();396397if (!originalRange) {398logService.trace('InsertSearchResultAction: no selection');399return;400}401402let insertText = `#${AttachSearchResultAction.Name}`;403const varRange = new Range(originalRange.startLineNumber, originalRange.startColumn, originalRange.endLineNumber, originalRange.startLineNumber + insertText.length);404// check character before the start of the range. If it's not a space, add a space405const model = editor.getModel();406if (model && model.getValueInRange(new Range(originalRange.startLineNumber, originalRange.startColumn - 1, originalRange.startLineNumber, originalRange.startColumn)) !== ' ') {407insertText = ' ' + insertText;408}409const success = editor.executeEdits('chatInsertSearch', [{ range: varRange, text: insertText + ' ' }]);410if (!success) {411logService.trace(`InsertSearchResultAction: failed to insert "${insertText}"`);412return;413}414}415}416417/** This is our type */418interface IContextPickItemItem extends IQuickPickItem {419kind: 'contextPick';420item: IChatContextValueItem | IChatContextPickerItem;421}422423/** These are the types we get from "platform QP" */424type IQuickPickServicePickItem = IGotoSymbolQuickPickItem | ISymbolQuickPickItem | IQuickPickItemWithResource;425426function isIContextPickItemItem(obj: unknown): obj is IContextPickItemItem {427return (428isObject(obj)429&& typeof (<IContextPickItemItem>obj).kind === 'string'430&& (<IContextPickItemItem>obj).kind === 'contextPick'431);432}433434function isIGotoSymbolQuickPickItem(obj: unknown): obj is IGotoSymbolQuickPickItem {435return (436isObject(obj)437&& typeof (obj as IGotoSymbolQuickPickItem).symbolName === 'string'438&& !!(obj as IGotoSymbolQuickPickItem).uri439&& !!(obj as IGotoSymbolQuickPickItem).range);440}441442function isIQuickPickItemWithResource(obj: unknown): obj is IQuickPickItemWithResource {443return (444isObject(obj)445&& URI.isUri((obj as IQuickPickItemWithResource).resource));446}447448449export class AttachContextAction extends Action2 {450451constructor() {452super({453id: 'workbench.action.chat.attachContext',454title: localize2('workbench.action.chat.attachContext.label.2', "Add Context..."),455icon: Codicon.attach,456category: CHAT_CATEGORY,457keybinding: {458when: ContextKeyExpr.and(ChatContextKeys.inChatInput, ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat)),459primary: KeyMod.CtrlCmd | KeyCode.Slash,460weight: KeybindingWeight.EditorContrib461},462menu: {463when: ContextKeyExpr.and(464ContextKeyExpr.or(465ChatContextKeys.location.isEqualTo(ChatAgentLocation.Chat),466ContextKeyExpr.and(ChatContextKeys.location.isEqualTo(ChatAgentLocation.EditorInline), CTX_INLINE_CHAT_V2_ENABLED)467),468ContextKeyExpr.or(469ChatContextKeys.lockedToCodingAgent.negate(),470ChatContextKeys.agentSupportsAttachments471)472),473id: MenuId.ChatInputAttachmentToolbar,474group: 'navigation',475order: 3476},477478});479}480481override async run(accessor: ServicesAccessor, ...args: unknown[]): Promise<void> {482483const instantiationService = accessor.get(IInstantiationService);484const widgetService = accessor.get(IChatWidgetService);485const contextKeyService = accessor.get(IContextKeyService);486const keybindingService = accessor.get(IKeybindingService);487const contextPickService = accessor.get(IChatContextPickService);488489const context = args[0] as { widget?: IChatWidget; placeholder?: string } | undefined;490const widget = context?.widget ?? widgetService.lastFocusedWidget;491if (!widget) {492return;493}494495const quickPickItems: IContextPickItemItem[] = [];496497for (const item of contextPickService.items) {498499if (item.isEnabled && !await item.isEnabled(widget)) {500continue;501}502503quickPickItems.push({504kind: 'contextPick',505item,506label: item.label,507iconClass: ThemeIcon.asClassName(item.icon),508keybinding: item.commandId ? keybindingService.lookupKeybinding(item.commandId, contextKeyService) : undefined,509});510}511512instantiationService.invokeFunction(this._show.bind(this), widget, quickPickItems, context?.placeholder);513}514515private _show(accessor: ServicesAccessor, widget: IChatWidget, additionPicks: IContextPickItemItem[] | undefined, placeholder?: string) {516const quickInputService = accessor.get(IQuickInputService);517const quickChatService = accessor.get(IQuickChatService);518const instantiationService = accessor.get(IInstantiationService);519const commandService = accessor.get(ICommandService);520521const providerOptions: AnythingQuickAccessProviderRunOptions = {522filter: (pick) => {523if (isIQuickPickItemWithResource(pick) && pick.resource) {524return instantiationService.invokeFunction(accessor => isSupportedChatFileScheme(accessor, pick.resource!.scheme));525}526return true;527},528additionPicks,529handleAccept: async (item: IQuickPickServicePickItem | IContextPickItemItem, isBackgroundAccept: boolean) => {530531if (isIContextPickItemItem(item)) {532533let isDone = true;534if (item.item.type === 'valuePick') {535this._handleContextPick(item.item, widget);536537} else if (item.item.type === 'pickerPick') {538isDone = await this._handleContextPickerItem(quickInputService, commandService, item.item, widget);539}540541if (!isDone) {542// restart picker when sub-picker didn't return anything543instantiationService.invokeFunction(this._show.bind(this), widget, additionPicks, placeholder);544return;545}546547} else {548instantiationService.invokeFunction(this._handleQPPick.bind(this), widget, isBackgroundAccept, item);549}550if (isQuickChat(widget)) {551quickChatService.open();552}553}554};555556quickInputService.quickAccess.show('', {557enabledProviderPrefixes: [558AnythingQuickAccessProvider.PREFIX,559SymbolsQuickAccessProvider.PREFIX,560AbstractGotoSymbolQuickAccessProvider.PREFIX561],562placeholder: placeholder ?? localize('chatContext.attach.placeholder', 'Search attachments'),563providerOptions,564});565}566567private async _handleQPPick(accessor: ServicesAccessor, widget: IChatWidget, isInBackground: boolean, pick: IQuickPickServicePickItem) {568const fileService = accessor.get(IFileService);569const textModelService = accessor.get(ITextModelService);570571const toAttach: IChatRequestVariableEntry[] = [];572573if (isIQuickPickItemWithResource(pick) && pick.resource) {574if (/\.(png|jpg|jpeg|bmp|gif|tiff)$/i.test(pick.resource.path)) {575// checks if the file is an image576if (URI.isUri(pick.resource)) {577// read the image and attach a new file context.578const readFile = await fileService.readFile(pick.resource);579const resizedImage = await resizeImage(readFile.value.buffer);580toAttach.push({581id: pick.resource.toString(),582name: pick.label,583fullName: pick.label,584value: resizedImage,585kind: 'image',586references: [{ reference: pick.resource, kind: 'reference' }]587});588}589} else {590let omittedState = OmittedState.NotOmitted;591try {592const createdModel = await textModelService.createModelReference(pick.resource);593createdModel.dispose();594} catch {595omittedState = OmittedState.Full;596}597598toAttach.push({599kind: 'file',600id: pick.resource.toString(),601value: pick.resource,602name: pick.label,603omittedState604});605}606} else if (isIGotoSymbolQuickPickItem(pick) && pick.uri && pick.range) {607toAttach.push({608kind: 'generic',609id: JSON.stringify({ uri: pick.uri, range: pick.range.decoration }),610value: { uri: pick.uri, range: pick.range.decoration },611fullName: pick.label,612name: pick.symbolName!,613});614}615616617widget.attachmentModel.addContext(...toAttach);618619if (!isInBackground) {620// Set focus back into the input once the user is done attaching items621// so that the user can start typing their message622widget.focusInput();623}624}625626private async _handleContextPick(item: IChatContextValueItem, widget: IChatWidget) {627628const value = await item.asAttachment(widget);629if (Array.isArray(value)) {630widget.attachmentModel.addContext(...value);631} else if (value) {632widget.attachmentModel.addContext(value);633}634}635636private async _handleContextPickerItem(quickInputService: IQuickInputService, commandService: ICommandService, item: IChatContextPickerItem, widget: IChatWidget): Promise<boolean> {637638const pickerConfig = item.asPicker(widget);639640const store = new DisposableStore();641642const goBackItem: IQuickPickItem = {643label: localize('goBack', 'Go back ↩'),644alwaysShow: true645};646const configureItem = pickerConfig.configure ? {647label: pickerConfig.configure.label,648commandId: pickerConfig.configure.commandId,649alwaysShow: true650} : undefined;651const extraPicks: QuickPickItem[] = [{ type: 'separator' }];652if (configureItem) {653extraPicks.push(configureItem);654}655extraPicks.push(goBackItem);656657const qp = store.add(quickInputService.createQuickPick({ useSeparators: true }));658659const cts = new CancellationTokenSource();660store.add(qp.onDidHide(() => cts.cancel()));661store.add(toDisposable(() => cts.dispose(true)));662663qp.placeholder = pickerConfig.placeholder;664qp.matchOnDescription = true;665qp.matchOnDetail = true;666// qp.ignoreFocusOut = true;667qp.canAcceptInBackground = true;668qp.busy = true;669qp.show();670671if (isThenable(pickerConfig.picks)) {672const items = await (pickerConfig.picks.then(value => {673return ([] as QuickPickItem[]).concat(value, extraPicks);674}));675676qp.items = items;677qp.busy = false;678} else {679const query = observableValue<string>('attachContext.query', qp.value);680store.add(qp.onDidChangeValue(() => query.set(qp.value, undefined)));681682const picksObservable = pickerConfig.picks(query, cts.token);683store.add(autorun(reader => {684const { busy, picks } = picksObservable.read(reader);685qp.items = ([] as QuickPickItem[]).concat(picks, extraPicks);686qp.busy = busy;687}));688}689690if (cts.token.isCancellationRequested) {691pickerConfig.dispose?.();692return true; // picker got hidden already693}694695const defer = new DeferredPromise<boolean>();696const addPromises: Promise<void>[] = [];697698store.add(qp.onDidAccept(async e => {699const noop = 'noop';700const [selected] = qp.selectedItems;701if (isChatContextPickerPickItem(selected)) {702const attachment = selected.asAttachment();703if (!attachment || attachment === noop) {704return;705}706if (isThenable(attachment)) {707addPromises.push(attachment.then(v => {708if (v !== noop) {709widget.attachmentModel.addContext(...asArray(v));710}711}));712} else {713widget.attachmentModel.addContext(...asArray(attachment));714}715}716if (selected === goBackItem) {717if (pickerConfig.goBack?.()) {718// Custom goBack handled the navigation, stay in the picker719return; // Don't complete, keep picker open720}721// Default behavior: go back to main picker722defer.complete(false);723}724if (selected === configureItem) {725defer.complete(true);726commandService.executeCommand(configureItem.commandId);727}728if (!e.inBackground) {729defer.complete(true);730}731}));732733store.add(qp.onDidHide(() => {734defer.complete(true);735pickerConfig.dispose?.();736}));737738try {739const result = await defer.p;740qp.busy = true; // if still visible741await Promise.all(addPromises);742return result;743} finally {744store.dispose();745}746}747}748749750