diff --git a/src/positron-dts/positron.d.ts b/src/positron-dts/positron.d.ts index 801e15bf386c..1e01be10f39c 100644 --- a/src/positron-dts/positron.d.ts +++ b/src/positron-dts/positron.d.ts @@ -3734,6 +3734,56 @@ declare module 'positron' { * @returns A Thenable that resolves to true if completions should be enabled for the file, false otherwise. */ export function areCompletionsEnabled(uri: vscode.Uri): Thenable; + + /** + * A parameter accepted by an allowed command. + */ + export interface AllowedCommandArg { + /** Parameter name. */ + name: string; + /** Human-readable description of the parameter. */ + description?: string; + /** Whether the parameter may be omitted. */ + isOptional?: boolean; + } + + /** + * Where a command was registered from. + */ + export interface AllowedCommandSource { + /** `'builtin'` for core Positron/VS Code commands; `'extension'` for extension-contributed commands. */ + type: 'builtin' | 'extension'; + /** Extension identifier (e.g. `ms-python.python`). Only present when `type` is `'extension'`. */ + id?: string; + /** Extension display name. Only present when `type` is `'extension'`. */ + displayName?: string; + } + + /** + * Metadata for a single Positron command available to AI agents. + */ + export interface AllowedCommand { + /** Unique command identifier (e.g. `workbench.action.files.save`). */ + id: string; + /** Human-readable description of what the command does. */ + description?: string; + /** Ordered list of parameters the command accepts. */ + args?: AllowedCommandArg[]; + /** Description of the command's return value, if any. */ + returns?: string; + /** Where the command was registered from. */ + source: AllowedCommandSource; + } + + /** + * Returns all commands available to AI agents, including their IDs, + * descriptions, and parameter/return-value metadata. + * + * In the future this may be filtered to a configured allowlist. + * + * @returns A Thenable that resolves to an array of command descriptors. + */ + export function getAllowedCommands(): Thenable; } /** diff --git a/src/vs/workbench/api/browser/positron/mainThreadAiFeatures.ts b/src/vs/workbench/api/browser/positron/mainThreadAiFeatures.ts index bd7bb6196a40..36fe95a11ac0 100644 --- a/src/vs/workbench/api/browser/positron/mainThreadAiFeatures.ts +++ b/src/vs/workbench/api/browser/positron/mainThreadAiFeatures.ts @@ -6,6 +6,9 @@ import { Disposable, DisposableMap } from '../../../../base/common/lifecycle.js'; import { revive } from '../../../../base/common/marshalling.js'; import { URI, UriComponents } from '../../../../base/common/uri.js'; +import { CommandsRegistry } from '../../../../platform/commands/common/commands.js'; +import { isIMenuItem, MenuId, MenuRegistry } from '../../../../platform/actions/common/actions.js'; +import { EditorExtensionsRegistry } from '../../../../editor/browser/editorExtensions.js'; import { ChatViewId } from '../../../contrib/chat/browser/chat.js'; import { ChatViewPane } from '../../../contrib/chat/browser/widgetHosts/viewPane/chatViewPane.js'; import { IChatAgentData, IChatAgentService } from '../../../contrib/chat/common/participants/chatAgents.js'; @@ -16,7 +19,7 @@ import { IChatRequestData, IPositronAssistantConfigurationService, IPositronAssi import { extHostNamedCustomer, IExtHostContext } from '../../../services/extensions/common/extHostCustomers.js'; import { IViewsService } from '../../../services/views/common/viewsService.js'; import { IChatProgressDto } from '../../common/extHost.protocol.js'; -import { ExtHostAiFeaturesShape, ExtHostPositronContext, MainPositronContext, MainThreadAiFeaturesShape } from '../../common/positron/extHost.positron.protocol.js'; +import { ExtHostAiFeaturesShape, ExtHostPositronContext, ISerializedAllowedCommand, MainPositronContext, MainThreadAiFeaturesShape } from '../../common/positron/extHost.positron.protocol.js'; @extHostNamedCustomer(MainPositronContext.MainThreadAiFeatures) export class MainThreadAiFeatures extends Disposable implements MainThreadAiFeaturesShape { @@ -186,4 +189,73 @@ export class MainThreadAiFeatures extends Disposable implements MainThreadAiFeat async $getEnabledProviders(): Promise { return this._positronAssistantConfigurationService.getEnabledProviders(); } + + /** + * Return all registered commands with their IDs, descriptions, and parameter metadata. + * Internal commands (IDs starting with '_') are excluded. + */ + async $getAllowedCommands(): Promise { + const allCommands = CommandsRegistry.getCommands(); + const menuCommands = MenuRegistry.getCommands(); + + // Build title map from command palette menu items — catches MultiCommand/EditorCommand + // registrations (e.g. undo, redo) that use appendMenuItem instead of addCommand. + const paletteItemTitles = new Map(); + for (const item of MenuRegistry.getMenuItems(MenuId.CommandPalette)) { + if (isIMenuItem(item)) { + const { id, title } = item.command; + if (title) { + paletteItemTitles.set(id, typeof title === 'string' ? title : title.value); + } + } + } + + // Build label map from editor actions (covers undo, redo, cursor commands, etc.) + const editorActionLabels = new Map(); + for (const action of EditorExtensionsRegistry.getEditorActions()) { + editorActionLabels.set(action.id, action.label); + } + + const result: ISerializedAllowedCommand[] = []; + + for (const [id, command] of allCommands) { + if (id.startsWith('_')) { + continue; + } + + const meta = command.metadata; + const menuCmd = menuCommands.get(id); + + let description: string | undefined; + if (meta?.description) { + description = typeof meta.description === 'string' + ? meta.description + : meta.description.value; + } else if (menuCmd) { + const title = menuCmd.title; + description = typeof title === 'string' ? title : title.value; + } else { + description = paletteItemTitles.get(id) ?? editorActionLabels.get(id); + } + + const cmdSource = menuCmd?.source; + const source: ISerializedAllowedCommand['source'] = cmdSource + ? { type: 'extension', id: cmdSource.id, displayName: cmdSource.title } + : { type: 'builtin' }; + + result.push({ + id, + description, + args: meta?.args?.map(a => ({ + name: a.name, + description: a.description, + isOptional: a.isOptional, + })), + returns: meta?.returns, + source, + }); + } + + return result; + } } diff --git a/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts b/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts index e36e77789422..a92027280241 100644 --- a/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts +++ b/src/vs/workbench/api/common/positron/extHost.positron.api.impl.ts @@ -522,6 +522,9 @@ export function createPositronApiFactoryAndRegisterActors(accessor: ServicesAcce getEnabledProviders(): Thenable { return extHostAiFeatures.getEnabledProviders(); }, + getAllowedCommands(): Thenable { + return extHostAiFeatures.getAllowedCommands(); + }, LanguageModelAutoconfigureType: extHostTypes.LanguageModelAutoconfigureType }; diff --git a/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts b/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts index eaa47a4c0355..e0f8f471b8ae 100644 --- a/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts +++ b/src/vs/workbench/api/common/positron/extHost.positron.protocol.ts @@ -320,6 +320,28 @@ export interface MainThreadEnvironmentShape extends IDisposable { export interface ExtHostEnvironmentShape { } +export interface ISerializedAllowedCommandArg { + name: string; + description?: string; + isOptional?: boolean; +} + +export interface ISerializedAllowedCommandSource { + type: 'builtin' | 'extension'; + /** Extension identifier (e.g. `ms-python.python`). Only present when type is 'extension'. */ + id?: string; + /** Extension display name. Only present when type is 'extension'. */ + displayName?: string; +} + +export interface ISerializedAllowedCommand { + id: string; + description?: string; + args?: ISerializedAllowedCommandArg[]; + returns?: string; + source: ISerializedAllowedCommandSource; +} + export interface MainThreadAiFeaturesShape { $registerChatAgent(agentData: IChatAgentData): Thenable; $unregisterChatAgent(id: string): void; @@ -338,6 +360,7 @@ export interface MainThreadAiFeaturesShape { $getProviders(): Thenable; $setCurrentProvider(id: string): Thenable; $getEnabledProviders(): Thenable; + $getAllowedCommands(): Promise; } export interface ExtHostAiFeaturesShape { diff --git a/src/vs/workbench/api/common/positron/extHostAiFeatures.ts b/src/vs/workbench/api/common/positron/extHostAiFeatures.ts index 75671e401d74..d270d99acf4c 100644 --- a/src/vs/workbench/api/common/positron/extHostAiFeatures.ts +++ b/src/vs/workbench/api/common/positron/extHostAiFeatures.ts @@ -153,4 +153,8 @@ export class ExtHostAiFeatures implements extHostProtocol.ExtHostAiFeaturesShape return this._proxy.$getEnabledProviders(); } + async getAllowedCommands(): Promise { + return this._proxy.$getAllowedCommands(); + } + } diff --git a/src/vs/workbench/contrib/positronAssistant/browser/positronAssistant.contribution.ts b/src/vs/workbench/contrib/positronAssistant/browser/positronAssistant.contribution.ts index c535ed290b0a..93af5303f79a 100644 --- a/src/vs/workbench/contrib/positronAssistant/browser/positronAssistant.contribution.ts +++ b/src/vs/workbench/contrib/positronAssistant/browser/positronAssistant.contribution.ts @@ -7,10 +7,14 @@ import { Registry } from '../../../../platform/registry/common/platform.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { IWorkbenchContributionsRegistry, Extensions as WorkbenchExtensions, IWorkbenchContribution } from '../../../common/contributions.js'; import { LifecyclePhase } from '../../../services/lifecycle/common/lifecycle.js'; -import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'; +import { Action2, isIMenuItem, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js'; import { localize2 } from '../../../../nls.js'; +import { Categories } from '../../../../platform/action/common/actionCommonCategories.js'; +import { CommandsRegistry, ICommandService } from '../../../../platform/commands/common/commands.js'; +import { EditorExtensionsRegistry, ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; +import { IEditorService } from '../../../services/editor/common/editorService.js'; +import { IUntitledTextResourceEditorInput } from '../../../common/editor.js'; import { codiconsLibrary } from '../../../../base/common/codiconsLibrary.js'; -import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js'; import { ICodeBlockActionContext } from '../../chat/browser/widget/chatContentParts/codeBlockPart.js'; import { IPositronConsoleService } from '../../../services/positronConsole/browser/interfaces/positronConsoleService.js'; import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js'; @@ -18,7 +22,6 @@ import { ChatContextKeys } from '../../chat/common/actions/chatContextKeys.js'; import { RuntimeCodeExecutionMode } from '../../../services/languageRuntime/common/languageRuntimeService.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { INotificationService } from '../../../../platform/notification/common/notification.js'; -import { ICommandService } from '../../../../platform/commands/common/commands.js'; import { ChatAgentLocation, ChatConfiguration } from '../../chat/common/constants.js'; import { CodeAttributionSource, IConsoleCodeAttribution } from '../../../services/positronConsole/common/positronConsoleCodeExecution.js'; import { EditorContextKeys } from '../../../../editor/common/editorContextKeys.js'; @@ -121,3 +124,70 @@ class PositronAssistantContribution extends Disposable implements IWorkbenchCont Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(PositronAssistantContribution, LifecyclePhase.Restored); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(NextEditSuggestionsStatusBarEntry, LifecyclePhase.Restored); Registry.as(WorkbenchExtensions.Workbench).registerWorkbenchContribution(CommitMessageMenuContribution, LifecyclePhase.Restored); + +registerAction2(class ShowAllowedCommandsAction extends Action2 { + constructor() { + super({ + id: 'positron.assistant.showAllowedCommands', + title: localize2('positron.assistant.showAllowedCommands', 'Show Allowed AI Commands'), + category: Categories.Developer, + f1: true, + }); + } + + override async run(accessor: ServicesAccessor): Promise { + const paletteItemTitles = new Map(); + for (const item of MenuRegistry.getMenuItems(MenuId.CommandPalette)) { + if (isIMenuItem(item)) { + const { id, title } = item.command; + if (title) { + paletteItemTitles.set(id, typeof title === 'string' ? title : title.value); + } + } + } + + const editorActionLabels = new Map(); + for (const action of EditorExtensionsRegistry.getEditorActions()) { + editorActionLabels.set(action.id, action.label); + } + + const menuCommands = MenuRegistry.getCommands(); + const result = []; + + for (const [id, command] of CommandsRegistry.getCommands()) { + if (id.startsWith('_')) { + continue; + } + + const meta = command.metadata; + const menuCmd = menuCommands.get(id); + + let description: string | undefined; + if (meta?.description) { + description = typeof meta.description === 'string' ? meta.description : meta.description.value; + } else if (menuCmd) { + const t = menuCmd.title; + description = typeof t === 'string' ? t : t.value; + } else { + description = paletteItemTitles.get(id) ?? editorActionLabels.get(id); + } + + const cmdSource = menuCmd?.source; + result.push({ + id, + description, + args: meta?.args?.map(a => ({ name: a.name, description: a.description, isOptional: a.isOptional })), + returns: meta?.returns, + source: cmdSource + ? { type: 'extension', id: cmdSource.id, displayName: cmdSource.title } + : { type: 'builtin' }, + }); + } + + await accessor.get(IEditorService).openEditor({ + resource: undefined, + contents: JSON.stringify(result, null, 2), + languageId: 'json', + } satisfies IUntitledTextResourceEditorInput); + } +});