AI related quick picker accessible even when AI disabled (fix #286526) (#286540)

This commit is contained in:
Benjamin Pasero
2026-01-08 15:15:18 +01:00
committed by GitHub
parent 10a5645f9d
commit f0a5b2f90f
7 changed files with 97 additions and 20 deletions

View File

@@ -6,6 +6,7 @@
import { localize } from '../../../nls.js';
import { Registry } from '../../registry/common/platform.js';
import { DisposableStore, IDisposable } from '../../../base/common/lifecycle.js';
import { IContextKeyService } from '../../contextkey/common/contextkey.js';
import { IKeybindingService } from '../../keybinding/common/keybinding.js';
import { Extensions, IQuickAccessProvider, IQuickAccessProviderDescriptor, IQuickAccessRegistry } from '../common/quickAccess.js';
import { IQuickInputService, IQuickPick, IQuickPickItem } from '../common/quickInput.js';
@@ -22,7 +23,8 @@ export class HelpQuickAccessProvider implements IQuickAccessProvider {
constructor(
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IKeybindingService private readonly keybindingService: IKeybindingService
@IKeybindingService private readonly keybindingService: IKeybindingService,
@IContextKeyService private readonly contextKeyService: IContextKeyService
) { }
provide(picker: IQuickPick<IHelpQuickAccessPickItem, { useSeparators: true }>): IDisposable {
@@ -39,8 +41,8 @@ export class HelpQuickAccessProvider implements IQuickAccessProvider {
// Also open a picker when we detect the user typed the exact
// name of a provider (e.g. `?term` for terminals)
disposables.add(picker.onDidChangeValue(value => {
const providerDescriptor = this.registry.getQuickAccessProvider(value.substr(HelpQuickAccessProvider.PREFIX.length));
if (providerDescriptor && providerDescriptor.prefix && providerDescriptor.prefix !== HelpQuickAccessProvider.PREFIX) {
const providerDescriptor = this.registry.getQuickAccessProvider(value.substr(HelpQuickAccessProvider.PREFIX.length), this.contextKeyService);
if (providerDescriptor?.prefix && providerDescriptor.prefix !== HelpQuickAccessProvider.PREFIX) {
this.quickInputService.quickAccess.show(providerDescriptor.prefix, { preserveValue: true });
}
}));
@@ -53,7 +55,7 @@ export class HelpQuickAccessProvider implements IQuickAccessProvider {
getQuickAccessProviders(): IHelpQuickAccessPickItem[] {
const providers: IHelpQuickAccessPickItem[] = this.registry
.getQuickAccessProviders()
.getQuickAccessProviders(this.contextKeyService)
.sort((providerA, providerB) => providerA.prefix.localeCompare(providerB.prefix))
.flatMap(provider => this.createPicks(provider));

View File

@@ -7,6 +7,7 @@ import { DeferredPromise } from '../../../base/common/async.js';
import { CancellationTokenSource } from '../../../base/common/cancellation.js';
import { Event } from '../../../base/common/event.js';
import { Disposable, DisposableStore, IDisposable, isDisposable, toDisposable } from '../../../base/common/lifecycle.js';
import { IContextKeyService } from '../../contextkey/common/contextkey.js';
import { IInstantiationService } from '../../instantiation/common/instantiation.js';
import { DefaultQuickAccessFilterValue, Extensions, IQuickAccessController, IQuickAccessOptions, IQuickAccessProvider, IQuickAccessProviderDescriptor, IQuickAccessRegistry } from '../common/quickAccess.js';
import { IQuickInputService, IQuickPick, IQuickPickItem, ItemActivation } from '../common/quickInput.js';
@@ -27,7 +28,8 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
constructor(
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IInstantiationService private readonly instantiationService: IInstantiationService
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IContextKeyService private readonly contextKeyService: IContextKeyService
) {
super();
this._register(toDisposable(() => {
@@ -233,7 +235,7 @@ export class QuickAccessController extends Disposable implements IQuickAccessCon
}
private getOrInstantiateProvider(value: string, enabledProviderPrefixes?: string[]): [IQuickAccessProvider | undefined, IQuickAccessProviderDescriptor | undefined] {
const providerDescriptor = this.registry.getQuickAccessProvider(value);
const providerDescriptor = this.registry.getQuickAccessProvider(value, this.contextKeyService);
if (!providerDescriptor || enabledProviderPrefixes && !enabledProviderPrefixes?.includes(providerDescriptor.prefix)) {
return [undefined, undefined];
}

View File

@@ -6,6 +6,7 @@
import { coalesce } from '../../../base/common/arrays.js';
import { CancellationToken } from '../../../base/common/cancellation.js';
import { IDisposable, toDisposable } from '../../../base/common/lifecycle.js';
import { ContextKeyExpression, IContextKeyService } from '../../contextkey/common/contextkey.js';
import { ItemActivation, IQuickNavigateConfiguration, IQuickPick, IQuickPickItem, QuickPickItem, IQuickPickSeparator } from './quickInput.js';
import { Registry } from '../../registry/common/platform.js';
@@ -194,6 +195,12 @@ export interface IQuickAccessProviderDescriptor {
* picker for the provider is showing.
*/
readonly contextKey?: string;
/**
* A context key expression that must evaluate to true for the
* provider to be considered in the registry.
*/
readonly when?: ContextKeyExpression;
}
export const Extensions = {
@@ -210,12 +217,12 @@ export interface IQuickAccessRegistry {
/**
* Get all registered quick access providers.
*/
getQuickAccessProviders(): IQuickAccessProviderDescriptor[];
getQuickAccessProviders(contextKeyService: IContextKeyService): IQuickAccessProviderDescriptor[];
/**
* Get a specific quick access provider for a given prefix.
*/
getQuickAccessProvider(prefix: string): IQuickAccessProviderDescriptor | undefined;
getQuickAccessProvider(prefix: string, contextKeyService: IContextKeyService): IQuickAccessProviderDescriptor | undefined;
}
export class QuickAccessRegistry implements IQuickAccessRegistry {
@@ -245,12 +252,15 @@ export class QuickAccessRegistry implements IQuickAccessRegistry {
});
}
getQuickAccessProviders(): IQuickAccessProviderDescriptor[] {
return coalesce([this.defaultProvider, ...this.providers]);
getQuickAccessProviders(contextKeyService: IContextKeyService): IQuickAccessProviderDescriptor[] {
return coalesce([this.defaultProvider, ...this.providers])
.filter(provider => !provider.when || contextKeyService.contextMatchesRules(provider.when));
}
getQuickAccessProvider(prefix: string): IQuickAccessProviderDescriptor | undefined {
const result = prefix ? (this.providers.find(provider => prefix.startsWith(provider.prefix)) || undefined) : undefined;
getQuickAccessProvider(prefix: string, contextKeyService: IContextKeyService): IQuickAccessProviderDescriptor | undefined {
const result = prefix
? this.providers.find(provider => prefix.startsWith(provider.prefix) && (!provider.when || contextKeyService.contextMatchesRules(provider.when)))
: undefined;
return result || this.defaultProvider;
}

View File

@@ -154,6 +154,7 @@ Registry.as<IQuickAccessRegistry>(QuickAccessExtensions.Quickaccess).registerQui
ctor: AgentSessionsQuickAccessProvider,
prefix: AGENT_SESSIONS_QUICK_ACCESS_PREFIX,
contextKey: 'inAgentSessionsPicker',
when: ChatContextKeys.enabled,
placeholder: localize('agentSessionsQuickAccessPlaceholder', "Search agent sessions by name"),
helpEntries: [{
description: localize('agentSessionsQuickAccessHelp', "Show All Agent Sessions"),

View File

@@ -16,6 +16,7 @@ import { IConfigurationMigrationRegistry, Extensions as ConfigurationMigrationEx
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
import { EditorExtensions } from '../../../common/editor.js';
import { mcpSchemaId } from '../../../services/configuration/common/configuration.js';
import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js';
import { ExtensionMcpDiscovery } from '../common/discovery/extensionMcpDiscovery.js';
import { InstalledMcpServersDiscovery } from '../common/discovery/installedMcpServersDiscovery.js';
import { mcpDiscoveryRegistry } from '../common/discovery/mcpDiscovery.js';
@@ -108,6 +109,7 @@ Registry.as<IEditorPaneRegistry>(EditorExtensions.EditorPane).registerEditorPane
Registry.as<IQuickAccessRegistry>(QuickAccessExtensions.Quickaccess).registerQuickAccessProvider({
ctor: McpResourceQuickAccess,
prefix: McpResourceQuickAccess.PREFIX,
when: ChatContextKeys.enabled,
placeholder: localize('mcp.quickaccess.placeholder', "Filter to an MCP resource"),
helpEntries: [{
description: localize('mcp.quickaccess.add', "MCP Server Resources"),

View File

@@ -52,6 +52,7 @@ import { IUriIdentityService } from '../../../../platform/uriIdentity/common/uri
import { stripIcons } from '../../../../base/common/iconLabels.js';
import { Lazy } from '../../../../base/common/lazy.js';
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { Registry } from '../../../../platform/registry/common/platform.js';
import { ASK_QUICK_QUESTION_ACTION_ID } from '../../chat/browser/actions/chatQuickInputActions.js';
import { IQuickChatService } from '../../chat/browser/chat.js';
@@ -136,6 +137,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
@IUriIdentityService private readonly uriIdentityService: IUriIdentityService,
@IQuickInputService private readonly quickInputService: IQuickInputService,
@IKeybindingService private readonly keybindingService: IKeybindingService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IQuickChatService private readonly quickChatService: IQuickChatService,
@ILogService private readonly logService: ILogService,
@ICustomEditorLabelService private readonly customEditorLabelService: ICustomEditorLabelService
@@ -827,7 +829,7 @@ export class AnythingQuickAccessProvider extends PickerQuickAccessProvider<IAnyt
}
type IHelpAnythingQuickPickItem = IAnythingQuickPickItem & { commandCenterOrder: number };
const providers: IHelpAnythingQuickPickItem[] = this.lazyRegistry.value.getQuickAccessProviders()
const providers: IHelpAnythingQuickPickItem[] = this.lazyRegistry.value.getQuickAccessProviders(this.contextKeyService)
.filter(p => p.helpEntries.some(h => h.commandCenterOrder !== undefined))
.flatMap(provider => provider.helpEntries
.filter(h => h.commandCenterOrder !== undefined)

View File

@@ -21,6 +21,9 @@ import { EditorsOrder } from '../../common/editor.js';
import { Range } from '../../../editor/common/core/range.js';
import { TestInstantiationService } from '../../../platform/instantiation/test/common/instantiationServiceMock.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../base/test/common/utils.js';
import { IContextKeyService, ContextKeyExpr } from '../../../platform/contextkey/common/contextkey.js';
import { ContextKeyService } from '../../../platform/contextkey/browser/contextKeyService.js';
import { TestConfigurationService } from '../../../platform/configuration/test/common/testConfigurationService.js';
suite('QuickAccess', () => {
@@ -114,26 +117,81 @@ suite('QuickAccess', () => {
test('registry', () => {
const registry = (Registry.as<IQuickAccessRegistry>(Extensions.Quickaccess));
const restore = (registry as QuickAccessRegistry).clear();
const contextKeyService = instantiationService.get(IContextKeyService);
assert.ok(!registry.getQuickAccessProvider('test'));
assert.ok(!registry.getQuickAccessProvider('test', contextKeyService));
const disposables = new DisposableStore();
disposables.add(registry.registerQuickAccessProvider(providerDescriptorDefault));
assert(registry.getQuickAccessProvider('') === providerDescriptorDefault);
assert(registry.getQuickAccessProvider('test') === providerDescriptorDefault);
assert(registry.getQuickAccessProvider('', contextKeyService) === providerDescriptorDefault);
assert(registry.getQuickAccessProvider('test', contextKeyService) === providerDescriptorDefault);
const disposable = disposables.add(registry.registerQuickAccessProvider(providerDescriptor1));
assert(registry.getQuickAccessProvider('test') === providerDescriptor1);
assert(registry.getQuickAccessProvider('test', contextKeyService) === providerDescriptor1);
const providers = registry.getQuickAccessProviders();
const providers = registry.getQuickAccessProviders(contextKeyService);
assert(providers.some(provider => provider.prefix === 'test'));
disposable.dispose();
assert(registry.getQuickAccessProvider('test') === providerDescriptorDefault);
assert(registry.getQuickAccessProvider('test', contextKeyService) === providerDescriptorDefault);
disposables.dispose();
assert.ok(!registry.getQuickAccessProvider('test'));
assert.ok(!registry.getQuickAccessProvider('test', contextKeyService));
restore();
});
test('registry - when condition', () => {
const registry = (Registry.as<IQuickAccessRegistry>(Extensions.Quickaccess));
const restore = (registry as QuickAccessRegistry).clear();
// Use real ContextKeyService that properly evaluates rules
const contextKeyService = disposables.add(new ContextKeyService(new TestConfigurationService()));
const localDisposables = new DisposableStore();
// Create a context key that starts as undefined (falsy)
const contextKey = contextKeyService.createKey<boolean | undefined>('testQuickAccessContextKey', undefined);
// Register a provider with a when condition that requires testQuickAccessContextKey to be truthy
const providerWithWhen = {
ctor: TestProvider1,
prefix: 'whentest',
helpEntries: [],
when: ContextKeyExpr.has('testQuickAccessContextKey')
};
localDisposables.add(registry.registerQuickAccessProvider(providerWithWhen));
// Verify the expression works with the context key service
assert.strictEqual(contextKeyService.contextMatchesRules(providerWithWhen.when), false);
// Provider with false when condition should not be found
assert.strictEqual(registry.getQuickAccessProvider('whentest', contextKeyService), undefined);
// Should not appear in the list of providers
let providers = registry.getQuickAccessProviders(contextKeyService);
assert.ok(!providers.some(p => p.prefix === 'whentest'));
// Set the context key to true
contextKey.set(true);
// Verify the expression now matches
assert.strictEqual(contextKeyService.contextMatchesRules(providerWithWhen.when), true);
// Now the provider should be found
assert.strictEqual(registry.getQuickAccessProvider('whentest', contextKeyService), providerWithWhen);
// Should appear in the list of providers
providers = registry.getQuickAccessProviders(contextKeyService);
assert.ok(providers.some(p => p.prefix === 'whentest'));
// Set context key back to undefined (falsy)
contextKey.set(undefined);
// Provider should not be found again
assert.strictEqual(registry.getQuickAccessProvider('whentest', contextKeyService), undefined);
localDisposables.dispose();
restore();
});