Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions src/positron-dts/positron.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<boolean>;

/**
* 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';

@dhruvisompura dhruvisompura Jul 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎉 ooh, it would be great if we could filter on extension vs builtin commands in the future (I don't think we'll need it for the curated Assistant experience).

/** 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<AllowedCommand[]>;
}

/**
Expand Down
74 changes: 73 additions & 1 deletion src/vs/workbench/api/browser/positron/mainThreadAiFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 {
Expand Down Expand Up @@ -186,4 +189,73 @@ export class MainThreadAiFeatures extends Disposable implements MainThreadAiFeat
async $getEnabledProviders(): Promise<string[]> {
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<ISerializedAllowedCommand[]> {

@dhruvisompura dhruvisompura Jul 16, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For this, I was thinking we may want to limit the commands we expose from this endpoint to be ones that we also show in the command palette (f1: true). This would ensure that the user can also lookup/validate the commands the Assistant is running from this endpoint. We're more likely to have some documentation of those commands as well.

Or we allow filtering on f1 at the very least!

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<string, string>();
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<string, string>();
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;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -522,6 +522,9 @@ export function createPositronApiFactoryAndRegisterActors(accessor: ServicesAcce
getEnabledProviders(): Thenable<string[]> {
return extHostAiFeatures.getEnabledProviders();
},
getAllowedCommands(): Thenable<positron.ai.AllowedCommand[]> {
return extHostAiFeatures.getAllowedCommands();
},
LanguageModelAutoconfigureType: extHostTypes.LanguageModelAutoconfigureType
};

Expand Down
23 changes: 23 additions & 0 deletions src/vs/workbench/api/common/positron/extHost.positron.protocol.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<void>;
$unregisterChatAgent(id: string): void;
Expand All @@ -338,6 +360,7 @@ export interface MainThreadAiFeaturesShape {
$getProviders(): Thenable<IPositronChatProvider[]>;
$setCurrentProvider(id: string): Thenable<IPositronChatProvider | undefined>;
$getEnabledProviders(): Thenable<string[]>;
$getAllowedCommands(): Promise<ISerializedAllowedCommand[]>;
}

export interface ExtHostAiFeaturesShape {
Expand Down
4 changes: 4 additions & 0 deletions src/vs/workbench/api/common/positron/extHostAiFeatures.ts
Original file line number Diff line number Diff line change
Expand Up @@ -153,4 +153,8 @@ export class ExtHostAiFeatures implements extHostProtocol.ExtHostAiFeaturesShape
return this._proxy.$getEnabledProviders();
}

async getAllowedCommands(): Promise<extHostProtocol.ISerializedAllowedCommand[]> {
return this._proxy.$getAllowedCommands();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,18 +7,21 @@ 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';
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';
Expand Down Expand Up @@ -121,3 +124,70 @@ class PositronAssistantContribution extends Disposable implements IWorkbenchCont
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(PositronAssistantContribution, LifecyclePhase.Restored);
Registry.as<IWorkbenchContributionsRegistry>(WorkbenchExtensions.Workbench).registerWorkbenchContribution(NextEditSuggestionsStatusBarEntry, LifecyclePhase.Restored);
Registry.as<IWorkbenchContributionsRegistry>(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<void> {
const paletteItemTitles = new Map<string, string>();
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<string, string>();
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);
}
});
Loading